From 0c0b6bf96756d2b7e8f73a79a5af2fb66c35ea11 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Wed, 20 Aug 2025 12:33:46 -0700 Subject: [PATCH 01/22] fix(oauth): gdrive picker race condition, token route cleanup --- apps/sim/app/api/auth/oauth/token/route.ts | 11 ++-- apps/sim/app/api/auth/oauth/utils.ts | 54 ++++++++++--------- .../components/google-drive-picker.tsx | 9 ++-- 3 files changed, 38 insertions(+), 36 deletions(-) diff --git a/apps/sim/app/api/auth/oauth/token/route.ts b/apps/sim/app/api/auth/oauth/token/route.ts index 788d1f7d7d..15e7bf95d8 100644 --- a/apps/sim/app/api/auth/oauth/token/route.ts +++ b/apps/sim/app/api/auth/oauth/token/route.ts @@ -84,15 +84,12 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: 'Credential not found' }, { status: 404 }) } - // Check if the access token is valid - if (!credential.accessToken) { - logger.warn(`[${requestId}] No access token available for credential`) - return NextResponse.json({ error: 'No access token available' }, { status: 400 }) - } - try { - // Refresh the token if needed const { accessToken } = await refreshTokenIfNeeded(requestId, credential, credentialId) + if (!accessToken) { + logger.warn(`[${requestId}] No access token could be obtained for credential`) + return NextResponse.json({ error: 'No access token available' }, { status: 400 }) + } return NextResponse.json({ accessToken }, { status: 200 }) } catch (_error) { return NextResponse.json({ error: 'Failed to refresh access token' }, { status: 401 }) diff --git a/apps/sim/app/api/auth/oauth/utils.ts b/apps/sim/app/api/auth/oauth/utils.ts index b9f31c2334..666e20a094 100644 --- a/apps/sim/app/api/auth/oauth/utils.ts +++ b/apps/sim/app/api/auth/oauth/utils.ts @@ -1,4 +1,4 @@ -import { and, eq } from 'drizzle-orm' +import { and, desc, eq } from 'drizzle-orm' import { getSession } from '@/lib/auth' import { createLogger } from '@/lib/logs/console/logger' import { refreshOAuthToken } from '@/lib/oauth/oauth' @@ -70,7 +70,8 @@ export async function getOAuthToken(userId: string, providerId: string): Promise }) .from(account) .where(and(eq(account.userId, userId), eq(account.providerId, providerId))) - .orderBy(account.createdAt) + // Always use the most recently updated credential for this provider + .orderBy(desc(account.updatedAt)) .limit(1) if (connections.length === 0) { @@ -80,19 +81,13 @@ export async function getOAuthToken(userId: string, providerId: string): Promise const credential = connections[0] - // Check if we have a valid access token - if (!credential.accessToken) { - logger.warn(`Access token is null for user ${userId}, provider ${providerId}`) - return null - } - - // Check if the token is expired and needs refreshing + // Determine whether we should refresh: missing token OR expired token const now = new Date() const tokenExpiry = credential.accessTokenExpiresAt - // Only refresh if we have an expiration time AND it's expired AND we have a refresh token - const needsRefresh = tokenExpiry && tokenExpiry < now && !!credential.refreshToken + const shouldAttemptRefresh = + !!credential.refreshToken && (!credential.accessToken || (tokenExpiry && tokenExpiry < now)) - if (needsRefresh) { + if (shouldAttemptRefresh) { logger.info( `Access token expired for user ${userId}, provider ${providerId}. Attempting to refresh.` ) @@ -141,6 +136,13 @@ export async function getOAuthToken(userId: string, providerId: string): Promise } } + if (!credential.accessToken) { + logger.warn( + `Access token is null and no refresh attempted or available for user ${userId}, provider ${providerId}` + ) + return null + } + logger.info(`Found valid OAuth token for user ${userId}, provider ${providerId}`) return credential.accessToken } @@ -164,19 +166,21 @@ export async function refreshAccessTokenIfNeeded( return null } - // Check if we need to refresh the token + // Decide if we should refresh: token missing OR expired const expiresAt = credential.accessTokenExpiresAt const now = new Date() - // Only refresh if we have an expiration time AND it's expired - // If no expiration time is set (newly created credentials), assume token is valid - const needsRefresh = expiresAt && expiresAt <= now + const shouldRefresh = + !!credential.refreshToken && (!credential.accessToken || (expiresAt && expiresAt <= now)) const accessToken = credential.accessToken - if (needsRefresh && credential.refreshToken) { + if (shouldRefresh) { logger.info(`[${requestId}] Token expired, attempting to refresh for credential`) try { - const refreshedToken = await refreshOAuthToken(credential.providerId, credential.refreshToken) + const refreshedToken = await refreshOAuthToken( + credential.providerId, + credential.refreshToken! + ) if (!refreshedToken) { logger.error(`[${requestId}] Failed to refresh token for credential: ${credentialId}`, { @@ -217,6 +221,7 @@ export async function refreshAccessTokenIfNeeded( return null } } else if (!accessToken) { + // We have no access token and either no refresh token or not eligible to refresh logger.error(`[${requestId}] Missing access token for credential`) return null } @@ -233,21 +238,20 @@ export async function refreshTokenIfNeeded( credential: any, credentialId: string ): Promise<{ accessToken: string; refreshed: boolean }> { - // Check if we need to refresh the token + // Decide if we should refresh: token missing OR expired const expiresAt = credential.accessTokenExpiresAt const now = new Date() - // Only refresh if we have an expiration time AND it's expired - // If no expiration time is set (newly created credentials), assume token is valid - const needsRefresh = expiresAt && expiresAt <= now + const shouldRefresh = + !!credential.refreshToken && (!credential.accessToken || (expiresAt && expiresAt <= now)) - // If token is still valid, return it directly - if (!needsRefresh || !credential.refreshToken) { + // If token appears valid and present, return it directly + if (!shouldRefresh) { logger.info(`[${requestId}] Access token is valid`) return { accessToken: credential.accessToken, refreshed: false } } try { - const refreshResult = await refreshOAuthToken(credential.providerId, credential.refreshToken) + const refreshResult = await refreshOAuthToken(credential.providerId, credential.refreshToken!) if (!refreshResult) { logger.error(`[${requestId}] Failed to refresh token for credential`) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/google-drive-picker.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/google-drive-picker.tsx index c11b63bfcf..9648f5ac57 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/google-drive-picker.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/google-drive-picker.tsx @@ -237,10 +237,11 @@ export function GoogleDrivePicker({ setIsLoading(true) try { - const url = new URL('/api/auth/oauth/token', window.location.origin) - url.searchParams.set('credentialId', effectiveCredentialId) - // include workflowId if available via global registry (server adds session owner otherwise) - const response = await fetch(url.toString()) + const response = await fetch('/api/auth/oauth/token', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ credentialId: effectiveCredentialId, workflowId }), + }) if (!response.ok) { throw new Error(`Failed to fetch access token: ${response.status}`) From 9a5b035822cebacd7bc1fbfe13bb274ba95a53e6 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Wed, 20 Aug 2025 13:55:54 -0700 Subject: [PATCH 02/22] fix test --- apps/sim/app/api/auth/oauth/token/route.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/sim/app/api/auth/oauth/token/route.ts b/apps/sim/app/api/auth/oauth/token/route.ts index 15e7bf95d8..f6416ef010 100644 --- a/apps/sim/app/api/auth/oauth/token/route.ts +++ b/apps/sim/app/api/auth/oauth/token/route.ts @@ -84,12 +84,13 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: 'Credential not found' }, { status: 404 }) } + if (!credential.accessToken) { + logger.warn(`[${requestId}] No access token available for credential`) + return NextResponse.json({ error: 'No access token available' }, { status: 400 }) + } + try { const { accessToken } = await refreshTokenIfNeeded(requestId, credential, credentialId) - if (!accessToken) { - logger.warn(`[${requestId}] No access token could be obtained for credential`) - return NextResponse.json({ error: 'No access token available' }, { status: 400 }) - } return NextResponse.json({ accessToken }, { status: 200 }) } catch (_error) { return NextResponse.json({ error: 'Failed to refresh access token' }, { status: 401 }) From 6fd6f921dc61c018d930d4529a7f461beb3a0c00 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 20 Aug 2025 16:02:49 -0700 Subject: [PATCH 03/22] feat(mailer): consolidated all emailing to mailer service, added support for Azure ACS (#1054) * feat(mailer): consolidated all emailing to mailer service, added support for Azure ACS * fix batch invitation email template * cleanup * improvement(emails): add help template instead of doing it inline --- apps/sim/app/api/help/route.ts | 81 ++- .../app/api/workspaces/invitations/route.ts | 28 +- apps/sim/components/emails/base-styles.ts | 6 +- .../emails/batch-invitation-email.tsx | 304 ++++------- .../emails/help-confirmation-email.tsx | 136 +++++ apps/sim/components/emails/index.ts | 1 + apps/sim/components/emails/render-email.ts | 19 + apps/sim/lib/auth.ts | 65 +-- apps/sim/lib/email/mailer.test.ts | 196 ++++++- apps/sim/lib/email/mailer.ts | 490 ++++++++++-------- apps/sim/lib/env.ts | 2 + apps/sim/package.json | 2 + bun.lock | 386 ++++++++++++-- 13 files changed, 1147 insertions(+), 569 deletions(-) create mode 100644 apps/sim/components/emails/help-confirmation-email.tsx diff --git a/apps/sim/app/api/help/route.ts b/apps/sim/app/api/help/route.ts index abd6ae29a5..23d4ca4a0e 100644 --- a/apps/sim/app/api/help/route.ts +++ b/apps/sim/app/api/help/route.ts @@ -1,12 +1,12 @@ import { type NextRequest, NextResponse } from 'next/server' -import { Resend } from 'resend' import { z } from 'zod' +import { renderHelpConfirmationEmail } from '@/components/emails' import { getSession } from '@/lib/auth' +import { sendEmail } from '@/lib/email/mailer' import { env } from '@/lib/env' import { createLogger } from '@/lib/logs/console/logger' import { getEmailDomain } from '@/lib/urls/utils' -const resend = env.RESEND_API_KEY ? new Resend(env.RESEND_API_KEY) : null const logger = createLogger('HelpAPI') const helpFormSchema = z.object({ @@ -28,18 +28,6 @@ export async function POST(req: NextRequest) { const email = session.user.email - // Check if Resend API key is configured - if (!resend) { - logger.error(`[${requestId}] RESEND_API_KEY not configured`) - return NextResponse.json( - { - error: - 'Email service not configured. Please set RESEND_API_KEY in environment variables.', - }, - { status: 500 } - ) - } - // Handle multipart form data const formData = await req.formData() @@ -54,18 +42,18 @@ export async function POST(req: NextRequest) { }) // Validate the form data - const result = helpFormSchema.safeParse({ + const validationResult = helpFormSchema.safeParse({ subject, message, type, }) - if (!result.success) { + if (!validationResult.success) { logger.warn(`[${requestId}] Invalid help request data`, { - errors: result.error.format(), + errors: validationResult.error.format(), }) return NextResponse.json( - { error: 'Invalid request data', details: result.error.format() }, + { error: 'Invalid request data', details: validationResult.error.format() }, { status: 400 } ) } @@ -103,63 +91,60 @@ ${message} emailText += `\n\n${images.length} image(s) attached.` } - // Send email using Resend - const { error } = await resend.emails.send({ - from: `Sim `, + const emailResult = await sendEmail({ to: [`help@${env.EMAIL_DOMAIN || getEmailDomain()}`], subject: `[${type.toUpperCase()}] ${subject}`, - replyTo: email, text: emailText, + from: `${env.SENDER_NAME || 'Sim'} `, + replyTo: email, + emailType: 'transactional', attachments: images.map((image) => ({ filename: image.filename, content: image.content.toString('base64'), contentType: image.contentType, - disposition: 'attachment', // Explicitly set as attachment + disposition: 'attachment', })), }) - if (error) { - logger.error(`[${requestId}] Error sending help request email`, error) + if (!emailResult.success) { + logger.error(`[${requestId}] Error sending help request email`, emailResult.message) return NextResponse.json({ error: 'Failed to send email' }, { status: 500 }) } logger.info(`[${requestId}] Help request email sent successfully`) // Send confirmation email to the user - await resend.emails - .send({ - from: `Sim `, + try { + const confirmationHtml = await renderHelpConfirmationEmail( + email, + type as 'bug' | 'feedback' | 'feature_request' | 'other', + images.length + ) + + await sendEmail({ to: [email], subject: `Your ${type} request has been received: ${subject}`, - text: ` -Hello, - -Thank you for your ${type} submission. We've received your request and will get back to you as soon as possible. - -Your message: -${message} - -${images.length > 0 ? `You attached ${images.length} image(s).` : ''} - -Best regards, -The Sim Team - `, + html: confirmationHtml, + from: `${env.SENDER_NAME || 'Sim'} `, replyTo: `help@${env.EMAIL_DOMAIN || getEmailDomain()}`, + emailType: 'transactional', }) - .catch((err) => { - logger.warn(`[${requestId}] Failed to send confirmation email`, err) - }) + } catch (err) { + logger.warn(`[${requestId}] Failed to send confirmation email`, err) + } return NextResponse.json( { success: true, message: 'Help request submitted successfully' }, { status: 200 } ) } catch (error) { - // Check if error is related to missing API key - if (error instanceof Error && error.message.includes('API key')) { - logger.error(`[${requestId}] API key configuration error`, error) + if (error instanceof Error && error.message.includes('not configured')) { + logger.error(`[${requestId}] Email service configuration error`, error) return NextResponse.json( - { error: 'Email service configuration error. Please check your RESEND_API_KEY.' }, + { + error: + 'Email service configuration error. Please check your email service configuration.', + }, { status: 500 } ) } diff --git a/apps/sim/app/api/workspaces/invitations/route.ts b/apps/sim/app/api/workspaces/invitations/route.ts index 6b88f299fd..aacd9a716e 100644 --- a/apps/sim/app/api/workspaces/invitations/route.ts +++ b/apps/sim/app/api/workspaces/invitations/route.ts @@ -2,9 +2,9 @@ import { randomUUID } from 'crypto' import { render } from '@react-email/render' import { and, eq, inArray } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' -import { Resend } from 'resend' import { WorkspaceInvitationEmail } from '@/components/emails/workspace-invitation' import { getSession } from '@/lib/auth' +import { sendEmail } from '@/lib/email/mailer' import { env } from '@/lib/env' import { createLogger } from '@/lib/logs/console/logger' import { getEmailDomain } from '@/lib/urls/utils' @@ -20,7 +20,6 @@ import { export const dynamic = 'force-dynamic' const logger = createLogger('WorkspaceInvitationsAPI') -const resend = env.RESEND_API_KEY ? new Resend(env.RESEND_API_KEY) : null type PermissionType = (typeof permissionTypeEnum.enumValues)[number] @@ -241,30 +240,25 @@ async function sendInvitationEmail({ }) ) - if (!resend) { - logger.error('RESEND_API_KEY not configured') - return NextResponse.json( - { - error: - 'Email service not configured. Please set RESEND_API_KEY in environment variables.', - }, - { status: 500 } - ) - } - const emailDomain = env.EMAIL_DOMAIN || getEmailDomain() - const fromAddress = `noreply@${emailDomain}` + const fromAddress = `${env.SENDER_NAME || 'Sim'} ` logger.info(`Attempting to send email from ${fromAddress} to ${to}`) - const result = await resend.emails.send({ - from: fromAddress, + const result = await sendEmail({ to, subject: `You've been invited to join "${workspaceName}" on Sim`, html: emailHtml, + from: fromAddress, + emailType: 'transactional', + useCustomFromFormat: true, }) - logger.info(`Invitation email sent successfully to ${to}`, { result }) + if (result.success) { + logger.info(`Invitation email sent successfully to ${to}`, { result }) + } else { + logger.error(`Failed to send invitation email to ${to}`, { error: result.message }) + } } catch (error) { logger.error('Error sending invitation email:', error) // Continue even if email fails - the invitation is still created diff --git a/apps/sim/components/emails/base-styles.ts b/apps/sim/components/emails/base-styles.ts index 4afe17d263..4568984850 100644 --- a/apps/sim/components/emails/base-styles.ts +++ b/apps/sim/components/emails/base-styles.ts @@ -31,7 +31,7 @@ export const baseStyles = { }, button: { display: 'inline-block', - backgroundColor: 'var(--brand-primary-hover-hex)', + backgroundColor: '#802FFF', color: '#ffffff', fontWeight: 'bold', fontSize: '16px', @@ -42,7 +42,7 @@ export const baseStyles = { margin: '20px 0', }, link: { - color: 'var(--brand-primary-hover-hex)', + color: '#802FFF', textDecoration: 'underline', }, footer: { @@ -79,7 +79,7 @@ export const baseStyles = { width: '249px', }, sectionCenter: { - borderBottom: '1px solid var(--brand-primary-hover-hex)', + borderBottom: '1px solid #802FFF', width: '102px', }, } diff --git a/apps/sim/components/emails/batch-invitation-email.tsx b/apps/sim/components/emails/batch-invitation-email.tsx index 997f367419..83b706ae04 100644 --- a/apps/sim/components/emails/batch-invitation-email.tsx +++ b/apps/sim/components/emails/batch-invitation-email.tsx @@ -1,17 +1,21 @@ import { Body, - Button, + Column, Container, Head, - Heading, - Hr, Html, Img, + Link, Preview, + Row, Section, Text, } from '@react-email/components' import { getBrandConfig } from '@/lib/branding/branding' +import { env } from '@/lib/env' +import { getAssetUrl } from '@/lib/utils' +import { baseStyles } from './base-styles' +import EmailFooter from './footer' interface WorkspaceInvitation { workspaceId: string @@ -27,6 +31,8 @@ interface BatchInvitationEmailProps { acceptUrl: string } +const baseUrl = env.NEXT_PUBLIC_APP_URL || 'https://sim.ai' + const getPermissionLabel = (permission: string) => { switch (permission) { case 'admin': @@ -43,9 +49,9 @@ const getPermissionLabel = (permission: string) => { const getRoleLabel = (role: string) => { switch (role) { case 'admin': - return 'Team Admin (can manage team and billing)' + return 'Admin' case 'member': - return 'Team Member (billing access only)' + return 'Member' default: return role } @@ -64,217 +70,101 @@ export const BatchInvitationEmail = ({ return ( - - You've been invited to join {organizationName} - {hasWorkspaces ? ` and ${workspaceInvitations.length} workspace(s)` : ''} - - - -
- {brand.name} + + + You've been invited to join {organizationName} + {hasWorkspaces ? ` and ${workspaceInvitations.length} workspace(s)` : ''} + + +
+ + + {brand.name} + +
- You're invited to join {organizationName}! - - - {inviterName} has invited you to join{' '} - {organizationName} on Sim. - - - {/* Organization Invitation Details */} -
- Team Access -
- Team Role: {getRoleLabel(organizationRole)} - - {organizationRole === 'admin' - ? "You'll be able to manage team members, billing, and workspace access." - : "You'll have access to shared team billing and can be invited to workspaces."} - -
+
+ + + + +
- {/* Workspace Invitations */} - {hasWorkspaces && ( -
- - Workspace Access ({workspaceInvitations.length} workspace - {workspaceInvitations.length !== 1 ? 's' : ''}) - - You're also being invited to the following workspaces: - - {workspaceInvitations.map((ws, index) => ( -
- {ws.workspaceName} - {getPermissionLabel(ws.permission)} -
- ))} -
- )} - -
- +
+ Hello, + + {inviterName} has invited you to join{' '} + {organizationName} on Sim. + + + {/* Team Role Information */} + + Team Role: {getRoleLabel(organizationRole)} + + + {organizationRole === 'admin' + ? "As a Team Admin, you'll be able to manage team members, billing, and workspace access." + : "As a Team Member, you'll have access to shared team billing and can be invited to workspaces."} + + + {/* Workspace Invitations */} + {hasWorkspaces && ( + <> + + + Workspace Access ({workspaceInvitations.length} workspace + {workspaceInvitations.length !== 1 ? 's' : ''}): + + + {workspaceInvitations.map((ws) => ( + + • {ws.workspaceName} - {getPermissionLabel(ws.permission)} + + ))} + + )} + + + Accept Invitation + + + + By accepting this invitation, you'll join {organizationName} + {hasWorkspaces + ? ` and gain access to ${workspaceInvitations.length} workspace(s)` + : ''} + . + + + + This invitation will expire in 7 days. If you didn't expect this invitation, you can + safely ignore this email. + + + + Best regards, +
+ The Sim Team +
- - - By accepting this invitation, you'll join {organizationName} - {hasWorkspaces ? ` and gain access to ${workspaceInvitations.length} workspace(s)` : ''} - . - - -
- - - If you have any questions, you can reach out to {inviterName} directly or contact our - support team. - - - - This invitation will expire in 7 days. If you didn't expect this invitation, you can - safely ignore this email. - + + ) } export default BatchInvitationEmail - -// Styles -const main = { - backgroundColor: '#f6f9fc', - fontFamily: - '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif', -} - -const container = { - backgroundColor: '#ffffff', - margin: '0 auto', - padding: '20px 0 48px', - marginBottom: '64px', -} - -const logoContainer = { - margin: '32px 0', - textAlign: 'center' as const, -} - -const logo = { - margin: '0 auto', -} - -const h1 = { - color: '#333', - fontSize: '24px', - fontWeight: 'bold', - margin: '40px 0', - padding: '0', - textAlign: 'center' as const, -} - -const h2 = { - color: '#333', - fontSize: '18px', - fontWeight: 'bold', - margin: '24px 0 16px 0', - padding: '0', -} - -const text = { - color: '#333', - fontSize: '16px', - lineHeight: '26px', - margin: '16px 0', - padding: '0 40px', -} - -const invitationSection = { - margin: '32px 0', - padding: '0 40px', -} - -const roleCard = { - backgroundColor: '#f8f9fa', - border: '1px solid #e9ecef', - borderRadius: '8px', - padding: '16px', - margin: '16px 0', -} - -const roleTitle = { - color: '#333', - fontSize: '16px', - fontWeight: 'bold', - margin: '0 0 8px 0', -} - -const roleDescription = { - color: '#6c757d', - fontSize: '14px', - lineHeight: '20px', - margin: '0', -} - -const workspaceCard = { - backgroundColor: '#f8f9fa', - border: '1px solid #e9ecef', - borderRadius: '6px', - padding: '12px 16px', - margin: '8px 0', - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', -} - -const workspaceName = { - color: '#333', - fontSize: '15px', - fontWeight: '500', - margin: '0', -} - -const workspacePermission = { - color: '#6c757d', - fontSize: '13px', - margin: '0', -} - -const buttonContainer = { - margin: '32px 0', - textAlign: 'center' as const, -} - -const button = { - backgroundColor: '#007bff', - borderRadius: '6px', - color: '#fff', - fontSize: '16px', - fontWeight: 'bold', - textDecoration: 'none', - textAlign: 'center' as const, - display: 'inline-block', - padding: '12px 24px', - margin: '0 auto', -} - -const hr = { - borderColor: '#e9ecef', - margin: '32px 0', -} - -const footer = { - color: '#6c757d', - fontSize: '14px', - lineHeight: '20px', - margin: '8px 0', - padding: '0 40px', -} diff --git a/apps/sim/components/emails/help-confirmation-email.tsx b/apps/sim/components/emails/help-confirmation-email.tsx new file mode 100644 index 0000000000..73d98533a4 --- /dev/null +++ b/apps/sim/components/emails/help-confirmation-email.tsx @@ -0,0 +1,136 @@ +import { + Body, + Column, + Container, + Head, + Html, + Img, + Preview, + Row, + Section, + Text, +} from '@react-email/components' +import { format } from 'date-fns' +import { getBrandConfig } from '@/lib/branding/branding' +import { env } from '@/lib/env' +import { getAssetUrl } from '@/lib/utils' +import { baseStyles } from './base-styles' +import EmailFooter from './footer' + +interface HelpConfirmationEmailProps { + userEmail?: string + type?: 'bug' | 'feedback' | 'feature_request' | 'other' + attachmentCount?: number + submittedDate?: Date +} + +const baseUrl = env.NEXT_PUBLIC_APP_URL || 'https://sim.ai' + +const getTypeLabel = (type: string) => { + switch (type) { + case 'bug': + return 'Bug Report' + case 'feedback': + return 'Feedback' + case 'feature_request': + return 'Feature Request' + case 'other': + return 'General Inquiry' + default: + return 'Request' + } +} + +export const HelpConfirmationEmail = ({ + userEmail = '', + type = 'other', + attachmentCount = 0, + submittedDate = new Date(), +}: HelpConfirmationEmailProps) => { + const brand = getBrandConfig() + const typeLabel = getTypeLabel(type) + + return ( + + + + Your {typeLabel.toLowerCase()} has been received + +
+ + + {brand.name} + + +
+ +
+ + + + + +
+ +
+ Hello, + + Thank you for your {typeLabel.toLowerCase()} submission. We've + received your request and will get back to you as soon as possible. + + + {attachmentCount > 0 && ( + + You attached{' '} + + {attachmentCount} image{attachmentCount > 1 ? 's' : ''} + {' '} + with your request. + + )} + + + We typically respond to{' '} + {type === 'bug' + ? 'bug reports' + : type === 'feature_request' + ? 'feature requests' + : 'inquiries'}{' '} + within a few hours. If you need immediate assistance, please don't hesitate to reach + out to us directly. + + + + Best regards, +
+ The {brand.name} Team +
+ + + This confirmation was sent on {format(submittedDate, 'MMMM do, yyyy')} for your{' '} + {typeLabel.toLowerCase()} submission from {userEmail}. + +
+
+ + + + + ) +} + +export default HelpConfirmationEmail diff --git a/apps/sim/components/emails/index.ts b/apps/sim/components/emails/index.ts index a58b287ad6..60aeb1f092 100644 --- a/apps/sim/components/emails/index.ts +++ b/apps/sim/components/emails/index.ts @@ -1,6 +1,7 @@ export * from './base-styles' export { BatchInvitationEmail } from './batch-invitation-email' export { default as EmailFooter } from './footer' +export { HelpConfirmationEmail } from './help-confirmation-email' export { InvitationEmail } from './invitation-email' export { OTPVerificationEmail } from './otp-verification-email' export * from './render-email' diff --git a/apps/sim/components/emails/render-email.ts b/apps/sim/components/emails/render-email.ts index 0639eac81f..c78de363cb 100644 --- a/apps/sim/components/emails/render-email.ts +++ b/apps/sim/components/emails/render-email.ts @@ -1,6 +1,7 @@ import { render } from '@react-email/components' import { BatchInvitationEmail, + HelpConfirmationEmail, InvitationEmail, OTPVerificationEmail, ResetPasswordEmail, @@ -65,6 +66,21 @@ export async function renderBatchInvitationEmail( ) } +export async function renderHelpConfirmationEmail( + userEmail: string, + type: 'bug' | 'feedback' | 'feature_request' | 'other', + attachmentCount = 0 +): Promise { + return await render( + HelpConfirmationEmail({ + userEmail, + type, + attachmentCount, + submittedDate: new Date(), + }) + ) +} + export function getEmailSubject( type: | 'sign-in' @@ -73,6 +89,7 @@ export function getEmailSubject( | 'reset-password' | 'invitation' | 'batch-invitation' + | 'help-confirmation' ): string { switch (type) { case 'sign-in': @@ -87,6 +104,8 @@ export function getEmailSubject( return "You've been invited to join a team on Sim" case 'batch-invitation': return "You've been invited to join a team and workspaces on Sim" + case 'help-confirmation': + return 'Your request has been received' default: return 'Sim' } diff --git a/apps/sim/lib/auth.ts b/apps/sim/lib/auth.ts index 95466867cf..a2694372b3 100644 --- a/apps/sim/lib/auth.ts +++ b/apps/sim/lib/auth.ts @@ -11,7 +11,6 @@ import { } from 'better-auth/plugins' import { and, eq } from 'drizzle-orm' import { headers } from 'next/headers' -import { Resend } from 'resend' import Stripe from 'stripe' import { getEmailSubject, @@ -21,6 +20,7 @@ import { } from '@/components/emails/render-email' import { getBaseURL } from '@/lib/auth-client' import { DEFAULT_FREE_CREDITS } from '@/lib/billing/constants' +import { sendEmail } from '@/lib/email/mailer' import { quickValidateEmail } from '@/lib/email/validation' import { env, isTruthy } from '@/lib/env' import { isBillingEnabled, isProd } from '@/lib/environment' @@ -45,22 +45,6 @@ if (validStripeKey) { }) } -// If there is no resend key, it might be a local dev environment -// In that case, we don't want to send emails and just log them -const validResendAPIKEY = - env.RESEND_API_KEY && env.RESEND_API_KEY.trim() !== '' && env.RESEND_API_KEY !== 'placeholder' - -const resend = validResendAPIKEY - ? new Resend(env.RESEND_API_KEY) - : { - emails: { - send: async (...args: any[]) => { - logger.info('Email would have been sent in production. Details:', args) - return { id: 'local-dev-mode' } - }, - }, - } - export const auth = betterAuth({ baseURL: getBaseURL(), trustedOrigins: [ @@ -165,15 +149,16 @@ export const auth = betterAuth({ const html = await renderPasswordResetEmail(username, url) - const result = await resend.emails.send({ - from: `Sim `, + const result = await sendEmail({ to: user.email, subject: getEmailSubject('reset-password'), html, + from: `noreply@${env.EMAIL_DOMAIN || getEmailDomain()}`, + emailType: 'transactional', }) - if (!result) { - throw new Error('Failed to send reset password email') + if (!result.success) { + throw new Error(`Failed to send reset password email: ${result.message}`) } }, }, @@ -252,8 +237,19 @@ export const auth = betterAuth({ ) } - // In development with no RESEND_API_KEY, log verification code - if (!validResendAPIKEY) { + const html = await renderOTPEmail(data.otp, data.email, data.type) + + // Send email via consolidated mailer (supports Resend, Azure, or logging fallback) + const result = await sendEmail({ + to: data.email, + subject: getEmailSubject(data.type), + html, + from: `onboarding@${env.EMAIL_DOMAIN || getEmailDomain()}`, + emailType: 'transactional', + }) + + // If no email service is configured, log verification code for development + if (!result.success && result.message.includes('no email service configured')) { logger.info('🔑 VERIFICATION CODE FOR LOGIN/SIGNUP', { email: data.email, otp: data.otp, @@ -263,18 +259,8 @@ export const auth = betterAuth({ return } - const html = await renderOTPEmail(data.otp, data.email, data.type) - - // In production, send an actual email - const result = await resend.emails.send({ - from: `Sim `, - to: data.email, - subject: getEmailSubject(data.type), - html, - }) - - if (!result) { - throw new Error('Failed to send verification code') + if (!result.success) { + throw new Error(`Failed to send verification code: ${result.message}`) } } catch (error) { logger.error('Error sending verification code:', { @@ -1551,12 +1537,17 @@ export const auth = betterAuth({ invitation.email ) - await resend.emails.send({ - from: `Sim `, + const result = await sendEmail({ to: invitation.email, subject: `${inviterName} has invited you to join ${organization.name} on Sim`, html, + from: `noreply@${env.EMAIL_DOMAIN || getEmailDomain()}`, + emailType: 'transactional', }) + + if (!result.success) { + logger.error('Failed to send organization invitation email:', result.message) + } } catch (error) { logger.error('Error sending invitation email', { error }) } diff --git a/apps/sim/lib/email/mailer.test.ts b/apps/sim/lib/email/mailer.test.ts index f33d4a39e6..b527910bc1 100644 --- a/apps/sim/lib/email/mailer.test.ts +++ b/apps/sim/lib/email/mailer.test.ts @@ -1,6 +1,9 @@ import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest' const mockSend = vi.fn() +const mockBatchSend = vi.fn() +const mockAzureBeginSend = vi.fn() +const mockAzurePollUntilDone = vi.fn() vi.mock('resend', () => { return { @@ -8,6 +11,17 @@ vi.mock('resend', () => { emails: { send: (...args: any[]) => mockSend(...args), }, + batch: { + send: (...args: any[]) => mockBatchSend(...args), + }, + })), + } +}) + +vi.mock('@azure/communication-email', () => { + return { + EmailClient: vi.fn().mockImplementation(() => ({ + beginSend: (...args: any[]) => mockAzureBeginSend(...args), })), } }) @@ -20,7 +34,10 @@ vi.mock('@/lib/email/unsubscribe', () => ({ vi.mock('@/lib/env', () => ({ env: { RESEND_API_KEY: 'test-api-key', + AZURE_ACS_CONNECTION_STRING: 'test-azure-connection-string', + AZURE_COMMUNICATION_EMAIL_DOMAIN: 'test.azurecomm.net', NEXT_PUBLIC_APP_URL: 'https://test.sim.ai', + SENDER_NAME: 'Sim', }, })) @@ -28,7 +45,7 @@ vi.mock('@/lib/urls/utils', () => ({ getEmailDomain: vi.fn().mockReturnValue('sim.ai'), })) -import { type EmailType, sendEmail } from '@/lib/email/mailer' +import { type EmailType, sendBatchEmails, sendEmail } from '@/lib/email/mailer' import { generateUnsubscribeToken, isUnsubscribed } from '@/lib/email/unsubscribe' describe('mailer', () => { @@ -42,10 +59,27 @@ describe('mailer', () => { vi.clearAllMocks() ;(isUnsubscribed as Mock).mockResolvedValue(false) ;(generateUnsubscribeToken as Mock).mockReturnValue('mock-token-123') + + // Mock successful Resend response mockSend.mockResolvedValue({ data: { id: 'test-email-id' }, error: null, }) + + mockBatchSend.mockResolvedValue({ + data: [{ id: 'batch-email-1' }, { id: 'batch-email-2' }], + error: null, + }) + + // Mock successful Azure response + mockAzurePollUntilDone.mockResolvedValue({ + status: 'Succeeded', + id: 'azure-email-id', + }) + + mockAzureBeginSend.mockReturnValue({ + pollUntilDone: mockAzurePollUntilDone, + }) }) describe('sendEmail', () => { @@ -56,7 +90,7 @@ describe('mailer', () => { }) expect(result.success).toBe(true) - expect(result.message).toBe('Email sent successfully') + expect(result.message).toBe('Email sent successfully via Resend') expect(result.data).toEqual({ id: 'test-email-id' }) // Should not check unsubscribe status for transactional emails @@ -119,7 +153,8 @@ describe('mailer', () => { expect(mockSend).not.toHaveBeenCalled() }) - it.concurrent('should handle Resend API errors', async () => { + it.concurrent('should handle Resend API errors and fallback to Azure', async () => { + // Mock Resend to fail mockSend.mockResolvedValue({ data: null, error: { message: 'API rate limit exceeded' }, @@ -127,17 +162,32 @@ describe('mailer', () => { const result = await sendEmail(testEmailOptions) - expect(result.success).toBe(false) - expect(result.message).toBe('API rate limit exceeded') + expect(result.success).toBe(true) + expect(result.message).toBe('Email sent successfully via Azure Communication Services') + expect(result.data).toEqual({ id: 'azure-email-id' }) + + // Should have tried Resend first + expect(mockSend).toHaveBeenCalled() + + // Should have fallen back to Azure + expect(mockAzureBeginSend).toHaveBeenCalled() }) - it.concurrent('should handle unexpected errors', async () => { + it.concurrent('should handle unexpected errors and fallback to Azure', async () => { + // Mock Resend to throw an error mockSend.mockRejectedValue(new Error('Network error')) const result = await sendEmail(testEmailOptions) - expect(result.success).toBe(false) - expect(result.message).toBe('Failed to send email') + expect(result.success).toBe(true) + expect(result.message).toBe('Email sent successfully via Azure Communication Services') + expect(result.data).toEqual({ id: 'azure-email-id' }) + + // Should have tried Resend first + expect(mockSend).toHaveBeenCalled() + + // Should have fallen back to Azure + expect(mockAzureBeginSend).toHaveBeenCalled() }) it.concurrent('should use custom from address when provided', async () => { @@ -168,6 +218,23 @@ describe('mailer', () => { ) }) + it('should use custom from format when useCustomFromFormat is true', async () => { + const result = await sendEmail({ + ...testEmailOptions, + from: 'Sim ', + useCustomFromFormat: true, + }) + + expect(result.success).toBe(true) + + // Should call Resend with the exact from address provided (no modification) + expect(mockSend).toHaveBeenCalledWith( + expect.objectContaining({ + from: 'Sim ', // Uses custom format as-is + }) + ) + }) + it.concurrent('should replace unsubscribe token placeholders in HTML', async () => { const htmlWithPlaceholder = '

Content

Unsubscribe' @@ -184,4 +251,117 @@ describe('mailer', () => { ) }) }) + + describe('Azure Communication Services fallback', () => { + it('should fallback to Azure when Resend fails', async () => { + // Mock Resend to fail + mockSend.mockRejectedValue(new Error('Resend service unavailable')) + + const result = await sendEmail({ + ...testEmailOptions, + emailType: 'transactional', + }) + + expect(result.success).toBe(true) + expect(result.message).toBe('Email sent successfully via Azure Communication Services') + expect(result.data).toEqual({ id: 'azure-email-id' }) + + // Should have tried Resend first + expect(mockSend).toHaveBeenCalled() + + // Should have fallen back to Azure + expect(mockAzureBeginSend).toHaveBeenCalledWith({ + senderAddress: 'noreply@sim.ai', + content: { + subject: testEmailOptions.subject, + html: testEmailOptions.html, + }, + recipients: { + to: [{ address: testEmailOptions.to }], + }, + headers: {}, + }) + }) + + it('should handle Azure Communication Services failure', async () => { + // Mock both services to fail + mockSend.mockRejectedValue(new Error('Resend service unavailable')) + mockAzurePollUntilDone.mockResolvedValue({ + status: 'Failed', + id: 'failed-id', + }) + + const result = await sendEmail({ + ...testEmailOptions, + emailType: 'transactional', + }) + + expect(result.success).toBe(false) + expect(result.message).toBe('Both Resend and Azure Communication Services failed') + + // Should have tried both services + expect(mockSend).toHaveBeenCalled() + expect(mockAzureBeginSend).toHaveBeenCalled() + }) + }) + + describe('sendBatchEmails', () => { + const testBatchEmails = [ + { ...testEmailOptions, to: 'user1@example.com' }, + { ...testEmailOptions, to: 'user2@example.com' }, + ] + + it('should send batch emails via Resend successfully', async () => { + const result = await sendBatchEmails({ emails: testBatchEmails }) + + expect(result.success).toBe(true) + expect(result.message).toBe('All batch emails sent successfully via Resend') + expect(result.results).toHaveLength(2) + expect(mockBatchSend).toHaveBeenCalled() + }) + + it('should fallback to individual sends when Resend batch fails', async () => { + // Mock Resend batch to fail + mockBatchSend.mockRejectedValue(new Error('Batch service unavailable')) + + const result = await sendBatchEmails({ emails: testBatchEmails }) + + expect(result.success).toBe(true) + expect(result.message).toBe('All batch emails sent successfully') + expect(result.results).toHaveLength(2) + + // Should have tried Resend batch first + expect(mockBatchSend).toHaveBeenCalled() + + // Should have fallen back to individual sends (which will use Resend since it's available) + expect(mockSend).toHaveBeenCalledTimes(2) + }) + + it('should handle mixed success/failure in individual fallback', async () => { + // Mock Resend batch to fail + mockBatchSend.mockRejectedValue(new Error('Batch service unavailable')) + + // Mock first individual send to succeed, second to fail and Azure also fails + mockSend + .mockResolvedValueOnce({ + data: { id: 'email-1' }, + error: null, + }) + .mockRejectedValueOnce(new Error('Individual send failure')) + + // Mock Azure to fail for the second email (first call succeeds, but second fails) + mockAzurePollUntilDone.mockResolvedValue({ + status: 'Failed', + id: 'failed-id', + }) + + const result = await sendBatchEmails({ emails: testBatchEmails }) + + expect(result.success).toBe(false) + expect(result.message).toBe('1/2 emails sent successfully') + expect(result.results).toHaveLength(2) + expect(result.results[0].success).toBe(true) + expect(result.results[1].success).toBe(false) + }) + }) }) diff --git a/apps/sim/lib/email/mailer.ts b/apps/sim/lib/email/mailer.ts index 25aa227346..99ed584707 100644 --- a/apps/sim/lib/email/mailer.ts +++ b/apps/sim/lib/email/mailer.ts @@ -1,3 +1,4 @@ +import { EmailClient, type EmailMessage } from '@azure/communication-email' import { Resend } from 'resend' import { generateUnsubscribeToken, isUnsubscribed } from '@/lib/email/unsubscribe' import { env } from '@/lib/env' @@ -8,57 +9,81 @@ const logger = createLogger('Mailer') export type EmailType = 'transactional' | 'marketing' | 'updates' | 'notifications' -interface EmailOptions { - to: string +export interface EmailAttachment { + filename: string + content: string | Buffer + contentType: string + disposition?: 'attachment' | 'inline' +} + +export interface EmailOptions { + to: string | string[] subject: string - html: string + html?: string + text?: string from?: string emailType?: EmailType includeUnsubscribe?: boolean + attachments?: EmailAttachment[] + replyTo?: string + useCustomFromFormat?: boolean // If true, uses "from" as-is; if false, uses "SENDER_NAME " format } -interface BatchEmailOptions { +export interface BatchEmailOptions { emails: EmailOptions[] } -interface SendEmailResult { +export interface SendEmailResult { success: boolean message: string data?: any } -interface BatchSendEmailResult { +export interface BatchSendEmailResult { success: boolean message: string results: SendEmailResult[] data?: any } +interface ProcessedEmailData { + to: string | string[] + subject: string + html?: string + text?: string + senderEmail: string + headers: Record + attachments?: EmailAttachment[] + replyTo?: string + useCustomFromFormat: boolean +} + const resendApiKey = env.RESEND_API_KEY +const azureConnectionString = env.AZURE_ACS_CONNECTION_STRING const resend = resendApiKey && resendApiKey !== 'placeholder' && resendApiKey.trim() !== '' ? new Resend(resendApiKey) : null -export async function sendEmail({ - to, - subject, - html, - from, - emailType = 'transactional', - includeUnsubscribe = true, -}: EmailOptions): Promise { +const azureEmailClient = + azureConnectionString && azureConnectionString.trim() !== '' + ? new EmailClient(azureConnectionString) + : null + +export async function sendEmail(options: EmailOptions): Promise { try { // Check if user has unsubscribed (skip for critical transactional emails) - if (emailType !== 'transactional') { - const unsubscribeType = emailType as 'marketing' | 'updates' | 'notifications' - const hasUnsubscribed = await isUnsubscribed(to, unsubscribeType) + if (options.emailType !== 'transactional') { + const unsubscribeType = options.emailType as 'marketing' | 'updates' | 'notifications' + // For arrays, check the first email address (batch emails typically go to similar recipients) + const primaryEmail = Array.isArray(options.to) ? options.to[0] : options.to + const hasUnsubscribed = await isUnsubscribed(primaryEmail, unsubscribeType) if (hasUnsubscribed) { logger.info('Email not sent (user unsubscribed):', { - to, - subject, - emailType, + to: options.to, + subject: options.subject, + emailType: options.emailType, }) return { success: true, @@ -68,56 +93,41 @@ export async function sendEmail({ } } - const senderEmail = from || `noreply@${env.EMAIL_DOMAIN || getEmailDomain()}` + // Process email data with unsubscribe tokens and headers + const processedData = await processEmailData(options) - if (!resend) { - logger.info('Email not sent (Resend not configured):', { - to, - subject, - from: senderEmail, - }) - return { - success: true, - message: 'Email logging successful (Resend not configured)', - data: { id: 'mock-email-id' }, + // Try Resend first if configured + if (resend) { + try { + return await sendWithResend(processedData) + } catch (error) { + logger.warn('Resend failed, attempting Azure Communication Services fallback:', error) } } - // Generate unsubscribe token and add to HTML - let finalHtml = html - const headers: Record = {} - - if (includeUnsubscribe && emailType !== 'transactional') { - const unsubscribeToken = generateUnsubscribeToken(to, emailType) - const baseUrl = env.NEXT_PUBLIC_APP_URL || 'https://sim.ai' - const unsubscribeUrl = `${baseUrl}/unsubscribe?token=${unsubscribeToken}&email=${encodeURIComponent(to)}` - - headers['List-Unsubscribe'] = `<${unsubscribeUrl}>` - headers['List-Unsubscribe-Post'] = 'List-Unsubscribe=One-Click' - - finalHtml = html.replace(/\{\{UNSUBSCRIBE_TOKEN\}\}/g, unsubscribeToken) - } - - const { data, error } = await resend.emails.send({ - from: `Sim <${senderEmail}>`, - to, - subject, - html: finalHtml, - headers: Object.keys(headers).length > 0 ? headers : undefined, - }) - - if (error) { - logger.error('Resend API error:', error) - return { - success: false, - message: error.message || 'Failed to send email', + // Fallback to Azure Communication Services if configured + if (azureEmailClient) { + try { + return await sendWithAzure(processedData) + } catch (error) { + logger.error('Azure Communication Services also failed:', error) + return { + success: false, + message: 'Both Resend and Azure Communication Services failed', + } } } + // No email service configured + logger.info('Email not sent (no email service configured):', { + to: options.to, + subject: options.subject, + from: processedData.senderEmail, + }) return { success: true, - message: 'Email sent successfully', - data, + message: 'Email logging successful (no email service configured)', + data: { id: 'mock-email-id' }, } } catch (error) { logger.error('Error sending email:', error) @@ -128,183 +138,179 @@ export async function sendEmail({ } } -export async function sendBatchEmails({ - emails, -}: BatchEmailOptions): Promise { - try { - const senderEmail = `noreply@${env.EMAIL_DOMAIN || getEmailDomain()}` - const results: SendEmailResult[] = [] +async function processEmailData(options: EmailOptions): Promise { + const { + to, + subject, + html, + text, + from, + emailType = 'transactional', + includeUnsubscribe = true, + attachments, + replyTo, + useCustomFromFormat = false, + } = options + + const senderEmail = from || `noreply@${env.EMAIL_DOMAIN || getEmailDomain()}` + + // Generate unsubscribe token and add to content + let finalHtml = html + let finalText = text + const headers: Record = {} + + if (includeUnsubscribe && emailType !== 'transactional') { + // For arrays, use the first email for unsubscribe (batch emails typically go to similar recipients) + const primaryEmail = Array.isArray(to) ? to[0] : to + const unsubscribeToken = generateUnsubscribeToken(primaryEmail, emailType) + const baseUrl = env.NEXT_PUBLIC_APP_URL || 'https://sim.ai' + const unsubscribeUrl = `${baseUrl}/unsubscribe?token=${unsubscribeToken}&email=${encodeURIComponent(primaryEmail)}` + + headers['List-Unsubscribe'] = `<${unsubscribeUrl}>` + headers['List-Unsubscribe-Post'] = 'List-Unsubscribe=One-Click' + + if (html) { + finalHtml = html.replace(/\{\{UNSUBSCRIBE_TOKEN\}\}/g, unsubscribeToken) + } + if (text) { + finalText = text.replace(/\{\{UNSUBSCRIBE_TOKEN\}\}/g, unsubscribeToken) + } + } - if (!resend) { - logger.info('Batch emails not sent (Resend not configured):', { - emailCount: emails.length, - }) + return { + to, + subject, + html: finalHtml, + text: finalText, + senderEmail, + headers, + attachments, + replyTo, + useCustomFromFormat, + } +} - emails.forEach(() => { - results.push({ - success: true, - message: 'Email logging successful (Resend not configured)', - data: { id: 'mock-email-id' }, - }) - }) +async function sendWithResend(data: ProcessedEmailData): Promise { + if (!resend) throw new Error('Resend not configured') - return { - success: true, - message: 'Batch email logging successful (Resend not configured)', - results, - data: { ids: Array(emails.length).fill('mock-email-id') }, - } - } + const fromAddress = data.useCustomFromFormat + ? data.senderEmail + : `${env.SENDER_NAME || 'Sim'} <${data.senderEmail}>` - const batchEmails = emails.map((email) => ({ - from: `Sim <${email.from || senderEmail}>`, - to: email.to, - subject: email.subject, - html: email.html, + const emailData: any = { + from: fromAddress, + to: data.to, + subject: data.subject, + headers: Object.keys(data.headers).length > 0 ? data.headers : undefined, + } + + if (data.html) emailData.html = data.html + if (data.text) emailData.text = data.text + if (data.replyTo) emailData.replyTo = data.replyTo + if (data.attachments) { + emailData.attachments = data.attachments.map((att) => ({ + filename: att.filename, + content: typeof att.content === 'string' ? att.content : att.content.toString('base64'), + contentType: att.contentType, + disposition: att.disposition || 'attachment', })) + } - const BATCH_SIZE = 50 - let allSuccessful = true + const { data: responseData, error } = await resend.emails.send(emailData) - const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) + if (error) { + throw new Error(error.message || 'Failed to send email via Resend') + } - let rateDelay = 500 + return { + success: true, + message: 'Email sent successfully via Resend', + data: responseData, + } +} - for (let i = 0; i < batchEmails.length; i += BATCH_SIZE) { - if (i > 0) { - logger.info(`Rate limit protection: Waiting ${rateDelay}ms before sending next batch`) - await delay(rateDelay) - } +async function sendWithAzure(data: ProcessedEmailData): Promise { + if (!azureEmailClient) throw new Error('Azure Communication Services not configured') - const batch = batchEmails.slice(i, i + BATCH_SIZE) + // Azure Communication Services requires at least one content type + if (!data.html && !data.text) { + throw new Error('Azure Communication Services requires either HTML or text content') + } - try { - logger.info( - `Sending batch ${Math.floor(i / BATCH_SIZE) + 1} of ${Math.ceil(batchEmails.length / BATCH_SIZE)} (${batch.length} emails)` - ) - const response = await resend.batch.send(batch) - - if (response.error) { - logger.error('Resend batch API error:', response.error) - - // Add failure results for this batch - batch.forEach(() => { - results.push({ - success: false, - message: response.error?.message || 'Failed to send batch email', - }) - }) - - allSuccessful = false - } else if (response.data) { - if (Array.isArray(response.data)) { - response.data.forEach((item: { id: string }) => { - results.push({ - success: true, - message: 'Email sent successfully', - data: item, - }) - }) - } else { - logger.info('Resend batch API returned unexpected format, assuming success') - batch.forEach((_, index) => { - results.push({ - success: true, - message: 'Email sent successfully', - data: { id: `batch-${i}-item-${index}` }, - }) - }) - } + // For Azure, use just the email address part (no display name) + // Azure will use the display name configured in the portal for the sender address + const senderEmailOnly = data.senderEmail.includes('<') + ? data.senderEmail.match(/<(.+)>/)?.[1] || data.senderEmail + : data.senderEmail + + const message: EmailMessage = { + senderAddress: senderEmailOnly, + content: data.html + ? { + subject: data.subject, + html: data.html, } + : { + subject: data.subject, + plainText: data.text!, + }, + recipients: { + to: Array.isArray(data.to) + ? data.to.map((email) => ({ address: email })) + : [{ address: data.to }], + }, + headers: data.headers, + } + + const poller = await azureEmailClient.beginSend(message) + const result = await poller.pollUntilDone() + + if (result.status === 'Succeeded') { + return { + success: true, + message: 'Email sent successfully via Azure Communication Services', + data: { id: result.id }, + } + } + throw new Error(`Azure Communication Services failed with status: ${result.status}`) +} + +export async function sendBatchEmails(options: BatchEmailOptions): Promise { + try { + const results: SendEmailResult[] = [] + + // Try Resend first for batch emails if available + if (resend) { + try { + return await sendBatchWithResend(options.emails) } catch (error) { - logger.error('Error sending batch emails:', error) - - // Check if it's a rate limit error - if ( - error instanceof Error && - (error.message.toLowerCase().includes('rate') || - error.message.toLowerCase().includes('too many') || - error.message.toLowerCase().includes('429')) - ) { - logger.warn('Rate limit exceeded, increasing delay and retrying...') - - // Wait a bit longer and try again with this batch - await delay(rateDelay * 5) - - try { - logger.info(`Retrying batch ${Math.floor(i / BATCH_SIZE) + 1} with longer delay`) - const retryResponse = await resend.batch.send(batch) - - if (retryResponse.error) { - logger.error('Retry failed with error:', retryResponse.error) - - batch.forEach(() => { - results.push({ - success: false, - message: retryResponse.error?.message || 'Failed to send batch email after retry', - }) - }) - - allSuccessful = false - } else if (retryResponse.data) { - if (Array.isArray(retryResponse.data)) { - retryResponse.data.forEach((item: { id: string }) => { - results.push({ - success: true, - message: 'Email sent successfully on retry', - data: item, - }) - }) - } else { - batch.forEach((_, index) => { - results.push({ - success: true, - message: 'Email sent successfully on retry', - data: { id: `retry-batch-${i}-item-${index}` }, - }) - }) - } - - // Increase the standard delay since we hit a rate limit - logger.info('Increasing delay between batches after rate limit hit') - rateDelay = rateDelay * 2 - } - } catch (retryError) { - logger.error('Retry also failed:', retryError) - - batch.forEach(() => { - results.push({ - success: false, - message: - retryError instanceof Error - ? retryError.message - : 'Failed to send email even after retry', - }) - }) - - allSuccessful = false - } - } else { - // Non-rate limit error - batch.forEach(() => { - results.push({ - success: false, - message: error instanceof Error ? error.message : 'Failed to send batch email', - }) - }) - - allSuccessful = false - } + logger.warn('Resend batch failed, falling back to individual sends:', error) + } + } + + // Fallback to individual sends (works with both Azure and Resend) + logger.info('Sending batch emails individually') + for (const email of options.emails) { + try { + const result = await sendEmail(email) + results.push(result) + } catch (error) { + results.push({ + success: false, + message: error instanceof Error ? error.message : 'Failed to send email', + }) } } + const successCount = results.filter((r) => r.success).length return { - success: allSuccessful, - message: allSuccessful - ? 'All batch emails sent successfully' - : 'Some batch emails failed to send', + success: successCount === results.length, + message: + successCount === results.length + ? 'All batch emails sent successfully' + : `${successCount}/${results.length} emails sent successfully`, results, - data: { count: results.filter((r) => r.success).length }, + data: { count: successCount }, } } catch (error) { logger.error('Error in batch email sending:', error) @@ -315,3 +321,47 @@ export async function sendBatchEmails({ } } } + +async function sendBatchWithResend(emails: EmailOptions[]): Promise { + if (!resend) throw new Error('Resend not configured') + + const results: SendEmailResult[] = [] + const batchEmails = emails.map((email) => { + const senderEmail = email.from || `noreply@${env.EMAIL_DOMAIN || getEmailDomain()}` + const emailData: any = { + from: `${env.SENDER_NAME || 'Sim'} <${senderEmail}>`, + to: email.to, + subject: email.subject, + } + if (email.html) emailData.html = email.html + if (email.text) emailData.text = email.text + return emailData + }) + + try { + const response = await resend.batch.send(batchEmails as any) + + if (response.error) { + throw new Error(response.error.message || 'Resend batch API error') + } + + // Success - create results for each email + batchEmails.forEach((_, index) => { + results.push({ + success: true, + message: 'Email sent successfully via Resend batch', + data: { id: `batch-${index}` }, + }) + }) + + return { + success: true, + message: 'All batch emails sent successfully via Resend', + results, + data: { count: results.length }, + } + } catch (error) { + logger.error('Resend batch send failed:', error) + throw error // Let the caller handle fallback + } +} diff --git a/apps/sim/lib/env.ts b/apps/sim/lib/env.ts index 5f0f5aaa81..f14a597007 100644 --- a/apps/sim/lib/env.ts +++ b/apps/sim/lib/env.ts @@ -50,6 +50,8 @@ export const env = createEnv({ // Email & Communication RESEND_API_KEY: z.string().min(1).optional(), // Resend API key for transactional emails EMAIL_DOMAIN: z.string().min(1).optional(), // Domain for sending emails + SENDER_NAME: z.string().optional(), // Name to use as email sender (e.g., "Sim" in "Sim ") + AZURE_ACS_CONNECTION_STRING: z.string().optional(), // Azure Communication Services connection string // AI/LLM Provider API Keys OPENAI_API_KEY: z.string().min(1).optional(), // Primary OpenAI API key diff --git a/apps/sim/package.json b/apps/sim/package.json index 9e0e2cd82e..4ece8a35c7 100644 --- a/apps/sim/package.json +++ b/apps/sim/package.json @@ -29,6 +29,7 @@ "@anthropic-ai/sdk": "^0.39.0", "@aws-sdk/client-s3": "^3.779.0", "@aws-sdk/s3-request-presigner": "^3.779.0", + "@azure/communication-email": "1.0.0", "@azure/storage-blob": "12.27.0", "@better-auth/stripe": "^1.2.9", "@browserbasehq/stagehand": "^2.0.0", @@ -125,6 +126,7 @@ "zod": "^3.24.2" }, "devDependencies": { + "@react-email/preview-server": "4.2.4", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", diff --git a/bun.lock b/bun.lock index 38cd829ad4..b5558f6f79 100644 --- a/bun.lock +++ b/bun.lock @@ -58,6 +58,7 @@ "@anthropic-ai/sdk": "^0.39.0", "@aws-sdk/client-s3": "^3.779.0", "@aws-sdk/s3-request-presigner": "^3.779.0", + "@azure/communication-email": "1.0.0", "@azure/storage-blob": "12.27.0", "@better-auth/stripe": "^1.2.9", "@browserbasehq/stagehand": "^2.0.0", @@ -154,6 +155,7 @@ "zod": "^3.24.2", }, "devDependencies": { + "@react-email/preview-server": "4.2.4", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", @@ -348,8 +350,14 @@ "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.821.0", "", { "dependencies": { "@smithy/types": "^4.3.1", "tslib": "^2.6.2" } }, "sha512-DIIotRnefVL6DiaHtO6/21DhJ4JZnnIwdNbpwiAhdt/AVbttcE4yw925gsjur0OGv5BTYXQXU3YnANBYnZjuQA=="], + "@azure-rest/core-client": ["@azure-rest/core-client@2.5.0", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.9.0", "@azure/core-rest-pipeline": "^1.5.0", "@azure/core-tracing": "^1.0.1", "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-KMVIPxG6ygcQ1M2hKHahF7eddKejYsWTjoLIfTWiqnaj42dBkYzj4+S8rK9xxmlOaEHKZHcMrRbm0NfN4kgwHw=="], + "@azure/abort-controller": ["@azure/abort-controller@2.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA=="], + "@azure/communication-common": ["@azure/communication-common@2.4.0", "", { "dependencies": { "@azure-rest/core-client": "^2.3.3", "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.9.0", "@azure/core-rest-pipeline": "^1.17.0", "@azure/core-tracing": "^1.2.0", "@azure/core-util": "^1.11.0", "events": "^3.3.0", "jwt-decode": "^4.0.0", "tslib": "^2.8.1" } }, "sha512-wwn4AoOgTgoA9OZkO34SKBpQg7/kfcABnzbaYEbc+9bCkBtwwjgMEk6xM+XLEE/uuODZ8q8jidUoNcZHQyP5AQ=="], + + "@azure/communication-email": ["@azure/communication-email@1.0.0", "", { "dependencies": { "@azure/communication-common": "^2.2.0", "@azure/core-auth": "^1.3.0", "@azure/core-client": "^1.3.2", "@azure/core-lro": "^2.5.0", "@azure/core-rest-pipeline": "^1.8.0", "@azure/logger": "^1.0.0", "tslib": "^1.9.3", "uuid": "^8.3.2" } }, "sha512-aY/qE3u4gadd6I895WOJPXrbKaPqeFDxGOK5xgAAqHkqNadI+hCp/D59q5Kfcj5Qcxal6mLm1GwZ1Cka0x4KZw=="], + "@azure/core-auth": ["@azure/core-auth@1.10.0", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-util": "^1.11.0", "tslib": "^2.6.2" } }, "sha512-88Djs5vBvGbHQHf5ZZcaoNHo6Y8BKZkt3cw2iuJIQzLEgH4Ox6Tm4hjFhbqOxyYsgIG/eJbFEHpxRIfEEWv5Ow=="], "@azure/core-client": ["@azure/core-client@1.10.0", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.4.0", "@azure/core-rest-pipeline": "^1.20.0", "@azure/core-tracing": "^1.0.0", "@azure/core-util": "^1.6.1", "@azure/logger": "^1.0.0", "tslib": "^2.6.2" } }, "sha512-O4aP3CLFNodg8eTHXECaH3B3CjicfzkxVtnrfLkOq0XNP7TIECGfHpK/C6vADZkWP75wzmdBnsIA8ksuJMk18g=="], @@ -376,7 +384,7 @@ "@babel/compat-data": ["@babel/compat-data@7.28.0", "", {}, "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw=="], - "@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="], + "@babel/core": ["@babel/core@7.26.10", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.26.10", "@babel/helper-compilation-targets": "^7.26.5", "@babel/helper-module-transforms": "^7.26.0", "@babel/helpers": "^7.26.10", "@babel/parser": "^7.26.10", "@babel/template": "^7.26.9", "@babel/traverse": "^7.26.10", "@babel/types": "^7.26.10", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ=="], "@babel/generator": ["@babel/generator@7.28.0", "", { "dependencies": { "@babel/parser": "^7.28.0", "@babel/types": "^7.28.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg=="], @@ -398,7 +406,7 @@ "@babel/helpers": ["@babel/helpers@7.28.2", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.2" } }, "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw=="], - "@babel/parser": ["@babel/parser@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.0" }, "bin": "./bin/babel-parser.js" }, "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g=="], + "@babel/parser": ["@babel/parser@7.27.0", "", { "dependencies": { "@babel/types": "^7.27.0" }, "bin": "./bin/babel-parser.js" }, "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg=="], "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="], @@ -408,7 +416,7 @@ "@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], - "@babel/traverse": ["@babel/traverse@7.28.0", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/types": "^7.28.0", "debug": "^4.3.1" } }, "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg=="], + "@babel/traverse": ["@babel/traverse@7.27.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.27.0", "@babel/parser": "^7.27.0", "@babel/template": "^7.27.0", "@babel/types": "^7.27.0", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA=="], "@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="], @@ -630,6 +638,10 @@ "@linear/sdk": ["@linear/sdk@40.0.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.0", "graphql": "^15.4.0", "isomorphic-unfetch": "^3.1.0" } }, "sha512-R4lyDIivdi00fO+DYPs7gWNX221dkPJhgDowFrsfos/rNG6o5HixsCPgwXWtKN0GA0nlqLvFTmzvzLXpud1xKw=="], + "@lottiefiles/dotlottie-react": ["@lottiefiles/dotlottie-react@0.13.3", "", { "dependencies": { "@lottiefiles/dotlottie-web": "0.42.0" }, "peerDependencies": { "react": "^17 || ^18 || ^19" } }, "sha512-V4FfdYlqzjBUX7f0KV6vfQOOI0Cp+3XeG/ZqSDFSEVg5P7fpROpDv5/I9aTM8sOCESK1SWT96Fem+QVUnBV1wQ=="], + + "@lottiefiles/dotlottie-web": ["@lottiefiles/dotlottie-web@0.42.0", "", {}, "sha512-Zr2LCaOAoPCsdAQgeLyCSiQ1+xrAJtRCyuEYDj0qR5heUwpc+Pxbb88JyTVumcXFfKOBMOMmrlsTScLz2mrvQQ=="], + "@mdx-js/mdx": ["@mdx-js/mdx@3.1.0", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw=="], "@next/env": ["@next/env@15.4.4", "", {}, "sha512-SJKOOkULKENyHSYXE5+KiFU6itcIb6wSBjgM92meK0HVKpo94dNOLZVdLLuS7/BxImROkGoPsjR4EnuDucqiiA=="], @@ -816,6 +828,8 @@ "@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="], + "@radix-ui/colors": ["@radix-ui/colors@3.0.0", "", {}, "sha512-FUOsGBkHrYJwCSEtWRCIfQbZG7q1e6DgxCIOe1SUQzDe/7rXXeA47s8yCn6fuTNQAj1Zq4oTFi9Yjp3wzElcxg=="], + "@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="], "@radix-ui/primitive": ["@radix-ui/primitive@1.1.2", "", {}, "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA=="], @@ -890,6 +904,8 @@ "@radix-ui/react-toggle": ["@radix-ui/react-toggle@1.1.9", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ZoFkBBz9zv9GWer7wIjvdRxmh2wyc2oKWw6C6CseWd6/yq1DK/l5lJ+wnsmFwJZbBYqr02mrf8A2q/CVCuM3ZA=="], + "@radix-ui/react-toggle-group": ["@radix-ui/react-toggle-group@1.1.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-roving-focus": "1.1.6", "@radix-ui/react-toggle": "1.1.6", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XOBq9VqC+mIn5hzjGdJLhQbvQeiOpV5ExNE6qMQQPvFsCT44QUcxFzYytTWVoyWg9XKfgrleKmTeEyu6aoTPhg=="], + "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.7", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.10", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.7", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Ap+fNYwKTYJ9pzqW+Xe2HtMRbQ/EeWkj2qykZ6SuEV4iS/o1bZI5ssJbk4D2r8XuDuOBVz/tIx2JObtuqU+5Zw=="], "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], @@ -946,6 +962,8 @@ "@react-email/preview": ["@react-email/preview@0.0.12", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-g/H5fa9PQPDK6WUEG7iTlC19sAktI23qyoiJtMLqQiXFCfWeQMhqjLGKeLSKkfzszqmfJCjZtpSiKtBoOdxp3Q=="], + "@react-email/preview-server": ["@react-email/preview-server@4.2.4", "", { "dependencies": { "@babel/core": "7.26.10", "@babel/parser": "7.27.0", "@babel/traverse": "7.27.0", "@lottiefiles/dotlottie-react": "0.13.3", "@radix-ui/colors": "3.0.0", "@radix-ui/react-collapsible": "1.1.7", "@radix-ui/react-dropdown-menu": "2.1.10", "@radix-ui/react-popover": "1.1.10", "@radix-ui/react-slot": "1.2.0", "@radix-ui/react-tabs": "1.1.7", "@radix-ui/react-toggle-group": "1.1.6", "@radix-ui/react-tooltip": "1.2.3", "@types/node": "22.14.1", "@types/normalize-path": "3.0.2", "@types/react": "19.0.10", "@types/react-dom": "19.0.4", "@types/webpack": "5.28.5", "autoprefixer": "10.4.21", "chalk": "4.1.2", "clsx": "2.1.1", "esbuild": "0.25.0", "framer-motion": "12.7.5", "json5": "2.2.3", "log-symbols": "4.1.0", "module-punycode": "npm:punycode@2.3.1", "next": "15.4.1", "node-html-parser": "7.0.1", "ora": "5.4.1", "pretty-bytes": "6.1.1", "prism-react-renderer": "2.4.1", "react": "19.0.0", "react-dom": "19.0.0", "sharp": "0.34.1", "socket.io-client": "4.8.1", "sonner": "2.0.3", "source-map-js": "1.2.1", "spamc": "0.0.5", "stacktrace-parser": "0.1.11", "tailwind-merge": "3.2.0", "tailwindcss": "3.4.0", "use-debounce": "10.0.4", "zod": "3.24.3" } }, "sha512-QRh7MUK9rG48lwIvwHoL8ByNCNkQzX9G7hl8T+IsleI55lGeAtlAzze/QHeLfoYZ7wl5LCG05ok/00DP06Xogw=="], + "@react-email/render": ["@react-email/render@1.0.5", "", { "dependencies": { "html-to-text": "9.0.5", "prettier": "3.4.2", "react-promise-suspense": "0.3.4" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-CA69HYXPk21HhtAXATIr+9JJwpDNmAFCvdMUjWmeoD1+KhJ9NAxusMRxKNeibdZdslmq3edaeOKGbdQ9qjK8LQ=="], "@react-email/row": ["@react-email/row@0.0.12", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-HkCdnEjvK3o+n0y0tZKXYhIXUNPDx+2vq1dJTmqappVHXS5tXS6W5JOPZr5j+eoZ8gY3PShI2LWj5rWF7ZEtIQ=="], @@ -1372,6 +1390,8 @@ "@types/node-fetch": ["@types/node-fetch@2.6.12", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.0" } }, "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA=="], + "@types/normalize-path": ["@types/normalize-path@3.0.2", "", {}, "sha512-DO++toKYPaFn0Z8hQ7Tx+3iT9t77IJo/nDiqTXilgEP+kPNIYdpS9kh3fXuc53ugqwp9pxC1PVjCpV1tQDyqMA=="], + "@types/pg": ["@types/pg@8.6.1", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w=="], "@types/pg-pool": ["@types/pg-pool@2.0.6", "", { "dependencies": { "@types/pg": "*" } }, "sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ=="], @@ -1398,6 +1418,8 @@ "@types/uuid": ["@types/uuid@9.0.8", "", {}, "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA=="], + "@types/webpack": ["@types/webpack@5.28.5", "", { "dependencies": { "@types/node": "*", "tapable": "^2.2.0", "webpack": "^5" } }, "sha512-wR87cgvxj3p6D0Crt1r5avwqffqPXUkNlnQ1mjU93G7gCuFjufZR4I6j8cz5g1F1tTYpfOOFvly+cmIQwL9wvw=="], + "@types/webxr": ["@types/webxr@0.5.22", "", {}, "sha512-Vr6Stjv5jPRqH690f5I5GLjVk8GSsoQSYJ2FVd/3jJF7KaqfwPi3ehfBS96mlQ2kPCwZaX6U0rG2+NGHBKkA/A=="], "@types/xlsx": ["@types/xlsx@0.0.36", "", { "dependencies": { "xlsx": "*" } }, "sha512-mvfrKiKKMErQzLMF8ElYEH21qxWCZtN59pHhWGmWCWFJStYdMWjkDSAy6mGowFxHXaXZWe5/TW7pBUiWclIVOw=="], @@ -1528,6 +1550,8 @@ "atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="], + "autoprefixer": ["autoprefixer@10.4.21", "", { "dependencies": { "browserslist": "^4.24.4", "caniuse-lite": "^1.0.30001702", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ=="], + "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], @@ -1962,6 +1986,8 @@ "frac": ["frac@1.1.2", "", {}, "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA=="], + "fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="], + "framer-motion": ["framer-motion@12.23.9", "", { "dependencies": { "motion-dom": "^12.23.9", "motion-utils": "^12.23.6", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-TqEHXj8LWfQSKqfdr5Y4mYltYLw96deu6/K9kGDd+ysqRJPNwF9nb5mZcrLmybHbU7gcJ+HQar41U3UTGanbbQ=="], "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], @@ -2008,6 +2034,8 @@ "glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="], + "globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="], + "globrex": ["globrex@0.1.2", "", {}, "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg=="], "google-auth-library": ["google-auth-library@9.15.1", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^6.1.1", "gcp-metadata": "^6.1.0", "gtoken": "^7.0.0", "jws": "^4.0.0" } }, "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng=="], @@ -2046,6 +2074,8 @@ "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], + "he": ["he@1.2.0", "", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="], + "help-me": ["help-me@5.0.0", "", {}, "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="], "hex-rgb": ["hex-rgb@4.3.0", "", {}, "sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw=="], @@ -2124,7 +2154,7 @@ "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], - "is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="], + "is-interactive": ["is-interactive@1.0.0", "", {}, "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="], "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], @@ -2136,7 +2166,7 @@ "is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="], - "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], + "is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="], "is-what": ["is-what@4.1.16", "", {}, "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A=="], @@ -2250,7 +2280,7 @@ "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], - "log-symbols": ["log-symbols@7.0.1", "", { "dependencies": { "is-unicode-supported": "^2.0.0", "yoctocolors": "^2.1.1" } }, "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg=="], + "log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], "log-update": ["log-update@5.0.1", "", { "dependencies": { "ansi-escapes": "^5.0.0", "cli-cursor": "^4.0.0", "slice-ansi": "^5.0.0", "strip-ansi": "^7.0.1", "wrap-ansi": "^8.0.1" } }, "sha512-5UtUDQ/6edw4ofyljDNcOVJQ4c7OjDro4h3y8e1GQL5iYElYclVHJ3zeWchylvMaKnDbDilC8irOVyexnA/Slw=="], @@ -2426,6 +2456,8 @@ "module-details-from-path": ["module-details-from-path@1.0.4", "", {}, "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w=="], + "module-punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "motion-dom": ["motion-dom@12.23.9", "", { "dependencies": { "motion-utils": "^12.23.6" } }, "sha512-6Sv++iWS8XMFCgU1qwKj9l4xuC47Hp4+2jvPfyTXkqDg2tTzSgX6nWKD4kNFXk0k7llO59LZTPuJigza4A2K1A=="], "motion-utils": ["motion-utils@12.23.6", "", {}, "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ=="], @@ -2458,12 +2490,16 @@ "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], + "node-html-parser": ["node-html-parser@7.0.1", "", { "dependencies": { "css-select": "^5.1.0", "he": "1.2.0" } }, "sha512-KGtmPY2kS0thCWGK0VuPyOS+pBKhhe8gXztzA2ilAOhbUbxa9homF1bOyKvhGzMLXUoRds9IOmr/v5lr/lqNmA=="], + "node-int64": ["node-int64@0.4.0", "", {}, "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw=="], "node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="], "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + "normalize-range": ["normalize-range@0.1.2", "", {}, "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA=="], + "npm-run-path": ["npm-run-path@5.3.0", "", { "dependencies": { "path-key": "^4.0.0" } }, "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ=="], "npm-to-yarn": ["npm-to-yarn@3.0.1", "", {}, "sha512-tt6PvKu4WyzPwWUzy/hvPFqn+uwXO0K1ZHka8az3NnrhWJDmSqI8ncWq0fkL0k/lmmi5tAC11FXwXuh0rFbt1A=="], @@ -2498,7 +2534,7 @@ "option": ["option@0.2.4", "", {}, "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A=="], - "ora": ["ora@8.2.0", "", { "dependencies": { "chalk": "^5.3.0", "cli-cursor": "^5.0.0", "cli-spinners": "^2.9.2", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.0.0", "log-symbols": "^6.0.0", "stdin-discarder": "^0.2.2", "string-width": "^7.2.0", "strip-ansi": "^7.1.0" } }, "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw=="], + "ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="], "os-tmpdir": ["os-tmpdir@1.0.2", "", {}, "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g=="], @@ -2596,8 +2632,12 @@ "prettier": ["prettier@3.4.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ=="], + "pretty-bytes": ["pretty-bytes@6.1.1", "", {}, "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ=="], + "pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="], + "prism-react-renderer": ["prism-react-renderer@2.4.1", "", { "dependencies": { "@types/prismjs": "^1.26.0", "clsx": "^2.0.0" }, "peerDependencies": { "react": ">=16.0.0" } }, "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig=="], + "prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="], "process": ["process@0.10.1", "", {}, "sha512-dyIett8dgGIZ/TXKUzeYExt7WA6ldDzys9vTDU/cCA9L17Ypme+KzS+NjQCjpn9xsvi/shbMC+yP/BcFMBz0NA=="], @@ -2828,6 +2868,8 @@ "sonic-boom": ["sonic-boom@4.2.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="], + "sonner": ["sonner@2.0.3", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-njQ4Hht92m0sMqqHVDL32V2Oun9W1+PHO9NDv9FHfJjT3JT22IG4Jpo3FPQy+mouRKCXFWO+r67v6MrHX2zeIA=="], + "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], @@ -2836,6 +2878,8 @@ "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], + "spamc": ["spamc@0.0.5", "", {}, "sha512-jYXItuZuiWZyG9fIdvgTUbp2MNRuyhuSwvvhhpPJd4JK/9oSZxkD7zAj53GJtowSlXwCJzLg6sCKAoE9wXsKgg=="], + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], @@ -3036,6 +3080,8 @@ "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], + "use-debounce": ["use-debounce@10.0.4", "", { "peerDependencies": { "react": "*" } }, "sha512-6Cf7Yr7Wk7Kdv77nnJMf6de4HuDE4dTxKij+RqE9rufDsI6zsbjyAxcH5y2ueJCQAnfgKbzXbZHYlkFwmBlWkw=="], + "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], "use-sync-external-store": ["use-sync-external-store@1.5.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A=="], @@ -3158,14 +3204,32 @@ "@aws-sdk/client-s3/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], + "@azure/communication-email/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], + + "@azure/communication-email/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], + "@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + "@babel/core/@babel/parser": ["@babel/parser@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.0" }, "bin": "./bin/babel-parser.js" }, "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g=="], + + "@babel/core/@babel/traverse": ["@babel/traverse@7.28.0", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/types": "^7.28.0", "debug": "^4.3.1" } }, "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg=="], + "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@babel/generator/@babel/parser": ["@babel/parser@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.0" }, "bin": "./bin/babel-parser.js" }, "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g=="], + "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@babel/helper-module-imports/@babel/traverse": ["@babel/traverse@7.28.0", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/types": "^7.28.0", "debug": "^4.3.1" } }, "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg=="], + + "@babel/helper-module-transforms/@babel/traverse": ["@babel/traverse@7.28.0", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/types": "^7.28.0", "debug": "^4.3.1" } }, "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg=="], + + "@babel/template/@babel/parser": ["@babel/parser@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.0" }, "bin": "./bin/babel-parser.js" }, "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g=="], + + "@babel/traverse/@babel/parser": ["@babel/parser@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.0" }, "bin": "./bin/babel-parser.js" }, "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g=="], + "@better-auth/stripe/zod": ["zod@4.0.10", "", {}, "sha512-3vB+UU3/VmLL2lvwcY/4RV2i9z/YU0DTV/tDuYjrwmx5WeJ7hwy+rGEEx8glHp6Yxw7ibRbKSaIFBgReRPe5KA=="], "@browserbasehq/sdk/@types/node": ["@types/node@18.19.120", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-WtCGHFXnVI8WHLxDAt5TbnCM4eSE+nI0QN2NJtwzcgMhht2eNz6V9evJrk+lwC8bCY8OWV5Ym8Jz7ZEyGnKnMA=="], @@ -3376,14 +3440,54 @@ "@radix-ui/react-select/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@radix-ui/react-toggle-group/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.0", "", { "dependencies": { "@radix-ui/react-slot": "1.2.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw=="], + + "@radix-ui/react-toggle-group/@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.4", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-D2ReXCuIueKf5L2f1ks/wTj3bWck1SvK1pjLmEHPbwksS1nOHBsvgY0b9Hypt81FczqBqSyLHQxn/vbsQ0gDHw=="], + + "@radix-ui/react-toggle-group/@radix-ui/react-toggle": ["@radix-ui/react-toggle@1.1.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3SeJxKeO3TO1zVw1Nl++Cp0krYk6zHDHMCUXXVkosIzl6Nxcvb07EerQpyD2wXQSJ5RZajrYAmPaydU8Hk1IyQ=="], + "@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], "@react-email/code-block/prismjs": ["prismjs@1.29.0", "", {}, "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q=="], + "@react-email/preview-server/@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.7", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.3", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zGFsPcFJNdQa/UNd6MOgF40BS054FIGj32oOWBllixz42f+AkQg3QJ1YT9pw7vs+Ai+EgWkh839h69GEK8oH2A=="], + + "@react-email/preview-server/@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.10", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-8qnILty92BmXbxKugWX3jgEeFeMoxtdggeCCxb/aB7l34QFAKB23IhJfnwyVMbRnAUJiT5LOay4kUS22+AWuRg=="], + + "@react-email/preview-server/@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.7", "@radix-ui/react-focus-guards": "1.1.2", "@radix-ui/react-focus-scope": "1.1.4", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.4", "@radix-ui/react-portal": "1.1.6", "@radix-ui/react-presence": "1.1.3", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-slot": "1.2.0", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-IZN7b3sXqajiPsOzKuNJBSP9obF4MX5/5UhTgWNofw4r1H+eATWb0SyMlaxPD/kzA4vadFgy1s7Z1AEJ6WMyHQ=="], + + "@react-email/preview-server/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w=="], + + "@react-email/preview-server/@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.7", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.3", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-roving-focus": "1.1.6", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sawt4HkD+6haVGjYOC3BMIiCumBpqTK6o407n6zN/6yReed2EN7bXyykNrpqg+xCfudpBUZg7Y2cJBd/x/iybA=="], + + "@react-email/preview-server/@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.4", "@radix-ui/react-portal": "1.1.6", "@radix-ui/react-presence": "1.1.3", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-slot": "1.2.0", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0KX7jUYFA02np01Y11NWkk6Ip6TqMNmD4ijLelYAzeIndl2aVeltjJFJ2gwjNa1P8U/dgjQ+8cr9Y3Ni+ZNoRA=="], + + "@react-email/preview-server/@types/node": ["@types/node@22.14.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw=="], + + "@react-email/preview-server/@types/react": ["@types/react@19.0.10", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g=="], + + "@react-email/preview-server/@types/react-dom": ["@types/react-dom@19.0.4", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg=="], + + "@react-email/preview-server/esbuild": ["esbuild@0.25.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.0", "@esbuild/android-arm": "0.25.0", "@esbuild/android-arm64": "0.25.0", "@esbuild/android-x64": "0.25.0", "@esbuild/darwin-arm64": "0.25.0", "@esbuild/darwin-x64": "0.25.0", "@esbuild/freebsd-arm64": "0.25.0", "@esbuild/freebsd-x64": "0.25.0", "@esbuild/linux-arm": "0.25.0", "@esbuild/linux-arm64": "0.25.0", "@esbuild/linux-ia32": "0.25.0", "@esbuild/linux-loong64": "0.25.0", "@esbuild/linux-mips64el": "0.25.0", "@esbuild/linux-ppc64": "0.25.0", "@esbuild/linux-riscv64": "0.25.0", "@esbuild/linux-s390x": "0.25.0", "@esbuild/linux-x64": "0.25.0", "@esbuild/netbsd-arm64": "0.25.0", "@esbuild/netbsd-x64": "0.25.0", "@esbuild/openbsd-arm64": "0.25.0", "@esbuild/openbsd-x64": "0.25.0", "@esbuild/sunos-x64": "0.25.0", "@esbuild/win32-arm64": "0.25.0", "@esbuild/win32-ia32": "0.25.0", "@esbuild/win32-x64": "0.25.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw=="], + + "@react-email/preview-server/framer-motion": ["framer-motion@12.7.5", "", { "dependencies": { "motion-dom": "^12.7.5", "motion-utils": "^12.7.5", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-iD+vBOLn8E8bwBAFUQ1DYXjivm+cGGPgQUQ4Doleq7YP/zHdozUVwAMBJwOOfCTbtM8uOooMi77noD261Kxiyw=="], + + "@react-email/preview-server/next": ["next@15.4.1", "", { "dependencies": { "@next/env": "15.4.1", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.4.1", "@next/swc-darwin-x64": "15.4.1", "@next/swc-linux-arm64-gnu": "15.4.1", "@next/swc-linux-arm64-musl": "15.4.1", "@next/swc-linux-x64-gnu": "15.4.1", "@next/swc-linux-x64-musl": "15.4.1", "@next/swc-win32-arm64-msvc": "15.4.1", "@next/swc-win32-x64-msvc": "15.4.1", "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-eNKB1q8C7o9zXF8+jgJs2CzSLIU3T6bQtX6DcTnCq1sIR1CJ0GlSyRs1BubQi3/JgCnr9Vr+rS5mOMI38FFyQw=="], + + "@react-email/preview-server/sharp": ["sharp@0.34.1", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.7.1" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.1", "@img/sharp-darwin-x64": "0.34.1", "@img/sharp-libvips-darwin-arm64": "1.1.0", "@img/sharp-libvips-darwin-x64": "1.1.0", "@img/sharp-libvips-linux-arm": "1.1.0", "@img/sharp-libvips-linux-arm64": "1.1.0", "@img/sharp-libvips-linux-ppc64": "1.1.0", "@img/sharp-libvips-linux-s390x": "1.1.0", "@img/sharp-libvips-linux-x64": "1.1.0", "@img/sharp-libvips-linuxmusl-arm64": "1.1.0", "@img/sharp-libvips-linuxmusl-x64": "1.1.0", "@img/sharp-linux-arm": "0.34.1", "@img/sharp-linux-arm64": "0.34.1", "@img/sharp-linux-s390x": "0.34.1", "@img/sharp-linux-x64": "0.34.1", "@img/sharp-linuxmusl-arm64": "0.34.1", "@img/sharp-linuxmusl-x64": "0.34.1", "@img/sharp-wasm32": "0.34.1", "@img/sharp-win32-ia32": "0.34.1", "@img/sharp-win32-x64": "0.34.1" } }, "sha512-1j0w61+eVxu7DawFJtnfYcvSv6qPFvfTaqzTQ2BLknVhHTwGS8sc63ZBF4rzkWMBVKybo4S5OBtDdZahh2A1xg=="], + + "@react-email/preview-server/tailwind-merge": ["tailwind-merge@3.2.0", "", {}, "sha512-FQT/OVqCD+7edmmJpsgCsY820RTD5AkBryuG5IUqR5YQZSdj5xlH5nLgH7YPths7WsLPSpSBNneJdM8aS8aeFA=="], + + "@react-email/preview-server/tailwindcss": ["tailwindcss@3.4.0", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.5.3", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.0", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.19.1", "lilconfig": "^2.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.0.0", "postcss": "^8.4.23", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.1", "postcss-nested": "^6.0.1", "postcss-selector-parser": "^6.0.11", "resolve": "^1.22.2", "sucrase": "^3.32.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-VigzymniH77knD1dryXbyxR+ePHihHociZbXnLZHUyzf2MMs2ZVqlUrZ3FvpXP8pno9JzmILt1sZPD19M3IxtA=="], + + "@react-email/preview-server/zod": ["zod@3.24.3", "", {}, "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg=="], + "@rollup/plugin-commonjs/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + "@sentry/bundler-plugin-core/@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="], + "@sentry/bundler-plugin-core/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="], "@sentry/bundler-plugin-core/magic-string": ["magic-string@0.30.8", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ=="], @@ -3474,6 +3578,14 @@ "@trigger.dev/sdk/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], + "@types/babel__core/@babel/parser": ["@babel/parser@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.0" }, "bin": "./bin/babel-parser.js" }, "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g=="], + + "@types/babel__template/@babel/parser": ["@babel/parser@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.0" }, "bin": "./bin/babel-parser.js" }, "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g=="], + + "@types/webpack/@types/node": ["@types/node@24.2.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ=="], + + "@vitejs/plugin-react/@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="], + "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], @@ -3538,8 +3650,6 @@ "htmlparser2/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], - "inquirer/ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="], - "isomorphic-unfetch/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], "istanbul-lib-report/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], @@ -3570,6 +3680,8 @@ "loose-envify/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + "magicast/@babel/parser": ["@babel/parser@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.0" }, "bin": "./bin/babel-parser.js" }, "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g=="], + "mammoth/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], @@ -3590,16 +3702,6 @@ "openai/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], - "ora/chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], - - "ora/cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], - - "ora/log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="], - - "ora/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], - - "ora/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], - "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], "pdf-parse/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], @@ -3616,10 +3718,18 @@ "protobufjs/long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], + "react-email/@babel/parser": ["@babel/parser@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.0" }, "bin": "./bin/babel-parser.js" }, "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g=="], + + "react-email/@babel/traverse": ["@babel/traverse@7.28.0", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/types": "^7.28.0", "debug": "^4.3.1" } }, "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg=="], + "react-email/chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], "react-email/commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="], + "react-email/log-symbols": ["log-symbols@7.0.1", "", { "dependencies": { "is-unicode-supported": "^2.0.0", "yoctocolors": "^2.1.1" } }, "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg=="], + + "react-email/ora": ["ora@8.2.0", "", { "dependencies": { "chalk": "^5.3.0", "cli-cursor": "^5.0.0", "cli-spinners": "^2.9.2", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.0.0", "log-symbols": "^6.0.0", "stdin-discarder": "^0.2.2", "string-width": "^7.2.0", "strip-ansi": "^7.1.0" } }, "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw=="], + "resend/@react-email/render": ["@react-email/render@1.1.2", "", { "dependencies": { "html-to-text": "^9.0.5", "prettier": "^3.5.3", "react-promise-suspense": "^0.3.4" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-RnRehYN3v9gVlNMehHPHhyp2RQo7+pSkHDtXPvg3s0GbzM9SQMW4Qrf8GRNvtpLC4gsI+Wt0VatNRUFqjvevbw=="], "restore-cursor/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], @@ -3690,6 +3800,10 @@ "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "@babel/helper-module-imports/@babel/traverse/@babel/parser": ["@babel/parser@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.0" }, "bin": "./bin/babel-parser.js" }, "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g=="], + + "@babel/helper-module-transforms/@babel/traverse/@babel/parser": ["@babel/parser@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.0" }, "bin": "./bin/babel-parser.js" }, "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g=="], + "@browserbasehq/sdk/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], "@browserbasehq/sdk/node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], @@ -3820,6 +3934,174 @@ "@prisma/instrumentation/@opentelemetry/instrumentation/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.57.2", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A=="], + "@radix-ui/react-toggle-group/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w=="], + + "@radix-ui/react-toggle-group/@radix-ui/react-roving-focus/@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-slot": "1.2.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-cv4vSf7HttqXilDnAnvINd53OTl1/bjUYVZrkFnA7nwmY9Ob2POUy0WY0sfqBAe1s5FyKsyceQlqiEGPYNTadg=="], + + "@react-email/preview-server/@radix-ui/react-collapsible/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-IrVLIhskYhH3nLvtcBLQFZr61tBG7wx7O3kEmdzcYwRGAEBmBicGGL7ATzNgruYJ3xBTbuzEEq9OXJM3PAX3tA=="], + + "@react-email/preview-server/@radix-ui/react-collapsible/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.0", "", { "dependencies": { "@radix-ui/react-slot": "1.2.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw=="], + + "@react-email/preview-server/@radix-ui/react-dropdown-menu/@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.4", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.7", "@radix-ui/react-focus-guards": "1.1.2", "@radix-ui/react-focus-scope": "1.1.4", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.4", "@radix-ui/react-portal": "1.1.6", "@radix-ui/react-presence": "1.1.3", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-roving-focus": "1.1.6", "@radix-ui/react-slot": "1.2.0", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-OupA+1PrVf2H0K4jIwkDyA+rsJ7vF1y/VxLEO43dmZ68GtCjvx9K1/B/QscPZM3jIeFNK/wPd0HmiLjT36hVcA=="], + + "@react-email/preview-server/@radix-ui/react-dropdown-menu/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.0", "", { "dependencies": { "@radix-ui/react-slot": "1.2.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw=="], + + "@react-email/preview-server/@radix-ui/react-popover/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.7", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-j5+WBUdhccJsmH5/H0K6RncjDtoALSEr6jbkaZu+bjw6hOPOhHycr6vEUujl+HBK8kjUfWcoCJXxP6e4lUlMZw=="], + + "@react-email/preview-server/@radix-ui/react-popover/@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-r2annK27lIW5w9Ho5NyQgqs0MmgZSTIKXWpVCJaLC1q2kZrZkcqnmHkCHMEmv8XLvsLlurKMPT+kbKkRkm/xVA=="], + + "@react-email/preview-server/@radix-ui/react-popover/@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.4", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.4", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3p2Rgm/a1cK0r/UVkx5F/K9v/EplfjAeIFCGOPYPO4lZ0jtg4iSQXt/YGTSLWaf4x7NG6Z4+uKFcylcTZjeqDA=="], + + "@react-email/preview-server/@radix-ui/react-popover/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.6", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XmsIl2z1n/TsYFLIdYam2rmFwf9OC/Sh2avkbmVMDuBZIe7hSpM0cYnWPAo7nHOVx8zTuwDZGByfcqLdnzp3Vw=="], + + "@react-email/preview-server/@radix-ui/react-popover/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-IrVLIhskYhH3nLvtcBLQFZr61tBG7wx7O3kEmdzcYwRGAEBmBicGGL7ATzNgruYJ3xBTbuzEEq9OXJM3PAX3tA=="], + + "@react-email/preview-server/@radix-ui/react-popover/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.0", "", { "dependencies": { "@radix-ui/react-slot": "1.2.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw=="], + + "@react-email/preview-server/@radix-ui/react-tabs/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-IrVLIhskYhH3nLvtcBLQFZr61tBG7wx7O3kEmdzcYwRGAEBmBicGGL7ATzNgruYJ3xBTbuzEEq9OXJM3PAX3tA=="], + + "@react-email/preview-server/@radix-ui/react-tabs/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.0", "", { "dependencies": { "@radix-ui/react-slot": "1.2.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw=="], + + "@react-email/preview-server/@radix-ui/react-tabs/@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.4", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-D2ReXCuIueKf5L2f1ks/wTj3bWck1SvK1pjLmEHPbwksS1nOHBsvgY0b9Hypt81FczqBqSyLHQxn/vbsQ0gDHw=="], + + "@react-email/preview-server/@radix-ui/react-tooltip/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.7", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-j5+WBUdhccJsmH5/H0K6RncjDtoALSEr6jbkaZu+bjw6hOPOhHycr6vEUujl+HBK8kjUfWcoCJXxP6e4lUlMZw=="], + + "@react-email/preview-server/@radix-ui/react-tooltip/@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.4", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.4", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3p2Rgm/a1cK0r/UVkx5F/K9v/EplfjAeIFCGOPYPO4lZ0jtg4iSQXt/YGTSLWaf4x7NG6Z4+uKFcylcTZjeqDA=="], + + "@react-email/preview-server/@radix-ui/react-tooltip/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.6", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XmsIl2z1n/TsYFLIdYam2rmFwf9OC/Sh2avkbmVMDuBZIe7hSpM0cYnWPAo7nHOVx8zTuwDZGByfcqLdnzp3Vw=="], + + "@react-email/preview-server/@radix-ui/react-tooltip/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-IrVLIhskYhH3nLvtcBLQFZr61tBG7wx7O3kEmdzcYwRGAEBmBicGGL7ATzNgruYJ3xBTbuzEEq9OXJM3PAX3tA=="], + + "@react-email/preview-server/@radix-ui/react-tooltip/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.0", "", { "dependencies": { "@radix-ui/react-slot": "1.2.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw=="], + + "@react-email/preview-server/@radix-ui/react-tooltip/@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.0", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-rQj0aAWOpCdCMRbI6pLQm8r7S2BM3YhTa0SzOYD55k+hJA8oo9J+H+9wLM9oMlZWOX/wJWPTzfDfmZkf7LvCfg=="], + + "@react-email/preview-server/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ=="], + + "@react-email/preview-server/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.0", "", { "os": "android", "cpu": "arm" }, "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g=="], + + "@react-email/preview-server/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.0", "", { "os": "android", "cpu": "arm64" }, "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g=="], + + "@react-email/preview-server/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.0", "", { "os": "android", "cpu": "x64" }, "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg=="], + + "@react-email/preview-server/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw=="], + + "@react-email/preview-server/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg=="], + + "@react-email/preview-server/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w=="], + + "@react-email/preview-server/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A=="], + + "@react-email/preview-server/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.0", "", { "os": "linux", "cpu": "arm" }, "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg=="], + + "@react-email/preview-server/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg=="], + + "@react-email/preview-server/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.0", "", { "os": "linux", "cpu": "ia32" }, "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg=="], + + "@react-email/preview-server/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.0", "", { "os": "linux", "cpu": "none" }, "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw=="], + + "@react-email/preview-server/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.0", "", { "os": "linux", "cpu": "none" }, "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ=="], + + "@react-email/preview-server/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw=="], + + "@react-email/preview-server/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.0", "", { "os": "linux", "cpu": "none" }, "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA=="], + + "@react-email/preview-server/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA=="], + + "@react-email/preview-server/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.0", "", { "os": "linux", "cpu": "x64" }, "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw=="], + + "@react-email/preview-server/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.0", "", { "os": "none", "cpu": "arm64" }, "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw=="], + + "@react-email/preview-server/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.0", "", { "os": "none", "cpu": "x64" }, "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA=="], + + "@react-email/preview-server/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.0", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw=="], + + "@react-email/preview-server/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg=="], + + "@react-email/preview-server/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.0", "", { "os": "sunos", "cpu": "x64" }, "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg=="], + + "@react-email/preview-server/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw=="], + + "@react-email/preview-server/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA=="], + + "@react-email/preview-server/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.0", "", { "os": "win32", "cpu": "x64" }, "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ=="], + + "@react-email/preview-server/next/@next/env": ["@next/env@15.4.1", "", {}, "sha512-DXQwFGAE2VH+f2TJsKepRXpODPU+scf5fDbKOME8MMyeyswe4XwgRdiiIYmBfkXU+2ssliLYznajTrOQdnLR5A=="], + + "@react-email/preview-server/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.4.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-L+81yMsiHq82VRXS2RVq6OgDwjvA4kDksGU8hfiDHEXP+ncKIUhUsadAVB+MRIp2FErs/5hpXR0u2eluWPAhig=="], + + "@react-email/preview-server/next/@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.4.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-jfz1RXu6SzL14lFl05/MNkcN35lTLMJWPbqt7Xaj35+ZWAX342aePIJrN6xBdGeKl6jPXJm0Yqo3Xvh3Gpo3Uw=="], + + "@react-email/preview-server/next/@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.4.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-k0tOFn3dsnkaGfs6iQz8Ms6f1CyQe4GacXF979sL8PNQxjYS1swx9VsOyUQYaPoGV8nAZ7OX8cYaeiXGq9ahPQ=="], + + "@react-email/preview-server/next/@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.4.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-4ogGQ/3qDzbbK3IwV88ltihHFbQVq6Qr+uEapzXHXBH1KsVBZOB50sn6BWHPcFjwSoMX2Tj9eH/fZvQnSIgc3g=="], + + "@react-email/preview-server/next/@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.4.1", "", { "os": "linux", "cpu": "x64" }, "sha512-Jj0Rfw3wIgp+eahMz/tOGwlcYYEFjlBPKU7NqoOkTX0LY45i5W0WcDpgiDWSLrN8KFQq/LW7fZq46gxGCiOYlQ=="], + + "@react-email/preview-server/next/@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.4.1", "", { "os": "linux", "cpu": "x64" }, "sha512-9WlEZfnw1vFqkWsTMzZDgNL7AUI1aiBHi0S2m8jvycPyCq/fbZjtE/nDkhJRYbSjXbtRHYLDBlmP95kpjEmJbw=="], + + "@react-email/preview-server/next/@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.4.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-WodRbZ9g6CQLRZsG3gtrA9w7Qfa9BwDzhFVdlI6sV0OCPq9JrOrJSp9/ioLsezbV8w9RCJ8v55uzJuJ5RgWLZg=="], + + "@react-email/preview-server/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.4.1", "", { "os": "win32", "cpu": "x64" }, "sha512-y+wTBxelk2xiNofmDOVU7O5WxTHcvOoL3srOM0kxTzKDjQ57kPU0tpnPJ/BWrRnsOwXEv0+3QSbGR7hY4n9LkQ=="], + + "@react-email/preview-server/next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], + + "@react-email/preview-server/next/sharp": ["sharp@0.34.3", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.4", "semver": "^7.7.2" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.3", "@img/sharp-darwin-x64": "0.34.3", "@img/sharp-libvips-darwin-arm64": "1.2.0", "@img/sharp-libvips-darwin-x64": "1.2.0", "@img/sharp-libvips-linux-arm": "1.2.0", "@img/sharp-libvips-linux-arm64": "1.2.0", "@img/sharp-libvips-linux-ppc64": "1.2.0", "@img/sharp-libvips-linux-s390x": "1.2.0", "@img/sharp-libvips-linux-x64": "1.2.0", "@img/sharp-libvips-linuxmusl-arm64": "1.2.0", "@img/sharp-libvips-linuxmusl-x64": "1.2.0", "@img/sharp-linux-arm": "0.34.3", "@img/sharp-linux-arm64": "0.34.3", "@img/sharp-linux-ppc64": "0.34.3", "@img/sharp-linux-s390x": "0.34.3", "@img/sharp-linux-x64": "0.34.3", "@img/sharp-linuxmusl-arm64": "0.34.3", "@img/sharp-linuxmusl-x64": "0.34.3", "@img/sharp-wasm32": "0.34.3", "@img/sharp-win32-arm64": "0.34.3", "@img/sharp-win32-ia32": "0.34.3", "@img/sharp-win32-x64": "0.34.3" } }, "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg=="], + + "@react-email/preview-server/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.1", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.1.0" }, "os": "darwin", "cpu": "arm64" }, "sha512-pn44xgBtgpEbZsu+lWf2KNb6OAf70X68k+yk69Ic2Xz11zHR/w24/U49XT7AeRwJ0Px+mhALhU5LPci1Aymk7A=="], + + "@react-email/preview-server/sharp/@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.1", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.1.0" }, "os": "darwin", "cpu": "x64" }, "sha512-VfuYgG2r8BpYiOUN+BfYeFo69nP/MIwAtSJ7/Zpxc5QF3KS22z8Pvg3FkrSFJBPNQ7mmcUcYQFBmEQp7eu1F8Q=="], + + "@react-email/preview-server/sharp/@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.1.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA=="], + + "@react-email/preview-server/sharp/@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.1.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ=="], + + "@react-email/preview-server/sharp/@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.1.0", "", { "os": "linux", "cpu": "arm" }, "sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA=="], + + "@react-email/preview-server/sharp/@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.1.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew=="], + + "@react-email/preview-server/sharp/@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.1.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ=="], + + "@react-email/preview-server/sharp/@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.1.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA=="], + + "@react-email/preview-server/sharp/@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.1.0", "", { "os": "linux", "cpu": "x64" }, "sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q=="], + + "@react-email/preview-server/sharp/@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.1.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w=="], + + "@react-email/preview-server/sharp/@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.1.0", "", { "os": "linux", "cpu": "x64" }, "sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A=="], + + "@react-email/preview-server/sharp/@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.1", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.1.0" }, "os": "linux", "cpu": "arm" }, "sha512-anKiszvACti2sGy9CirTlNyk7BjjZPiML1jt2ZkTdcvpLU1YH6CXwRAZCA2UmRXnhiIftXQ7+Oh62Ji25W72jA=="], + + "@react-email/preview-server/sharp/@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.1", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.1.0" }, "os": "linux", "cpu": "arm64" }, "sha512-kX2c+vbvaXC6vly1RDf/IWNXxrlxLNpBVWkdpRq5Ka7OOKj6nr66etKy2IENf6FtOgklkg9ZdGpEu9kwdlcwOQ=="], + + "@react-email/preview-server/sharp/@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.1", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.1.0" }, "os": "linux", "cpu": "s390x" }, "sha512-7s0KX2tI9mZI2buRipKIw2X1ufdTeaRgwmRabt5bi9chYfhur+/C1OXg3TKg/eag1W+6CCWLVmSauV1owmRPxA=="], + + "@react-email/preview-server/sharp/@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.1", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.1.0" }, "os": "linux", "cpu": "x64" }, "sha512-wExv7SH9nmoBW3Wr2gvQopX1k8q2g5V5Iag8Zk6AVENsjwd+3adjwxtp3Dcu2QhOXr8W9NusBU6XcQUohBZ5MA=="], + + "@react-email/preview-server/sharp/@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.1", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.1.0" }, "os": "linux", "cpu": "arm64" }, "sha512-DfvyxzHxw4WGdPiTF0SOHnm11Xv4aQexvqhRDAoD00MzHekAj9a/jADXeXYCDFH/DzYruwHbXU7uz+H+nWmSOQ=="], + + "@react-email/preview-server/sharp/@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.1", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.1.0" }, "os": "linux", "cpu": "x64" }, "sha512-pax/kTR407vNb9qaSIiWVnQplPcGU8LRIJpDT5o8PdAx5aAA7AS3X9PS8Isw1/WfqgQorPotjrZL3Pqh6C5EBg=="], + + "@react-email/preview-server/sharp/@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.1", "", { "dependencies": { "@emnapi/runtime": "^1.4.0" }, "cpu": "none" }, "sha512-YDybQnYrLQfEpzGOQe7OKcyLUCML4YOXl428gOOzBgN6Gw0rv8dpsJ7PqTHxBnXnwXr8S1mYFSLSa727tpz0xg=="], + + "@react-email/preview-server/sharp/@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-WKf/NAZITnonBf3U1LfdjoMgNO5JYRSlhovhRhMxXVdvWYveM4kM3L8m35onYIdh75cOMCo1BexgVQcCDzyoWw=="], + + "@react-email/preview-server/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.1", "", { "os": "win32", "cpu": "x64" }, "sha512-hw1iIAHpNE8q3uMIRCgGOeDoz9KtFNarFLQclLxr/LK1VBkj8nby18RjFvr6aP7USRYAjTZW6yisnBWMX571Tw=="], + + "@react-email/preview-server/tailwindcss/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], + + "@react-email/preview-server/tailwindcss/jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], + + "@react-email/preview-server/tailwindcss/lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="], + + "@react-email/preview-server/tailwindcss/postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="], + + "@sentry/bundler-plugin-core/@babel/core/@babel/parser": ["@babel/parser@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.0" }, "bin": "./bin/babel-parser.js" }, "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g=="], + + "@sentry/bundler-plugin-core/@babel/core/@babel/traverse": ["@babel/traverse@7.28.0", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/types": "^7.28.0", "debug": "^4.3.1" } }, "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg=="], + + "@sentry/bundler-plugin-core/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@sentry/bundler-plugin-core/glob/minimatch": ["minimatch@8.0.4", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA=="], "@sentry/bundler-plugin-core/glob/minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="], @@ -3888,6 +4170,14 @@ "@trigger.dev/core/socket.io-client/engine.io-client": ["engine.io-client@6.5.4", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", "ws": "~8.17.1", "xmlhttprequest-ssl": "~2.0.0" } }, "sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ=="], + "@types/webpack/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + + "@vitejs/plugin-react/@babel/core/@babel/parser": ["@babel/parser@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.0" }, "bin": "./bin/babel-parser.js" }, "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g=="], + + "@vitejs/plugin-react/@babel/core/@babel/traverse": ["@babel/traverse@7.28.0", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/types": "^7.28.0", "debug": "^4.3.1" } }, "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg=="], + + "@vitejs/plugin-react/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "cli-truncate/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], @@ -3902,12 +4192,6 @@ "groq-sdk/node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], - "inquirer/ora/is-interactive": ["is-interactive@1.0.0", "", {}, "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="], - - "inquirer/ora/is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="], - - "inquirer/ora/log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], - "isomorphic-unfetch/node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], "lint-staged/listr2/cli-truncate": ["cli-truncate@4.0.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^7.0.0" } }, "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA=="], @@ -3964,11 +4248,19 @@ "openai/node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], - "ora/cli-cursor/restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], + "react-email/log-symbols/is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], - "ora/log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="], + "react-email/ora/cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], - "ora/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + "react-email/ora/is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="], + + "react-email/ora/is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], + + "react-email/ora/log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="], + + "react-email/ora/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "react-email/ora/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], "resend/@react-email/render/prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="], @@ -4016,6 +4308,32 @@ "@cerebras/cerebras_cloud_sdk/node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + "@radix-ui/react-toggle-group/@radix-ui/react-roving-focus/@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w=="], + + "@react-email/preview-server/@radix-ui/react-dropdown-menu/@radix-ui/react-menu/@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-slot": "1.2.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-cv4vSf7HttqXilDnAnvINd53OTl1/bjUYVZrkFnA7nwmY9Ob2POUy0WY0sfqBAe1s5FyKsyceQlqiEGPYNTadg=="], + + "@react-email/preview-server/@radix-ui/react-dropdown-menu/@radix-ui/react-menu/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.7", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-j5+WBUdhccJsmH5/H0K6RncjDtoALSEr6jbkaZu+bjw6hOPOhHycr6vEUujl+HBK8kjUfWcoCJXxP6e4lUlMZw=="], + + "@react-email/preview-server/@radix-ui/react-dropdown-menu/@radix-ui/react-menu/@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-r2annK27lIW5w9Ho5NyQgqs0MmgZSTIKXWpVCJaLC1q2kZrZkcqnmHkCHMEmv8XLvsLlurKMPT+kbKkRkm/xVA=="], + + "@react-email/preview-server/@radix-ui/react-dropdown-menu/@radix-ui/react-menu/@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.4", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.4", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3p2Rgm/a1cK0r/UVkx5F/K9v/EplfjAeIFCGOPYPO4lZ0jtg4iSQXt/YGTSLWaf4x7NG6Z4+uKFcylcTZjeqDA=="], + + "@react-email/preview-server/@radix-ui/react-dropdown-menu/@radix-ui/react-menu/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.6", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XmsIl2z1n/TsYFLIdYam2rmFwf9OC/Sh2avkbmVMDuBZIe7hSpM0cYnWPAo7nHOVx8zTuwDZGByfcqLdnzp3Vw=="], + + "@react-email/preview-server/@radix-ui/react-dropdown-menu/@radix-ui/react-menu/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-IrVLIhskYhH3nLvtcBLQFZr61tBG7wx7O3kEmdzcYwRGAEBmBicGGL7ATzNgruYJ3xBTbuzEEq9OXJM3PAX3tA=="], + + "@react-email/preview-server/@radix-ui/react-dropdown-menu/@radix-ui/react-menu/@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.4", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-D2ReXCuIueKf5L2f1ks/wTj3bWck1SvK1pjLmEHPbwksS1nOHBsvgY0b9Hypt81FczqBqSyLHQxn/vbsQ0gDHw=="], + + "@react-email/preview-server/@radix-ui/react-popover/@radix-ui/react-popper/@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.4", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-qz+fxrqgNxG0dYew5l7qR3c7wdgRu1XVUHGnGYX7rg5HM4p9SWaRmJwfgR3J0SgyUKayLmzQIun+N6rWRgiRKw=="], + + "@react-email/preview-server/@radix-ui/react-tabs/@radix-ui/react-roving-focus/@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-slot": "1.2.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-cv4vSf7HttqXilDnAnvINd53OTl1/bjUYVZrkFnA7nwmY9Ob2POUy0WY0sfqBAe1s5FyKsyceQlqiEGPYNTadg=="], + + "@react-email/preview-server/@radix-ui/react-tooltip/@radix-ui/react-popper/@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.4", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-qz+fxrqgNxG0dYew5l7qR3c7wdgRu1XVUHGnGYX7rg5HM4p9SWaRmJwfgR3J0SgyUKayLmzQIun+N6rWRgiRKw=="], + + "@react-email/preview-server/tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "@react-email/preview-server/tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + "@sentry/bundler-plugin-core/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], "@sentry/bundler-plugin-core/glob/path-scurry/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], @@ -4084,7 +4402,11 @@ "openai/node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], - "ora/cli-cursor/restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], + "react-email/ora/cli-cursor/restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], + + "react-email/ora/log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="], + + "react-email/ora/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], "sim/tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], @@ -4096,6 +4418,10 @@ "unplugin/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "@react-email/preview-server/@radix-ui/react-dropdown-menu/@radix-ui/react-menu/@radix-ui/react-popper/@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.4", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-qz+fxrqgNxG0dYew5l7qR3c7wdgRu1XVUHGnGYX7rg5HM4p9SWaRmJwfgR3J0SgyUKayLmzQIun+N6rWRgiRKw=="], + + "@react-email/preview-server/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "@trigger.dev/core/@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.52.1", "", { "dependencies": { "@opentelemetry/core": "1.25.1", "@opentelemetry/otlp-transformer": "0.52.1" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0" } }, "sha512-z175NXOtX5ihdlshtYBe5RpGeBoTXVCKPPLiQlD6FHvpM4Ch+p2B0yWKYSrBfLH24H9zjJiBdTrtD+hLlfnXEQ=="], "lint-staged/listr2/cli-truncate/string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], @@ -4112,6 +4438,8 @@ "log-update/cli-cursor/restore-cursor/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + "react-email/ora/cli-cursor/restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], + "sim/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "lint-staged/listr2/cli-truncate/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], From cea42f5135216e130cd46f7ea4426089b495df23 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 20 Aug 2025 17:04:39 -0700 Subject: [PATCH 04/22] improvement(gpt-5): added reasoning level and verbosity to gpt-5 models (#1058) --- apps/sim/blocks/blocks/agent.ts | 34 +++++++++++ apps/sim/providers/azure-openai/index.ts | 4 ++ apps/sim/providers/models.ts | 75 ++++++++++++++++++++++++ apps/sim/providers/openai/index.ts | 4 ++ apps/sim/providers/types.ts | 3 + apps/sim/providers/utils.test.ts | 63 ++++++++++++++++++++ apps/sim/providers/utils.ts | 4 ++ 7 files changed, 187 insertions(+) diff --git a/apps/sim/blocks/blocks/agent.ts b/apps/sim/blocks/blocks/agent.ts index 0a7c0c13b0..ab5e84bc4d 100644 --- a/apps/sim/blocks/blocks/agent.ts +++ b/apps/sim/blocks/blocks/agent.ts @@ -9,7 +9,9 @@ import { getProviderIcon, MODELS_TEMP_RANGE_0_1, MODELS_TEMP_RANGE_0_2, + MODELS_WITH_REASONING_EFFORT, MODELS_WITH_TEMPERATURE_SUPPORT, + MODELS_WITH_VERBOSITY, providers, } from '@/providers/utils' @@ -210,6 +212,36 @@ Create a system prompt appropriately detailed for the request, using clear langu }, }, }, + { + id: 'reasoningEffort', + title: 'Reasoning Effort', + type: 'combobox', + layout: 'half', + placeholder: 'Select reasoning effort...', + options: () => { + return [ + { label: 'low', id: 'low' }, + { label: 'medium', id: 'medium' }, + { label: 'high', id: 'high' }, + ] + }, + condition: { + field: 'model', + value: MODELS_WITH_REASONING_EFFORT, + }, + }, + { + id: 'verbosity', + title: 'Verbosity', + type: 'slider', + layout: 'half', + min: 0, + max: 2, + condition: { + field: 'model', + value: MODELS_WITH_VERBOSITY, + }, + }, { id: 'apiKey', title: 'API Key', @@ -485,6 +517,8 @@ Example 3 (Array Input): }, }, temperature: { type: 'number', description: 'Response randomness level' }, + reasoningEffort: { type: 'string', description: 'Reasoning effort level for GPT-5 models' }, + verbosity: { type: 'number', description: 'Verbosity level for GPT-5 models' }, tools: { type: 'json', description: 'Available tools configuration' }, }, outputs: { diff --git a/apps/sim/providers/azure-openai/index.ts b/apps/sim/providers/azure-openai/index.ts index b000765ecb..decfe446ac 100644 --- a/apps/sim/providers/azure-openai/index.ts +++ b/apps/sim/providers/azure-openai/index.ts @@ -144,6 +144,10 @@ export const azureOpenAIProvider: ProviderConfig = { if (request.temperature !== undefined) payload.temperature = request.temperature if (request.maxTokens !== undefined) payload.max_tokens = request.maxTokens + // Add GPT-5 specific parameters + if (request.reasoningEffort !== undefined) payload.reasoning_effort = request.reasoningEffort + if (request.verbosity !== undefined) payload.verbosity = request.verbosity + // Add response format for structured output if specified if (request.responseFormat) { // Use Azure OpenAI's JSON schema format diff --git a/apps/sim/providers/models.ts b/apps/sim/providers/models.ts index 348f55a13f..dadeaa1804 100644 --- a/apps/sim/providers/models.ts +++ b/apps/sim/providers/models.ts @@ -34,6 +34,15 @@ export interface ModelCapabilities { } toolUsageControl?: boolean computerUse?: boolean + reasoningEffort?: { + min: string + max: string + values: string[] + } + verbosity?: { + min: number + max: number + } } export interface ModelDefinition { @@ -87,6 +96,12 @@ export const PROVIDER_DEFINITIONS: Record = { }, capabilities: { toolUsageControl: true, + reasoningEffort: { + min: 'low', + max: 'high', + values: ['low', 'medium', 'high'], + }, + verbosity: { min: 0, max: 2 }, }, }, { @@ -99,6 +114,12 @@ export const PROVIDER_DEFINITIONS: Record = { }, capabilities: { toolUsageControl: true, + reasoningEffort: { + min: 'low', + max: 'high', + values: ['low', 'medium', 'high'], + }, + verbosity: { min: 0, max: 2 }, }, }, { @@ -111,6 +132,12 @@ export const PROVIDER_DEFINITIONS: Record = { }, capabilities: { toolUsageControl: true, + reasoningEffort: { + min: 'low', + max: 'high', + values: ['low', 'medium', 'high'], + }, + verbosity: { min: 0, max: 2 }, }, }, { @@ -233,6 +260,12 @@ export const PROVIDER_DEFINITIONS: Record = { }, capabilities: { toolUsageControl: true, + reasoningEffort: { + min: 'low', + max: 'high', + values: ['low', 'medium', 'high'], + }, + verbosity: { min: 0, max: 2 }, }, }, { @@ -245,6 +278,12 @@ export const PROVIDER_DEFINITIONS: Record = { }, capabilities: { toolUsageControl: true, + reasoningEffort: { + min: 'low', + max: 'high', + values: ['low', 'medium', 'high'], + }, + verbosity: { min: 0, max: 2 }, }, }, { @@ -257,6 +296,12 @@ export const PROVIDER_DEFINITIONS: Record = { }, capabilities: { toolUsageControl: true, + reasoningEffort: { + min: 'low', + max: 'high', + values: ['low', 'medium', 'high'], + }, + verbosity: { min: 0, max: 2 }, }, }, { @@ -844,3 +889,33 @@ export const EMBEDDING_MODEL_PRICING: Record = { export function getEmbeddingModelPricing(modelId: string): ModelPricing | null { return EMBEDDING_MODEL_PRICING[modelId] || null } + +/** + * Get all models that support reasoning effort + */ +export function getModelsWithReasoningEffort(): string[] { + const models: string[] = [] + for (const provider of Object.values(PROVIDER_DEFINITIONS)) { + for (const model of provider.models) { + if (model.capabilities.reasoningEffort) { + models.push(model.id) + } + } + } + return models +} + +/** + * Get all models that support verbosity + */ +export function getModelsWithVerbosity(): string[] { + const models: string[] = [] + for (const provider of Object.values(PROVIDER_DEFINITIONS)) { + for (const model of provider.models) { + if (model.capabilities.verbosity) { + models.push(model.id) + } + } + } + return models +} diff --git a/apps/sim/providers/openai/index.ts b/apps/sim/providers/openai/index.ts index 3de716b56e..5d33812b44 100644 --- a/apps/sim/providers/openai/index.ts +++ b/apps/sim/providers/openai/index.ts @@ -130,6 +130,10 @@ export const openaiProvider: ProviderConfig = { if (request.temperature !== undefined) payload.temperature = request.temperature if (request.maxTokens !== undefined) payload.max_tokens = request.maxTokens + // Add GPT-5 specific parameters + if (request.reasoningEffort !== undefined) payload.reasoning_effort = request.reasoningEffort + if (request.verbosity !== undefined) payload.verbosity = request.verbosity + // Add response format for structured output if specified if (request.responseFormat) { // Use OpenAI's JSON schema format diff --git a/apps/sim/providers/types.ts b/apps/sim/providers/types.ts index 72ad3a4ef0..ff43fe931c 100644 --- a/apps/sim/providers/types.ts +++ b/apps/sim/providers/types.ts @@ -156,6 +156,9 @@ export interface ProviderRequest { // Azure OpenAI specific parameters azureEndpoint?: string azureApiVersion?: string + // GPT-5 specific parameters + reasoningEffort?: string + verbosity?: number } // Map of provider IDs to their configurations diff --git a/apps/sim/providers/utils.test.ts b/apps/sim/providers/utils.test.ts index 343171fcb0..6327dc91ef 100644 --- a/apps/sim/providers/utils.test.ts +++ b/apps/sim/providers/utils.test.ts @@ -19,7 +19,9 @@ import { getProviderModels, MODELS_TEMP_RANGE_0_1, MODELS_TEMP_RANGE_0_2, + MODELS_WITH_REASONING_EFFORT, MODELS_WITH_TEMPERATURE_SUPPORT, + MODELS_WITH_VERBOSITY, PROVIDERS_WITH_TOOL_USAGE_CONTROL, prepareToolsWithUsageControl, supportsTemperature, @@ -144,6 +146,15 @@ describe('Model Capabilities', () => { 'deepseek-chat', 'azure/gpt-4.1', 'azure/model-router', + // GPT-5 models don't support temperature (removed in our implementation) + 'gpt-5', + 'gpt-5-mini', + 'gpt-5-nano', + 'gpt-5-chat-latest', + 'azure/gpt-5', + 'azure/gpt-5-mini', + 'azure/gpt-5-nano', + 'azure/gpt-5-chat-latest', ] for (const model of unsupportedModels) { @@ -198,6 +209,15 @@ describe('Model Capabilities', () => { expect(getMaxTemperature('azure/o3')).toBeUndefined() expect(getMaxTemperature('azure/o4-mini')).toBeUndefined() expect(getMaxTemperature('deepseek-r1')).toBeUndefined() + // GPT-5 models don't support temperature (removed in our implementation) + expect(getMaxTemperature('gpt-5')).toBeUndefined() + expect(getMaxTemperature('gpt-5-mini')).toBeUndefined() + expect(getMaxTemperature('gpt-5-nano')).toBeUndefined() + expect(getMaxTemperature('gpt-5-chat-latest')).toBeUndefined() + expect(getMaxTemperature('azure/gpt-5')).toBeUndefined() + expect(getMaxTemperature('azure/gpt-5-mini')).toBeUndefined() + expect(getMaxTemperature('azure/gpt-5-nano')).toBeUndefined() + expect(getMaxTemperature('azure/gpt-5-chat-latest')).toBeUndefined() }) it.concurrent('should be case insensitive', () => { @@ -266,6 +286,49 @@ describe('Model Capabilities', () => { expect(MODELS_WITH_TEMPERATURE_SUPPORT).toContain('claude-sonnet-4-0') // From 0-1 range } ) + + it.concurrent('should have correct models in MODELS_WITH_REASONING_EFFORT', () => { + // Should contain GPT-5 models that support reasoning effort + expect(MODELS_WITH_REASONING_EFFORT).toContain('gpt-5') + expect(MODELS_WITH_REASONING_EFFORT).toContain('gpt-5-mini') + expect(MODELS_WITH_REASONING_EFFORT).toContain('gpt-5-nano') + expect(MODELS_WITH_REASONING_EFFORT).toContain('azure/gpt-5') + expect(MODELS_WITH_REASONING_EFFORT).toContain('azure/gpt-5-mini') + expect(MODELS_WITH_REASONING_EFFORT).toContain('azure/gpt-5-nano') + + // Should NOT contain non-reasoning GPT-5 models + expect(MODELS_WITH_REASONING_EFFORT).not.toContain('gpt-5-chat-latest') + expect(MODELS_WITH_REASONING_EFFORT).not.toContain('azure/gpt-5-chat-latest') + + // Should NOT contain other models + expect(MODELS_WITH_REASONING_EFFORT).not.toContain('gpt-4o') + expect(MODELS_WITH_REASONING_EFFORT).not.toContain('claude-sonnet-4-0') + expect(MODELS_WITH_REASONING_EFFORT).not.toContain('o1') + }) + + it.concurrent('should have correct models in MODELS_WITH_VERBOSITY', () => { + // Should contain GPT-5 models that support verbosity + expect(MODELS_WITH_VERBOSITY).toContain('gpt-5') + expect(MODELS_WITH_VERBOSITY).toContain('gpt-5-mini') + expect(MODELS_WITH_VERBOSITY).toContain('gpt-5-nano') + expect(MODELS_WITH_VERBOSITY).toContain('azure/gpt-5') + expect(MODELS_WITH_VERBOSITY).toContain('azure/gpt-5-mini') + expect(MODELS_WITH_VERBOSITY).toContain('azure/gpt-5-nano') + + // Should NOT contain non-reasoning GPT-5 models + expect(MODELS_WITH_VERBOSITY).not.toContain('gpt-5-chat-latest') + expect(MODELS_WITH_VERBOSITY).not.toContain('azure/gpt-5-chat-latest') + + // Should NOT contain other models + expect(MODELS_WITH_VERBOSITY).not.toContain('gpt-4o') + expect(MODELS_WITH_VERBOSITY).not.toContain('claude-sonnet-4-0') + expect(MODELS_WITH_VERBOSITY).not.toContain('o1') + }) + + it.concurrent('should have same models in both reasoning effort and verbosity arrays', () => { + // GPT-5 models that support reasoning effort should also support verbosity and vice versa + expect(MODELS_WITH_REASONING_EFFORT.sort()).toEqual(MODELS_WITH_VERBOSITY.sort()) + }) }) }) diff --git a/apps/sim/providers/utils.ts b/apps/sim/providers/utils.ts index 6ab2650f0f..d4b6e3918d 100644 --- a/apps/sim/providers/utils.ts +++ b/apps/sim/providers/utils.ts @@ -12,9 +12,11 @@ import { getHostedModels as getHostedModelsFromDefinitions, getMaxTemperature as getMaxTempFromDefinitions, getModelPricing as getModelPricingFromDefinitions, + getModelsWithReasoningEffort, getModelsWithTemperatureSupport, getModelsWithTempRange01, getModelsWithTempRange02, + getModelsWithVerbosity, getProviderModels as getProviderModelsFromDefinitions, getProvidersWithToolUsageControl, PROVIDER_DEFINITIONS, @@ -878,6 +880,8 @@ export function trackForcedToolUsage( export const MODELS_TEMP_RANGE_0_2 = getModelsWithTempRange02() export const MODELS_TEMP_RANGE_0_1 = getModelsWithTempRange01() export const MODELS_WITH_TEMPERATURE_SUPPORT = getModelsWithTemperatureSupport() +export const MODELS_WITH_REASONING_EFFORT = getModelsWithReasoningEffort() +export const MODELS_WITH_VERBOSITY = getModelsWithVerbosity() export const PROVIDERS_WITH_TOOL_USAGE_CONTROL = getProvidersWithToolUsageControl() /** From c795fc83aa78e486ad3c86040d6abf247ecfe87f Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 20 Aug 2025 17:04:52 -0700 Subject: [PATCH 05/22] feat(azure-openai): allow usage of azure-openai for knowledgebase uploads and wand generation (#1056) * feat(azure-openai): allow usage of azure-openai for knowledgebase uploads * feat(azure-openai): added azure-openai for kb and wand * added embeddings utils, added the ability to use mistral through Azure * fix(oauth): gdrive picker race condition, token route cleanup * fix test * feat(mailer): consolidated all emailing to mailer service, added support for Azure ACS (#1054) * feat(mailer): consolidated all emailing to mailer service, added support for Azure ACS * fix batch invitation email template * cleanup * improvement(emails): add help template instead of doing it inline * remove fallback version --------- Co-authored-by: Vikhyath Mondreti --- .../app/api/knowledge/search/utils.test.ts | 288 ++++++++- apps/sim/app/api/knowledge/search/utils.ts | 69 +-- apps/sim/app/api/knowledge/utils.test.ts | 71 +++ apps/sim/app/api/knowledge/utils.ts | 109 +--- apps/sim/app/api/wand-generate/route.ts | 73 ++- apps/sim/lib/documents/document-processor.ts | 552 +++++++++--------- apps/sim/lib/embeddings/utils.ts | 148 +++++ apps/sim/lib/env.ts | 11 +- 8 files changed, 851 insertions(+), 470 deletions(-) create mode 100644 apps/sim/lib/embeddings/utils.ts diff --git a/apps/sim/app/api/knowledge/search/utils.test.ts b/apps/sim/app/api/knowledge/search/utils.test.ts index 0f8937372d..3fcd04db76 100644 --- a/apps/sim/app/api/knowledge/search/utils.test.ts +++ b/apps/sim/app/api/knowledge/search/utils.test.ts @@ -4,15 +4,50 @@ * * @vitest-environment node */ -import { describe, expect, it, vi } from 'vitest' +import { beforeEach, describe, expect, it, vi } from 'vitest' vi.mock('drizzle-orm') -vi.mock('@/lib/logs/console/logger') +vi.mock('@/lib/logs/console/logger', () => ({ + createLogger: vi.fn(() => ({ + info: vi.fn(), + debug: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + })), +})) vi.mock('@/db') +vi.mock('@/lib/documents/utils', () => ({ + retryWithExponentialBackoff: (fn: any) => fn(), +})) -import { handleTagAndVectorSearch, handleTagOnlySearch, handleVectorOnlySearch } from './utils' +vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ + data: [{ embedding: [0.1, 0.2, 0.3] }], + }), + }) +) + +vi.mock('@/lib/env', () => ({ + env: {}, + isTruthy: (value: string | boolean | number | undefined) => + typeof value === 'string' ? value === 'true' || value === '1' : Boolean(value), +})) + +import { + generateSearchEmbedding, + handleTagAndVectorSearch, + handleTagOnlySearch, + handleVectorOnlySearch, +} from './utils' describe('Knowledge Search Utils', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + describe('handleTagOnlySearch', () => { it('should throw error when no filters provided', async () => { const params = { @@ -140,4 +175,251 @@ describe('Knowledge Search Utils', () => { expect(params.distanceThreshold).toBe(0.8) }) }) + + describe('generateSearchEmbedding', () => { + it('should use Azure OpenAI when KB-specific config is provided', async () => { + const { env } = await import('@/lib/env') + Object.keys(env).forEach((key) => delete (env as any)[key]) + Object.assign(env, { + AZURE_OPENAI_API_KEY: 'test-azure-key', + AZURE_OPENAI_ENDPOINT: 'https://test.openai.azure.com', + AZURE_OPENAI_API_VERSION: '2024-12-01-preview', + KB_OPENAI_MODEL_NAME: 'text-embedding-ada-002', + OPENAI_API_KEY: 'test-openai-key', + }) + + const fetchSpy = vi.mocked(fetch) + fetchSpy.mockResolvedValueOnce({ + ok: true, + json: async () => ({ + data: [{ embedding: [0.1, 0.2, 0.3] }], + }), + } as any) + + const result = await generateSearchEmbedding('test query') + + expect(fetchSpy).toHaveBeenCalledWith( + 'https://test.openai.azure.com/openai/deployments/text-embedding-ada-002/embeddings?api-version=2024-12-01-preview', + expect.objectContaining({ + headers: expect.objectContaining({ + 'api-key': 'test-azure-key', + }), + }) + ) + expect(result).toEqual([0.1, 0.2, 0.3]) + + // Clean up + Object.keys(env).forEach((key) => delete (env as any)[key]) + }) + + it('should fallback to OpenAI when no KB Azure config provided', async () => { + const { env } = await import('@/lib/env') + Object.keys(env).forEach((key) => delete (env as any)[key]) + Object.assign(env, { + OPENAI_API_KEY: 'test-openai-key', + }) + + const fetchSpy = vi.mocked(fetch) + fetchSpy.mockResolvedValueOnce({ + ok: true, + json: async () => ({ + data: [{ embedding: [0.1, 0.2, 0.3] }], + }), + } as any) + + const result = await generateSearchEmbedding('test query') + + expect(fetchSpy).toHaveBeenCalledWith( + 'https://api.openai.com/v1/embeddings', + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: 'Bearer test-openai-key', + }), + }) + ) + expect(result).toEqual([0.1, 0.2, 0.3]) + + // Clean up + Object.keys(env).forEach((key) => delete (env as any)[key]) + }) + + it('should use default API version when not provided in Azure config', async () => { + const { env } = await import('@/lib/env') + Object.keys(env).forEach((key) => delete (env as any)[key]) + Object.assign(env, { + AZURE_OPENAI_API_KEY: 'test-azure-key', + AZURE_OPENAI_ENDPOINT: 'https://test.openai.azure.com', + KB_OPENAI_MODEL_NAME: 'custom-embedding-model', + OPENAI_API_KEY: 'test-openai-key', + }) + + const fetchSpy = vi.mocked(fetch) + fetchSpy.mockResolvedValueOnce({ + ok: true, + json: async () => ({ + data: [{ embedding: [0.1, 0.2, 0.3] }], + }), + } as any) + + await generateSearchEmbedding('test query') + + expect(fetchSpy).toHaveBeenCalledWith( + expect.stringContaining('api-version='), + expect.any(Object) + ) + + // Clean up + Object.keys(env).forEach((key) => delete (env as any)[key]) + }) + + it('should use custom model name when provided in Azure config', async () => { + const { env } = await import('@/lib/env') + Object.keys(env).forEach((key) => delete (env as any)[key]) + Object.assign(env, { + AZURE_OPENAI_API_KEY: 'test-azure-key', + AZURE_OPENAI_ENDPOINT: 'https://test.openai.azure.com', + AZURE_OPENAI_API_VERSION: '2024-12-01-preview', + KB_OPENAI_MODEL_NAME: 'custom-embedding-model', + OPENAI_API_KEY: 'test-openai-key', + }) + + const fetchSpy = vi.mocked(fetch) + fetchSpy.mockResolvedValueOnce({ + ok: true, + json: async () => ({ + data: [{ embedding: [0.1, 0.2, 0.3] }], + }), + } as any) + + await generateSearchEmbedding('test query', 'text-embedding-3-small') + + expect(fetchSpy).toHaveBeenCalledWith( + 'https://test.openai.azure.com/openai/deployments/custom-embedding-model/embeddings?api-version=2024-12-01-preview', + expect.any(Object) + ) + + // Clean up + Object.keys(env).forEach((key) => delete (env as any)[key]) + }) + + it('should throw error when no API configuration provided', async () => { + const { env } = await import('@/lib/env') + Object.keys(env).forEach((key) => delete (env as any)[key]) + + await expect(generateSearchEmbedding('test query')).rejects.toThrow( + 'Either OPENAI_API_KEY or Azure OpenAI configuration (AZURE_OPENAI_API_KEY + AZURE_OPENAI_ENDPOINT) must be configured' + ) + }) + + it('should handle Azure OpenAI API errors properly', async () => { + const { env } = await import('@/lib/env') + Object.keys(env).forEach((key) => delete (env as any)[key]) + Object.assign(env, { + AZURE_OPENAI_API_KEY: 'test-azure-key', + AZURE_OPENAI_ENDPOINT: 'https://test.openai.azure.com', + AZURE_OPENAI_API_VERSION: '2024-12-01-preview', + KB_OPENAI_MODEL_NAME: 'text-embedding-ada-002', + }) + + const fetchSpy = vi.mocked(fetch) + fetchSpy.mockResolvedValueOnce({ + ok: false, + status: 404, + statusText: 'Not Found', + text: async () => 'Deployment not found', + } as any) + + await expect(generateSearchEmbedding('test query')).rejects.toThrow('Embedding API failed') + + // Clean up + Object.keys(env).forEach((key) => delete (env as any)[key]) + }) + + it('should handle OpenAI API errors properly', async () => { + const { env } = await import('@/lib/env') + Object.keys(env).forEach((key) => delete (env as any)[key]) + Object.assign(env, { + OPENAI_API_KEY: 'test-openai-key', + }) + + const fetchSpy = vi.mocked(fetch) + fetchSpy.mockResolvedValueOnce({ + ok: false, + status: 429, + statusText: 'Too Many Requests', + text: async () => 'Rate limit exceeded', + } as any) + + await expect(generateSearchEmbedding('test query')).rejects.toThrow('Embedding API failed') + + // Clean up + Object.keys(env).forEach((key) => delete (env as any)[key]) + }) + + it('should include correct request body for Azure OpenAI', async () => { + const { env } = await import('@/lib/env') + Object.keys(env).forEach((key) => delete (env as any)[key]) + Object.assign(env, { + AZURE_OPENAI_API_KEY: 'test-azure-key', + AZURE_OPENAI_ENDPOINT: 'https://test.openai.azure.com', + AZURE_OPENAI_API_VERSION: '2024-12-01-preview', + KB_OPENAI_MODEL_NAME: 'text-embedding-ada-002', + }) + + const fetchSpy = vi.mocked(fetch) + fetchSpy.mockResolvedValueOnce({ + ok: true, + json: async () => ({ + data: [{ embedding: [0.1, 0.2, 0.3] }], + }), + } as any) + + await generateSearchEmbedding('test query') + + expect(fetchSpy).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + body: JSON.stringify({ + input: ['test query'], + encoding_format: 'float', + }), + }) + ) + + // Clean up + Object.keys(env).forEach((key) => delete (env as any)[key]) + }) + + it('should include correct request body for OpenAI', async () => { + const { env } = await import('@/lib/env') + Object.keys(env).forEach((key) => delete (env as any)[key]) + Object.assign(env, { + OPENAI_API_KEY: 'test-openai-key', + }) + + const fetchSpy = vi.mocked(fetch) + fetchSpy.mockResolvedValueOnce({ + ok: true, + json: async () => ({ + data: [{ embedding: [0.1, 0.2, 0.3] }], + }), + } as any) + + await generateSearchEmbedding('test query', 'text-embedding-3-small') + + expect(fetchSpy).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + body: JSON.stringify({ + input: ['test query'], + model: 'text-embedding-3-small', + encoding_format: 'float', + }), + }) + ) + + // Clean up + Object.keys(env).forEach((key) => delete (env as any)[key]) + }) + }) }) diff --git a/apps/sim/app/api/knowledge/search/utils.ts b/apps/sim/app/api/knowledge/search/utils.ts index 151054ab72..7a72e2703d 100644 --- a/apps/sim/app/api/knowledge/search/utils.ts +++ b/apps/sim/app/api/knowledge/search/utils.ts @@ -1,22 +1,10 @@ import { and, eq, inArray, sql } from 'drizzle-orm' -import { retryWithExponentialBackoff } from '@/lib/documents/utils' -import { env } from '@/lib/env' import { createLogger } from '@/lib/logs/console/logger' import { db } from '@/db' import { embedding } from '@/db/schema' const logger = createLogger('KnowledgeSearchUtils') -export class APIError extends Error { - public status: number - - constructor(message: string, status: number) { - super(message) - this.name = 'APIError' - this.status = status - } -} - export interface SearchResult { id: string content: string @@ -41,61 +29,8 @@ export interface SearchParams { distanceThreshold?: number } -export async function generateSearchEmbedding(query: string): Promise { - const openaiApiKey = env.OPENAI_API_KEY - if (!openaiApiKey) { - throw new Error('OPENAI_API_KEY not configured') - } - - try { - const embedding = await retryWithExponentialBackoff( - async () => { - const response = await fetch('https://api.openai.com/v1/embeddings', { - method: 'POST', - headers: { - Authorization: `Bearer ${openaiApiKey}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - input: query, - model: 'text-embedding-3-small', - encoding_format: 'float', - }), - }) - - if (!response.ok) { - const errorText = await response.text() - const error = new APIError( - `OpenAI API error: ${response.status} ${response.statusText} - ${errorText}`, - response.status - ) - throw error - } - - const data = await response.json() - - if (!data.data || !Array.isArray(data.data) || data.data.length === 0) { - throw new Error('Invalid response format from OpenAI embeddings API') - } - - return data.data[0].embedding - }, - { - maxRetries: 5, - initialDelayMs: 1000, - maxDelayMs: 30000, - backoffMultiplier: 2, - } - ) - - return embedding - } catch (error) { - logger.error('Failed to generate search embedding:', error) - throw new Error( - `Embedding generation failed: ${error instanceof Error ? error.message : 'Unknown error'}` - ) - } -} +// Use shared embedding utility +export { generateSearchEmbedding } from '@/lib/embeddings/utils' function getTagFilters(filters: Record, embedding: any) { return Object.entries(filters).map(([key, value]) => { diff --git a/apps/sim/app/api/knowledge/utils.test.ts b/apps/sim/app/api/knowledge/utils.test.ts index 24d74a8897..0c5e84e637 100644 --- a/apps/sim/app/api/knowledge/utils.test.ts +++ b/apps/sim/app/api/knowledge/utils.test.ts @@ -252,5 +252,76 @@ describe('Knowledge Utils', () => { expect(result.length).toBe(2) }) + + it('should use Azure OpenAI when Azure config is provided', async () => { + const { env } = await import('@/lib/env') + Object.keys(env).forEach((key) => delete (env as any)[key]) + Object.assign(env, { + AZURE_OPENAI_API_KEY: 'test-azure-key', + AZURE_OPENAI_ENDPOINT: 'https://test.openai.azure.com', + AZURE_OPENAI_API_VERSION: '2024-12-01-preview', + KB_OPENAI_MODEL_NAME: 'text-embedding-ada-002', + OPENAI_API_KEY: 'test-openai-key', + }) + + const fetchSpy = vi.mocked(fetch) + fetchSpy.mockResolvedValueOnce({ + ok: true, + json: async () => ({ + data: [{ embedding: [0.1, 0.2], index: 0 }], + }), + } as any) + + await generateEmbeddings(['test text']) + + expect(fetchSpy).toHaveBeenCalledWith( + 'https://test.openai.azure.com/openai/deployments/text-embedding-ada-002/embeddings?api-version=2024-12-01-preview', + expect.objectContaining({ + headers: expect.objectContaining({ + 'api-key': 'test-azure-key', + }), + }) + ) + + Object.keys(env).forEach((key) => delete (env as any)[key]) + }) + + it('should fallback to OpenAI when no Azure config provided', async () => { + const { env } = await import('@/lib/env') + Object.keys(env).forEach((key) => delete (env as any)[key]) + Object.assign(env, { + OPENAI_API_KEY: 'test-openai-key', + }) + + const fetchSpy = vi.mocked(fetch) + fetchSpy.mockResolvedValueOnce({ + ok: true, + json: async () => ({ + data: [{ embedding: [0.1, 0.2], index: 0 }], + }), + } as any) + + await generateEmbeddings(['test text']) + + expect(fetchSpy).toHaveBeenCalledWith( + 'https://api.openai.com/v1/embeddings', + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: 'Bearer test-openai-key', + }), + }) + ) + + Object.keys(env).forEach((key) => delete (env as any)[key]) + }) + + it('should throw error when no API configuration provided', async () => { + const { env } = await import('@/lib/env') + Object.keys(env).forEach((key) => delete (env as any)[key]) + + await expect(generateEmbeddings(['test text'])).rejects.toThrow( + 'Either OPENAI_API_KEY or Azure OpenAI configuration (AZURE_OPENAI_API_KEY + AZURE_OPENAI_ENDPOINT) must be configured' + ) + }) }) }) diff --git a/apps/sim/app/api/knowledge/utils.ts b/apps/sim/app/api/knowledge/utils.ts index 448f6f53d1..df85c67df1 100644 --- a/apps/sim/app/api/knowledge/utils.ts +++ b/apps/sim/app/api/knowledge/utils.ts @@ -1,8 +1,7 @@ import crypto from 'crypto' import { and, eq, isNull } from 'drizzle-orm' import { processDocument } from '@/lib/documents/document-processor' -import { retryWithExponentialBackoff } from '@/lib/documents/utils' -import { env } from '@/lib/env' +import { generateEmbeddings } from '@/lib/embeddings/utils' import { createLogger } from '@/lib/logs/console/logger' import { getUserEntityPermissions } from '@/lib/permissions/utils' import { db } from '@/db' @@ -10,22 +9,11 @@ import { document, embedding, knowledgeBase } from '@/db/schema' const logger = createLogger('KnowledgeUtils') -// Timeout constants (in milliseconds) const TIMEOUTS = { OVERALL_PROCESSING: 150000, // 150 seconds (2.5 minutes) EMBEDDINGS_API: 60000, // 60 seconds per batch } as const -class APIError extends Error { - public status: number - - constructor(message: string, status: number) { - super(message) - this.name = 'APIError' - this.status = status - } -} - /** * Create a timeout wrapper for async operations */ @@ -110,18 +98,6 @@ export interface EmbeddingData { updatedAt: Date } -interface OpenAIEmbeddingResponse { - data: Array<{ - embedding: number[] - index: number - }> - model: string - usage: { - prompt_tokens: number - total_tokens: number - } -} - export interface KnowledgeBaseAccessResult { hasAccess: true knowledgeBase: Pick @@ -405,87 +381,8 @@ export async function checkChunkAccess( } } -/** - * Generate embeddings using OpenAI API with retry logic for rate limiting - */ -export async function generateEmbeddings( - texts: string[], - embeddingModel = 'text-embedding-3-small' -): Promise { - const openaiApiKey = env.OPENAI_API_KEY - if (!openaiApiKey) { - throw new Error('OPENAI_API_KEY not configured') - } - - try { - const batchSize = 100 - const allEmbeddings: number[][] = [] - - for (let i = 0; i < texts.length; i += batchSize) { - const batch = texts.slice(i, i + batchSize) - - logger.info( - `Generating embeddings for batch ${Math.floor(i / batchSize) + 1} (${batch.length} texts)` - ) - - const batchEmbeddings = await retryWithExponentialBackoff( - async () => { - const controller = new AbortController() - const timeoutId = setTimeout(() => controller.abort(), TIMEOUTS.EMBEDDINGS_API) - - try { - const response = await fetch('https://api.openai.com/v1/embeddings', { - method: 'POST', - headers: { - Authorization: `Bearer ${openaiApiKey}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - input: batch, - model: embeddingModel, - encoding_format: 'float', - }), - signal: controller.signal, - }) - - clearTimeout(timeoutId) - - if (!response.ok) { - const errorText = await response.text() - const error = new APIError( - `OpenAI API error: ${response.status} ${response.statusText} - ${errorText}`, - response.status - ) - throw error - } - - const data: OpenAIEmbeddingResponse = await response.json() - return data.data.map((item) => item.embedding) - } catch (error) { - clearTimeout(timeoutId) - if (error instanceof Error && error.name === 'AbortError') { - throw new Error('OpenAI API request timed out') - } - throw error - } - }, - { - maxRetries: 5, - initialDelayMs: 1000, - maxDelayMs: 60000, // Max 1 minute delay for embeddings - backoffMultiplier: 2, - } - ) - - allEmbeddings.push(...batchEmbeddings) - } - - return allEmbeddings - } catch (error) { - logger.error('Failed to generate embeddings:', error) - throw error - } -} +// Export for external use +export { generateEmbeddings } /** * Process a document asynchronously with full error handling diff --git a/apps/sim/app/api/wand-generate/route.ts b/apps/sim/app/api/wand-generate/route.ts index d7eeba5be0..a7bee44f90 100644 --- a/apps/sim/app/api/wand-generate/route.ts +++ b/apps/sim/app/api/wand-generate/route.ts @@ -1,6 +1,6 @@ import { unstable_noStore as noStore } from 'next/cache' import { type NextRequest, NextResponse } from 'next/server' -import OpenAI from 'openai' +import OpenAI, { AzureOpenAI } from 'openai' import { env } from '@/lib/env' import { createLogger } from '@/lib/logs/console/logger' @@ -10,14 +10,32 @@ export const maxDuration = 60 const logger = createLogger('WandGenerateAPI') -const openai = env.OPENAI_API_KEY - ? new OpenAI({ - apiKey: env.OPENAI_API_KEY, - }) - : null +const azureApiKey = env.AZURE_OPENAI_API_KEY +const azureEndpoint = env.AZURE_OPENAI_ENDPOINT +const azureApiVersion = env.AZURE_OPENAI_API_VERSION +const wandModelName = env.WAND_OPENAI_MODEL_NAME || 'gpt-4o' +const openaiApiKey = env.OPENAI_API_KEY + +const useWandAzure = azureApiKey && azureEndpoint && azureApiVersion -if (!env.OPENAI_API_KEY) { - logger.warn('OPENAI_API_KEY not found. Wand generation API will not function.') +const client = useWandAzure + ? new AzureOpenAI({ + apiKey: azureApiKey, + apiVersion: azureApiVersion, + endpoint: azureEndpoint, + }) + : openaiApiKey + ? new OpenAI({ + apiKey: openaiApiKey, + }) + : null + +if (!useWandAzure && !openaiApiKey) { + logger.warn( + 'Neither Azure OpenAI nor OpenAI API key found. Wand generation API will not function.' + ) +} else { + logger.info(`Using ${useWandAzure ? 'Azure OpenAI' : 'OpenAI'} for wand generation`) } interface ChatMessage { @@ -32,14 +50,12 @@ interface RequestBody { history?: ChatMessage[] } -// The endpoint is now generic - system prompts come from wand configs - export async function POST(req: NextRequest) { const requestId = crypto.randomUUID().slice(0, 8) logger.info(`[${requestId}] Received wand generation request`) - if (!openai) { - logger.error(`[${requestId}] OpenAI client not initialized. Missing API key.`) + if (!client) { + logger.error(`[${requestId}] AI client not initialized. Missing API key.`) return NextResponse.json( { success: false, error: 'Wand generation service is not configured.' }, { status: 503 } @@ -74,16 +90,19 @@ export async function POST(req: NextRequest) { // Add the current user prompt messages.push({ role: 'user', content: prompt }) - logger.debug(`[${requestId}] Calling OpenAI API for wand generation`, { - stream, - historyLength: history.length, - }) + logger.debug( + `[${requestId}] Calling ${useWandAzure ? 'Azure OpenAI' : 'OpenAI'} API for wand generation`, + { + stream, + historyLength: history.length, + } + ) // For streaming responses if (stream) { try { - const streamCompletion = await openai?.chat.completions.create({ - model: 'gpt-4o', + const streamCompletion = await client.chat.completions.create({ + model: useWandAzure ? wandModelName : 'gpt-4o', messages: messages, temperature: 0.3, max_tokens: 10000, @@ -141,8 +160,8 @@ export async function POST(req: NextRequest) { } // For non-streaming responses - const completion = await openai?.chat.completions.create({ - model: 'gpt-4o', + const completion = await client.chat.completions.create({ + model: useWandAzure ? wandModelName : 'gpt-4o', messages: messages, temperature: 0.3, max_tokens: 10000, @@ -151,9 +170,11 @@ export async function POST(req: NextRequest) { const generatedContent = completion.choices[0]?.message?.content?.trim() if (!generatedContent) { - logger.error(`[${requestId}] OpenAI response was empty or invalid.`) + logger.error( + `[${requestId}] ${useWandAzure ? 'Azure OpenAI' : 'OpenAI'} response was empty or invalid.` + ) return NextResponse.json( - { success: false, error: 'Failed to generate content. OpenAI response was empty.' }, + { success: false, error: 'Failed to generate content. AI response was empty.' }, { status: 500 } ) } @@ -171,7 +192,9 @@ export async function POST(req: NextRequest) { if (error instanceof OpenAI.APIError) { status = error.status || 500 - logger.error(`[${requestId}] OpenAI API Error: ${status} - ${error.message}`) + logger.error( + `[${requestId}] ${useWandAzure ? 'Azure OpenAI' : 'OpenAI'} API Error: ${status} - ${error.message}` + ) if (status === 401) { clientErrorMessage = 'Authentication failed. Please check your API key configuration.' @@ -181,6 +204,10 @@ export async function POST(req: NextRequest) { clientErrorMessage = 'The wand generation service is currently unavailable. Please try again later.' } + } else if (useWandAzure && error.message?.includes('DeploymentNotFound')) { + clientErrorMessage = + 'Azure OpenAI deployment not found. Please check your model deployment configuration.' + status = 404 } return NextResponse.json( diff --git a/apps/sim/lib/documents/document-processor.ts b/apps/sim/lib/documents/document-processor.ts index 66bb3fad20..7727282263 100644 --- a/apps/sim/lib/documents/document-processor.ts +++ b/apps/sim/lib/documents/document-processor.ts @@ -9,10 +9,9 @@ import { mistralParserTool } from '@/tools/mistral/parser' const logger = createLogger('DocumentProcessor') -// Timeout constants (in milliseconds) const TIMEOUTS = { - FILE_DOWNLOAD: 60000, // 60 seconds - MISTRAL_OCR_API: 90000, // 90 seconds + FILE_DOWNLOAD: 60000, + MISTRAL_OCR_API: 90000, } as const type S3Config = { @@ -27,20 +26,19 @@ type BlobConfig = { connectionString?: string } -function getKBConfig(): S3Config | BlobConfig { +const getKBConfig = (): S3Config | BlobConfig => { const provider = getStorageProvider() - if (provider === 'blob') { - return { - containerName: BLOB_KB_CONFIG.containerName, - accountName: BLOB_KB_CONFIG.accountName, - accountKey: BLOB_KB_CONFIG.accountKey, - connectionString: BLOB_KB_CONFIG.connectionString, - } - } - return { - bucket: S3_KB_CONFIG.bucket, - region: S3_KB_CONFIG.region, - } + return provider === 'blob' + ? { + containerName: BLOB_KB_CONFIG.containerName, + accountName: BLOB_KB_CONFIG.accountName, + accountKey: BLOB_KB_CONFIG.accountKey, + connectionString: BLOB_KB_CONFIG.connectionString, + } + : { + bucket: S3_KB_CONFIG.bucket, + region: S3_KB_CONFIG.region, + } } class APIError extends Error { @@ -53,9 +51,6 @@ class APIError extends Error { } } -/** - * Process a document by parsing it and chunking the content - */ export async function processDocument( fileUrl: string, filename: string, @@ -79,29 +74,23 @@ export async function processDocument( logger.info(`Processing document: ${filename}`) try { - // Parse the document - const { content, processingMethod, cloudUrl } = await parseDocument(fileUrl, filename, mimeType) - - // Create chunker and process content - const chunker = new TextChunker({ - chunkSize, - overlap: chunkOverlap, - minChunkSize, - }) + const parseResult = await parseDocument(fileUrl, filename, mimeType) + const { content, processingMethod } = parseResult + const cloudUrl = 'cloudUrl' in parseResult ? parseResult.cloudUrl : undefined + const chunker = new TextChunker({ chunkSize, overlap: chunkOverlap, minChunkSize }) const chunks = await chunker.chunk(content) - // Calculate metadata const characterCount = content.length - const tokenCount = chunks.reduce((sum: number, chunk: Chunk) => sum + chunk.tokenCount, 0) + const tokenCount = chunks.reduce((sum, chunk) => sum + chunk.tokenCount, 0) - logger.info(`Document processed successfully: ${chunks.length} chunks, ${tokenCount} tokens`) + logger.info(`Document processed: ${chunks.length} chunks, ${tokenCount} tokens`) return { chunks, metadata: { filename, - fileSize: content.length, // Using content length as file size approximation + fileSize: characterCount, mimeType, chunkCount: chunks.length, tokenCount, @@ -116,9 +105,6 @@ export async function processDocument( } } -/** - * Parse a document from a URL or file path - */ async function parseDocument( fileUrl: string, filename: string, @@ -128,283 +114,286 @@ async function parseDocument( processingMethod: 'file-parser' | 'mistral-ocr' cloudUrl?: string }> { - // Check if we should use Mistral OCR for PDFs - const shouldUseMistralOCR = mimeType === 'application/pdf' && env.MISTRAL_API_KEY + const isPDF = mimeType === 'application/pdf' + const hasAzureMistralOCR = + env.AZURE_OPENAI_API_KEY && env.OCR_AZURE_ENDPOINT && env.OCR_AZURE_MODEL_NAME + const hasMistralOCR = env.MISTRAL_API_KEY - if (shouldUseMistralOCR) { - logger.info(`Using Mistral OCR for PDF: ${filename}`) - return await parseWithMistralOCR(fileUrl, filename, mimeType) + // Check Azure Mistral OCR configuration + + if (isPDF && hasAzureMistralOCR) { + logger.info(`Using Azure Mistral OCR: ${filename}`) + return parseWithAzureMistralOCR(fileUrl, filename, mimeType) + } + + if (isPDF && hasMistralOCR) { + logger.info(`Using Mistral OCR: ${filename}`) + return parseWithMistralOCR(fileUrl, filename, mimeType) } - // Use standard file parser - logger.info(`Using file parser for: ${filename}`) - return await parseWithFileParser(fileUrl, filename, mimeType) + logger.info(`Using file parser: ${filename}`) + return parseWithFileParser(fileUrl, filename, mimeType) } -/** - * Parse document using Mistral OCR - */ -async function parseWithMistralOCR( - fileUrl: string, - filename: string, - mimeType: string -): Promise<{ - content: string - processingMethod: 'file-parser' | 'mistral-ocr' - cloudUrl?: string -}> { - const mistralApiKey = env.MISTRAL_API_KEY - if (!mistralApiKey) { - throw new Error('Mistral API key is required for OCR processing') +async function handleFileForOCR(fileUrl: string, filename: string, mimeType: string) { + if (fileUrl.startsWith('https://')) { + return { httpsUrl: fileUrl } } - let httpsUrl = fileUrl - let cloudUrl: string | undefined + logger.info(`Uploading "${filename}" to cloud storage for OCR`) - // If the URL is not HTTPS, we need to upload to cloud storage first - if (!fileUrl.startsWith('https://')) { - logger.info(`Uploading "${filename}" to cloud storage for Mistral OCR access`) + const buffer = await downloadFileWithTimeout(fileUrl) + const kbConfig = getKBConfig() - // Download the file content with timeout - const controller = new AbortController() - const timeoutId = setTimeout(() => controller.abort(), TIMEOUTS.FILE_DOWNLOAD) + validateCloudConfig(kbConfig) - try { - const response = await fetch(fileUrl, { signal: controller.signal }) - clearTimeout(timeoutId) + try { + const cloudResult = await uploadFile(buffer, filename, mimeType, kbConfig as any) + const httpsUrl = await getPresignedUrlWithConfig(cloudResult.key, kbConfig as any, 900) + logger.info(`Successfully uploaded for OCR: ${cloudResult.key}`) + return { httpsUrl, cloudUrl: httpsUrl } + } catch (uploadError) { + const message = uploadError instanceof Error ? uploadError.message : 'Unknown error' + throw new Error(`Cloud upload failed: ${message}. Cloud upload is required for OCR.`) + } +} - if (!response.ok) { - throw new Error(`Failed to download file for cloud upload: ${response.statusText}`) - } +async function downloadFileWithTimeout(fileUrl: string): Promise { + const controller = new AbortController() + const timeoutId = setTimeout(() => controller.abort(), TIMEOUTS.FILE_DOWNLOAD) - const buffer = Buffer.from(await response.arrayBuffer()) - - // Always upload to cloud storage for Mistral OCR, even in development - const kbConfig = getKBConfig() - const provider = getStorageProvider() - - if (provider === 'blob') { - const blobConfig = kbConfig as BlobConfig - if ( - !blobConfig.containerName || - (!blobConfig.connectionString && (!blobConfig.accountName || !blobConfig.accountKey)) - ) { - throw new Error( - 'Azure Blob configuration missing for PDF processing with Mistral OCR. Set AZURE_CONNECTION_STRING or both AZURE_ACCOUNT_NAME + AZURE_ACCOUNT_KEY, and AZURE_KB_CONTAINER_NAME.' - ) - } - } else { - const s3Config = kbConfig as S3Config - if (!s3Config.bucket || !s3Config.region) { - throw new Error( - 'S3 configuration missing for PDF processing with Mistral OCR. Set AWS_REGION and S3_KB_BUCKET_NAME environment variables.' - ) - } - } + try { + const response = await fetch(fileUrl, { signal: controller.signal }) + clearTimeout(timeoutId) - try { - // Upload to cloud storage - const cloudResult = await uploadFile(buffer, filename, mimeType, kbConfig as any) - // Generate presigned URL with 15 minutes expiration - httpsUrl = await getPresignedUrlWithConfig(cloudResult.key, kbConfig as any, 900) - cloudUrl = httpsUrl - logger.info(`Successfully uploaded to cloud storage for Mistral OCR: ${cloudResult.key}`) - } catch (uploadError) { - logger.error('Failed to upload to cloud storage for Mistral OCR:', uploadError) - throw new Error( - `Cloud upload failed: ${uploadError instanceof Error ? uploadError.message : 'Unknown error'}. Cloud upload is required for PDF processing with Mistral OCR.` - ) - } - } catch (error) { - clearTimeout(timeoutId) - if (error instanceof Error && error.name === 'AbortError') { - throw new Error('File download timed out for Mistral OCR processing') - } - throw error + if (!response.ok) { + throw new Error(`Failed to download file: ${response.statusText}`) + } + + return Buffer.from(await response.arrayBuffer()) + } catch (error) { + clearTimeout(timeoutId) + if (error instanceof Error && error.name === 'AbortError') { + throw new Error('File download timed out') + } + throw error + } +} + +async function downloadFileForBase64(fileUrl: string): Promise { + // Handle different URL types for Azure Mistral OCR base64 requirement + if (fileUrl.startsWith('data:')) { + // Extract base64 data from data URI + const [, base64Data] = fileUrl.split(',') + if (!base64Data) { + throw new Error('Invalid data URI format') + } + return Buffer.from(base64Data, 'base64') + } + if (fileUrl.startsWith('http')) { + // Download from HTTP(S) URL + return downloadFileWithTimeout(fileUrl) + } + // Local file - read it + const fs = await import('fs/promises') + return fs.readFile(fileUrl) +} + +function validateCloudConfig(kbConfig: S3Config | BlobConfig) { + const provider = getStorageProvider() + + if (provider === 'blob') { + const config = kbConfig as BlobConfig + if ( + !config.containerName || + (!config.connectionString && (!config.accountName || !config.accountKey)) + ) { + throw new Error( + 'Azure Blob configuration missing. Set AZURE_CONNECTION_STRING or AZURE_ACCOUNT_NAME + AZURE_ACCOUNT_KEY + AZURE_KB_CONTAINER_NAME' + ) + } + } else { + const config = kbConfig as S3Config + if (!config.bucket || !config.region) { + throw new Error('S3 configuration missing. Set AWS_REGION and S3_KB_BUCKET_NAME') + } + } +} + +function processOCRContent(result: any, filename: string): string { + if (!result.success) { + throw new Error(`OCR processing failed: ${result.error || 'Unknown error'}`) + } + + const content = result.output?.content || '' + if (!content.trim()) { + throw new Error('OCR returned empty content') + } + + logger.info(`OCR completed: ${filename}`) + return content +} + +function validateOCRConfig( + apiKey?: string, + endpoint?: string, + modelName?: string, + service = 'OCR' +) { + if (!apiKey) throw new Error(`${service} API key required`) + if (!endpoint) throw new Error(`${service} endpoint required`) + if (!modelName) throw new Error(`${service} model name required`) +} + +function extractPageContent(pages: any[]): string { + if (!pages?.length) return '' + + return pages + .map((page) => page?.markdown || '') + .filter(Boolean) + .join('\n\n') +} + +async function makeOCRRequest(endpoint: string, headers: Record, body: any) { + const controller = new AbortController() + const timeoutId = setTimeout(() => controller.abort(), TIMEOUTS.MISTRAL_OCR_API) + + try { + const response = await fetch(endpoint, { + method: 'POST', + headers, + body: JSON.stringify(body), + signal: controller.signal, + }) + + clearTimeout(timeoutId) + + if (!response.ok) { + const errorText = await response.text() + throw new APIError( + `OCR failed: ${response.status} ${response.statusText} - ${errorText}`, + response.status + ) + } + + return response + } catch (error) { + clearTimeout(timeoutId) + if (error instanceof Error && error.name === 'AbortError') { + throw new Error('OCR API request timed out') } + throw error + } +} + +async function parseWithAzureMistralOCR(fileUrl: string, filename: string, mimeType: string) { + validateOCRConfig( + env.AZURE_OPENAI_API_KEY, + env.OCR_AZURE_ENDPOINT, + env.OCR_AZURE_MODEL_NAME, + 'Azure Mistral OCR' + ) + + // Azure Mistral OCR accepts data URIs with base64 content + const fileBuffer = await downloadFileForBase64(fileUrl) + const base64Data = fileBuffer.toString('base64') + const dataUri = `data:${mimeType};base64,${base64Data}` + + try { + const response = await retryWithExponentialBackoff( + () => + makeOCRRequest( + env.OCR_AZURE_ENDPOINT!, + { + 'Content-Type': 'application/json', + Authorization: `Bearer ${env.AZURE_OPENAI_API_KEY}`, + }, + { + model: env.OCR_AZURE_MODEL_NAME, + document: { + type: 'document_url', + document_url: dataUri, + }, + include_image_base64: false, + } + ), + { maxRetries: 3, initialDelayMs: 1000, maxDelayMs: 10000 } + ) + + const ocrResult = await response.json() + const content = extractPageContent(ocrResult.pages) || JSON.stringify(ocrResult, null, 2) + + if (!content.trim()) { + throw new Error('Azure Mistral OCR returned empty content') + } + + logger.info(`Azure Mistral OCR completed: ${filename}`) + return { content, processingMethod: 'mistral-ocr' as const, cloudUrl: undefined } + } catch (error) { + logger.error(`Azure Mistral OCR failed for ${filename}:`, { + message: error instanceof Error ? error.message : String(error), + }) + + return env.MISTRAL_API_KEY + ? parseWithMistralOCR(fileUrl, filename, mimeType) + : parseWithFileParser(fileUrl, filename, mimeType) + } +} + +async function parseWithMistralOCR(fileUrl: string, filename: string, mimeType: string) { + if (!env.MISTRAL_API_KEY) { + throw new Error('Mistral API key required') } if (!mistralParserTool.request?.body) { - throw new Error('Mistral parser tool not properly configured') + throw new Error('Mistral parser tool not configured') } - const requestBody = mistralParserTool.request.body({ - filePath: httpsUrl, - apiKey: mistralApiKey, - resultType: 'text', - }) + const { httpsUrl, cloudUrl } = await handleFileForOCR(fileUrl, filename, mimeType) + const params = { filePath: httpsUrl, apiKey: env.MISTRAL_API_KEY, resultType: 'text' as const } try { const response = await retryWithExponentialBackoff( async () => { const url = typeof mistralParserTool.request!.url === 'function' - ? mistralParserTool.request!.url({ - filePath: httpsUrl, - apiKey: mistralApiKey, - resultType: 'text', - }) + ? mistralParserTool.request!.url(params) : mistralParserTool.request!.url const headers = typeof mistralParserTool.request!.headers === 'function' - ? mistralParserTool.request!.headers({ - filePath: httpsUrl, - apiKey: mistralApiKey, - resultType: 'text', - }) + ? mistralParserTool.request!.headers(params) : mistralParserTool.request!.headers - const controller = new AbortController() - const timeoutId = setTimeout(() => controller.abort(), TIMEOUTS.MISTRAL_OCR_API) - - try { - const method = - typeof mistralParserTool.request!.method === 'function' - ? mistralParserTool.request!.method(requestBody as any) - : mistralParserTool.request!.method - - const res = await fetch(url, { - method, - headers, - body: JSON.stringify(requestBody), - signal: controller.signal, - }) - - clearTimeout(timeoutId) - - if (!res.ok) { - const errorText = await res.text() - throw new APIError( - `Mistral OCR failed: ${res.status} ${res.statusText} - ${errorText}`, - res.status - ) - } - - return res - } catch (error) { - clearTimeout(timeoutId) - if (error instanceof Error && error.name === 'AbortError') { - throw new Error('Mistral OCR API request timed out') - } - throw error - } + const requestBody = mistralParserTool.request!.body!(params) + return makeOCRRequest(url, headers as Record, requestBody) }, - { - maxRetries: 3, - initialDelayMs: 1000, - maxDelayMs: 10000, - } + { maxRetries: 3, initialDelayMs: 1000, maxDelayMs: 10000 } ) - const result = await mistralParserTool.transformResponse!(response, { - filePath: httpsUrl, - apiKey: mistralApiKey, - resultType: 'text', - }) - - if (!result.success) { - throw new Error(`Mistral OCR processing failed: ${result.error || 'Unknown error'}`) - } - - const content = result.output?.content || '' - if (!content.trim()) { - throw new Error('Mistral OCR returned empty content') - } + const result = await mistralParserTool.transformResponse!(response, params) + const content = processOCRContent(result, filename) - logger.info(`Mistral OCR completed successfully for ${filename}`) - return { - content, - processingMethod: 'mistral-ocr', - cloudUrl, - } + return { content, processingMethod: 'mistral-ocr' as const, cloudUrl } } catch (error) { logger.error(`Mistral OCR failed for ${filename}:`, { message: error instanceof Error ? error.message : String(error), - stack: error instanceof Error ? error.stack : undefined, - name: error instanceof Error ? error.name : 'Unknown', }) - // Fall back to file parser - logger.info(`Falling back to file parser for ${filename}`) - return await parseWithFileParser(fileUrl, filename, mimeType) + logger.info(`Falling back to file parser: ${filename}`) + return parseWithFileParser(fileUrl, filename, mimeType) } } -/** - * Parse document using standard file parser - */ -async function parseWithFileParser( - fileUrl: string, - filename: string, - mimeType: string -): Promise<{ - content: string - processingMethod: 'file-parser' | 'mistral-ocr' - cloudUrl?: string -}> { +async function parseWithFileParser(fileUrl: string, filename: string, mimeType: string) { try { let content: string if (fileUrl.startsWith('data:')) { - logger.info(`Processing data URI for: ${filename}`) - - try { - const [header, base64Data] = fileUrl.split(',') - if (!base64Data) { - throw new Error('Invalid data URI format') - } - - if (header.includes('base64')) { - const buffer = Buffer.from(base64Data, 'base64') - content = buffer.toString('utf8') - } else { - content = decodeURIComponent(base64Data) - } - - if (mimeType === 'text/plain') { - logger.info(`Data URI processed successfully for text content: ${filename}`) - } else { - const extension = filename.split('.').pop()?.toLowerCase() || 'txt' - const buffer = Buffer.from(base64Data, 'base64') - const result = await parseBuffer(buffer, extension) - content = result.content - } - } catch (error) { - throw new Error( - `Failed to process data URI: ${error instanceof Error ? error.message : 'Unknown error'}` - ) - } - } else if (fileUrl.startsWith('http://') || fileUrl.startsWith('https://')) { - const controller = new AbortController() - const timeoutId = setTimeout(() => controller.abort(), TIMEOUTS.FILE_DOWNLOAD) - - try { - const response = await fetch(fileUrl, { signal: controller.signal }) - clearTimeout(timeoutId) - - if (!response.ok) { - throw new Error(`Failed to download file: ${response.status} ${response.statusText}`) - } - - const buffer = Buffer.from(await response.arrayBuffer()) - - const extension = filename.split('.').pop()?.toLowerCase() || '' - if (!extension) { - throw new Error(`Could not determine file extension from filename: ${filename}`) - } - - const result = await parseBuffer(buffer, extension) - content = result.content - } catch (error) { - clearTimeout(timeoutId) - if (error instanceof Error && error.name === 'AbortError') { - throw new Error('File download timed out') - } - throw error - } + content = await parseDataURI(fileUrl, filename, mimeType) + } else if (fileUrl.startsWith('http')) { + content = await parseHttpFile(fileUrl, filename) } else { - // Parse local file const result = await parseFile(fileUrl) content = result.content } @@ -413,12 +402,39 @@ async function parseWithFileParser( throw new Error('File parser returned empty content') } - return { - content, - processingMethod: 'file-parser', - } + return { content, processingMethod: 'file-parser' as const, cloudUrl: undefined } } catch (error) { logger.error(`File parser failed for ${filename}:`, error) throw error } } + +async function parseDataURI(fileUrl: string, filename: string, mimeType: string): Promise { + const [header, base64Data] = fileUrl.split(',') + if (!base64Data) { + throw new Error('Invalid data URI format') + } + + if (mimeType === 'text/plain') { + return header.includes('base64') + ? Buffer.from(base64Data, 'base64').toString('utf8') + : decodeURIComponent(base64Data) + } + + const extension = filename.split('.').pop()?.toLowerCase() || 'txt' + const buffer = Buffer.from(base64Data, 'base64') + const result = await parseBuffer(buffer, extension) + return result.content +} + +async function parseHttpFile(fileUrl: string, filename: string): Promise { + const buffer = await downloadFileWithTimeout(fileUrl) + + const extension = filename.split('.').pop()?.toLowerCase() + if (!extension) { + throw new Error(`Could not determine file extension: ${filename}`) + } + + const result = await parseBuffer(buffer, extension) + return result.content +} diff --git a/apps/sim/lib/embeddings/utils.ts b/apps/sim/lib/embeddings/utils.ts new file mode 100644 index 0000000000..a4250678c6 --- /dev/null +++ b/apps/sim/lib/embeddings/utils.ts @@ -0,0 +1,148 @@ +import { isRetryableError, retryWithExponentialBackoff } from '@/lib/documents/utils' +import { env } from '@/lib/env' +import { createLogger } from '@/lib/logs/console/logger' + +const logger = createLogger('EmbeddingUtils') + +export class EmbeddingAPIError extends Error { + public status: number + + constructor(message: string, status: number) { + super(message) + this.name = 'EmbeddingAPIError' + this.status = status + } +} + +interface EmbeddingConfig { + useAzure: boolean + apiUrl: string + headers: Record + modelName: string +} + +function getEmbeddingConfig(embeddingModel = 'text-embedding-3-small'): EmbeddingConfig { + const azureApiKey = env.AZURE_OPENAI_API_KEY + const azureEndpoint = env.AZURE_OPENAI_ENDPOINT + const azureApiVersion = env.AZURE_OPENAI_API_VERSION + const kbModelName = env.KB_OPENAI_MODEL_NAME || embeddingModel + const openaiApiKey = env.OPENAI_API_KEY + + const useAzure = !!(azureApiKey && azureEndpoint) + + if (!useAzure && !openaiApiKey) { + throw new Error( + 'Either OPENAI_API_KEY or Azure OpenAI configuration (AZURE_OPENAI_API_KEY + AZURE_OPENAI_ENDPOINT) must be configured' + ) + } + + const apiUrl = useAzure + ? `${azureEndpoint}/openai/deployments/${kbModelName}/embeddings?api-version=${azureApiVersion}` + : 'https://api.openai.com/v1/embeddings' + + const headers: Record = useAzure + ? { + 'api-key': azureApiKey!, + 'Content-Type': 'application/json', + } + : { + Authorization: `Bearer ${openaiApiKey!}`, + 'Content-Type': 'application/json', + } + + return { + useAzure, + apiUrl, + headers, + modelName: useAzure ? kbModelName : embeddingModel, + } +} + +async function callEmbeddingAPI(inputs: string[], config: EmbeddingConfig): Promise { + return retryWithExponentialBackoff( + async () => { + const requestBody = config.useAzure + ? { + input: inputs, + encoding_format: 'float', + } + : { + input: inputs, + model: config.modelName, + encoding_format: 'float', + } + + const response = await fetch(config.apiUrl, { + method: 'POST', + headers: config.headers, + body: JSON.stringify(requestBody), + }) + + if (!response.ok) { + const errorText = await response.text() + throw new EmbeddingAPIError( + `Embedding API failed: ${response.status} ${response.statusText} - ${errorText}`, + response.status + ) + } + + const data = await response.json() + return data.data.map((item: any) => item.embedding) + }, + { + maxRetries: 3, + initialDelayMs: 1000, + maxDelayMs: 10000, + retryCondition: (error: any) => { + if (error instanceof EmbeddingAPIError) { + return error.status === 429 || error.status >= 500 + } + return isRetryableError(error) + }, + } + ) +} + +/** + * Generate embeddings for multiple texts with batching + */ +export async function generateEmbeddings( + texts: string[], + embeddingModel = 'text-embedding-3-small' +): Promise { + const config = getEmbeddingConfig(embeddingModel) + + logger.info(`Using ${config.useAzure ? 'Azure OpenAI' : 'OpenAI'} for embeddings generation`) + + const batchSize = 100 + const allEmbeddings: number[][] = [] + + for (let i = 0; i < texts.length; i += batchSize) { + const batch = texts.slice(i, i + batchSize) + const batchEmbeddings = await callEmbeddingAPI(batch, config) + allEmbeddings.push(...batchEmbeddings) + + logger.info( + `Generated embeddings for batch ${Math.floor(i / batchSize) + 1}/${Math.ceil(texts.length / batchSize)}` + ) + } + + return allEmbeddings +} + +/** + * Generate embedding for a single search query + */ +export async function generateSearchEmbedding( + query: string, + embeddingModel = 'text-embedding-3-small' +): Promise { + const config = getEmbeddingConfig(embeddingModel) + + logger.info( + `Using ${config.useAzure ? 'Azure OpenAI' : 'OpenAI'} for search embedding generation` + ) + + const embeddings = await callEmbeddingAPI([query], config) + return embeddings[0] +} diff --git a/apps/sim/lib/env.ts b/apps/sim/lib/env.ts index f14a597007..7cb8a7cb7e 100644 --- a/apps/sim/lib/env.ts +++ b/apps/sim/lib/env.ts @@ -66,9 +66,14 @@ export const env = createEnv({ ELEVENLABS_API_KEY: z.string().min(1).optional(), // ElevenLabs API key for text-to-speech in deployed chat SERPER_API_KEY: z.string().min(1).optional(), // Serper API key for online search - // Azure OpenAI Configuration - AZURE_OPENAI_ENDPOINT: z.string().url().optional(), // Azure OpenAI service endpoint - AZURE_OPENAI_API_VERSION: z.string().optional(), // Azure OpenAI API version + // Azure Configuration - Shared credentials with feature-specific models + AZURE_OPENAI_ENDPOINT: z.string().url().optional(), // Shared Azure OpenAI service endpoint + AZURE_OPENAI_API_VERSION: z.string().optional(), // Shared Azure OpenAI API version + AZURE_OPENAI_API_KEY: z.string().min(1).optional(), // Shared Azure OpenAI API key + KB_OPENAI_MODEL_NAME: z.string().optional(), // Knowledge base OpenAI model name (works with both regular OpenAI and Azure OpenAI) + WAND_OPENAI_MODEL_NAME: z.string().optional(), // Wand generation OpenAI model name (works with both regular OpenAI and Azure OpenAI) + OCR_AZURE_ENDPOINT: z.string().url().optional(), // Azure Mistral OCR service endpoint + OCR_AZURE_MODEL_NAME: z.string().optional(), // Azure Mistral OCR model name for document processing // Monitoring & Analytics TELEMETRY_ENDPOINT: z.string().url().optional(), // Custom telemetry/analytics endpoint From 26e6286fda2b8f709808305e1d464755c1bcf82c Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 20 Aug 2025 17:05:35 -0700 Subject: [PATCH 06/22] fix(billing): fix team plan upgrade (#1053) --- apps/sim/lib/auth.ts | 159 ++++----------- apps/sim/lib/billing/core/billing.ts | 68 ++++++- .../lib/billing/core/organization-billing.ts | 28 ++- apps/sim/lib/billing/team-management.ts | 181 ++++++++++++++++++ .../lib/billing/validation/seat-management.ts | 24 ++- 5 files changed, 312 insertions(+), 148 deletions(-) create mode 100644 apps/sim/lib/billing/team-management.ts diff --git a/apps/sim/lib/auth.ts b/apps/sim/lib/auth.ts index a2694372b3..9e46a74b7b 100644 --- a/apps/sim/lib/auth.ts +++ b/apps/sim/lib/auth.ts @@ -1270,133 +1270,30 @@ export const auth = betterAuth({ }) // Auto-create organization for team plan purchases - if (subscription.plan === 'team') { - try { - // Get the user who purchased the subscription - const user = await db - .select() - .from(schema.user) - .where(eq(schema.user.id, subscription.referenceId)) - .limit(1) - - if (user.length > 0) { - const currentUser = user[0] - - // Store the original user ID before we change the referenceId - const originalUserId = subscription.referenceId - - // First, sync usage limits for the purchasing user with their new plan - try { - const { syncUsageLimitsFromSubscription } = await import('@/lib/billing') - await syncUsageLimitsFromSubscription(originalUserId) - logger.info( - 'Usage limits synced for purchasing user before organization creation', - { - userId: originalUserId, - } - ) - } catch (error) { - logger.error('Failed to sync usage limits for purchasing user', { - userId: originalUserId, - error, - }) - } - - // Create organization for the team - const orgId = `org_${Date.now()}_${Math.random().toString(36).substring(2, 15)}` - const orgSlug = `${currentUser.name?.toLowerCase().replace(/\s+/g, '-') || 'team'}-${Date.now()}` - - // Create a separate Stripe customer for the organization - let orgStripeCustomerId: string | null = null - if (stripeClient) { - try { - const orgStripeCustomer = await stripeClient.customers.create({ - name: `${currentUser.name || 'User'}'s Team`, - email: currentUser.email, - metadata: { - organizationId: orgId, - type: 'organization', - }, - }) - orgStripeCustomerId = orgStripeCustomer.id - } catch (error) { - logger.error('Failed to create Stripe customer for organization', { - organizationId: orgId, - error, - }) - // Continue without Stripe customer - can be created later - } - } - - const newOrg = await db - .insert(schema.organization) - .values({ - id: orgId, - name: `${currentUser.name || 'User'}'s Team`, - slug: orgSlug, - metadata: orgStripeCustomerId - ? { stripeCustomerId: orgStripeCustomerId } - : null, - createdAt: new Date(), - updatedAt: new Date(), - }) - .returning() - - // Add the user as owner of the organization - await db.insert(schema.member).values({ - id: `member_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`, - userId: currentUser.id, - organizationId: orgId, - role: 'owner', - createdAt: new Date(), - }) - - // Update the subscription to reference the organization instead of the user - await db - .update(schema.subscription) - .set({ referenceId: orgId }) - .where(eq(schema.subscription.id, subscription.id)) - - // Update the session to set the new organization as active - await db - .update(schema.session) - .set({ activeOrganizationId: orgId }) - .where(eq(schema.session.userId, currentUser.id)) - - logger.info('Auto-created organization for team subscription', { - organizationId: orgId, - userId: currentUser.id, - subscriptionId: subscription.id, - orgName: `${currentUser.name || 'User'}'s Team`, - }) - - // Update referenceId for usage limit sync - subscription.referenceId = orgId - } - } catch (error) { - logger.error('Failed to auto-create organization for team subscription', { - subscriptionId: subscription.id, - referenceId: subscription.referenceId, - error, - }) - } + try { + const { handleTeamPlanOrganization } = await import( + '@/lib/billing/team-management' + ) + await handleTeamPlanOrganization(subscription) + } catch (error) { + logger.error('Failed to handle team plan organization creation', { + subscriptionId: subscription.id, + referenceId: subscription.referenceId, + error, + }) } - // Initialize billing period for the user/organization + // Initialize billing period and sync usage limits try { const { initializeBillingPeriod } = await import( '@/lib/billing/core/billing-periods' ) + const { syncSubscriptionUsageLimits } = await import( + '@/lib/billing/team-management' + ) - // Note: Usage limits are already synced above for team plan users - // For non-team plans, sync usage limits using the referenceId (which is the user ID) - if (subscription.plan !== 'team') { - const { syncUsageLimitsFromSubscription } = await import('@/lib/billing') - await syncUsageLimitsFromSubscription(subscription.referenceId) - logger.info('Usage limits synced after subscription creation', { - referenceId: subscription.referenceId, - }) - } + // Sync usage limits for user or organization members + await syncSubscriptionUsageLimits(subscription) // Initialize billing period for new subscription using Stripe dates if (subscription.plan !== 'free') { @@ -1433,15 +1330,29 @@ export const auth = betterAuth({ logger.info('Subscription updated', { subscriptionId: subscription.id, status: subscription.status, + plan: subscription.plan, }) - // Sync usage limits for the user/organization + // Auto-create organization for team plan upgrades (free -> team) try { - const { syncUsageLimitsFromSubscription } = await import('@/lib/billing') - await syncUsageLimitsFromSubscription(subscription.referenceId) - logger.info('Usage limits synced after subscription update', { + const { handleTeamPlanOrganization } = await import( + '@/lib/billing/team-management' + ) + await handleTeamPlanOrganization(subscription) + } catch (error) { + logger.error('Failed to handle team plan organization creation on update', { + subscriptionId: subscription.id, referenceId: subscription.referenceId, + error, }) + } + + // Sync usage limits for the user/organization + try { + const { syncSubscriptionUsageLimits } = await import( + '@/lib/billing/team-management' + ) + await syncSubscriptionUsageLimits(subscription) } catch (error) { logger.error('Failed to sync usage limits after subscription update', { referenceId: subscription.referenceId, diff --git a/apps/sim/lib/billing/core/billing.ts b/apps/sim/lib/billing/core/billing.ts index 728575658b..32c74e4d02 100644 --- a/apps/sim/lib/billing/core/billing.ts +++ b/apps/sim/lib/billing/core/billing.ts @@ -13,6 +13,24 @@ import { member, organization, subscription, user, userStats } from '@/db/schema const logger = createLogger('Billing') +/** + * Get organization subscription directly by organization ID + */ +export async function getOrganizationSubscription(organizationId: string) { + try { + const orgSubs = await db + .select() + .from(subscription) + .where(and(eq(subscription.referenceId, organizationId), eq(subscription.status, 'active'))) + .limit(1) + + return orgSubs.length > 0 ? orgSubs[0] : null + } catch (error) { + logger.error('Error getting organization subscription', { error, organizationId }) + return null + } +} + interface BillingResult { success: boolean chargedAmount?: number @@ -89,15 +107,43 @@ async function getStripeCustomerId(referenceId: string): Promise .where(eq(organization.id, referenceId)) .limit(1) - if (orgRecord.length > 0 && orgRecord[0].metadata) { - const metadata = - typeof orgRecord[0].metadata === 'string' - ? JSON.parse(orgRecord[0].metadata) - : orgRecord[0].metadata + if (orgRecord.length > 0) { + // First, check if organization has its own Stripe customer (legacy support) + if (orgRecord[0].metadata) { + const metadata = + typeof orgRecord[0].metadata === 'string' + ? JSON.parse(orgRecord[0].metadata) + : orgRecord[0].metadata + + if (metadata?.stripeCustomerId) { + return metadata.stripeCustomerId + } + } - if (metadata?.stripeCustomerId) { - return metadata.stripeCustomerId + // If organization has no Stripe customer, use the owner's customer + // This is our new pattern: subscriptions stay with user, referenceId = orgId + const ownerRecord = await db + .select({ + stripeCustomerId: user.stripeCustomerId, + userId: user.id, + }) + .from(user) + .innerJoin(member, eq(member.userId, user.id)) + .where(and(eq(member.organizationId, referenceId), eq(member.role, 'owner'))) + .limit(1) + + if (ownerRecord.length > 0 && ownerRecord[0].stripeCustomerId) { + logger.debug('Using organization owner Stripe customer for billing', { + organizationId: referenceId, + ownerId: ownerRecord[0].userId, + stripeCustomerId: ownerRecord[0].stripeCustomerId, + }) + return ownerRecord[0].stripeCustomerId } + + logger.warn('No Stripe customer found for organization or its owner', { + organizationId: referenceId, + }) } return null @@ -431,8 +477,8 @@ export async function processOrganizationOverageBilling( organizationId: string ): Promise { try { - // Get organization subscription - const subscription = await getHighestPrioritySubscription(organizationId) + // Get organization subscription directly (referenceId = organizationId) + const subscription = await getOrganizationSubscription(organizationId) if (!subscription || !['team', 'enterprise'].includes(subscription.plan)) { logger.warn('No team/enterprise subscription found for organization', { organizationId }) @@ -759,7 +805,9 @@ export async function getSimplifiedBillingSummary( try { // Get subscription and usage data upfront const [subscription, usageData] = await Promise.all([ - getHighestPrioritySubscription(organizationId || userId), + organizationId + ? getOrganizationSubscription(organizationId) + : getHighestPrioritySubscription(userId), getUserUsageData(userId), ]) diff --git a/apps/sim/lib/billing/core/organization-billing.ts b/apps/sim/lib/billing/core/organization-billing.ts index d47c4d9559..dc2c758ba1 100644 --- a/apps/sim/lib/billing/core/organization-billing.ts +++ b/apps/sim/lib/billing/core/organization-billing.ts @@ -1,13 +1,31 @@ import { and, eq } from 'drizzle-orm' import { DEFAULT_FREE_CREDITS } from '@/lib/billing/constants' import { getPlanPricing } from '@/lib/billing/core/billing' -import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription' import { createLogger } from '@/lib/logs/console/logger' import { db } from '@/db' -import { member, organization, user, userStats } from '@/db/schema' +import { member, organization, subscription, user, userStats } from '@/db/schema' const logger = createLogger('OrganizationBilling') +/** + * Get organization subscription directly by organization ID + * This is for our new pattern where referenceId = organizationId + */ +async function getOrganizationSubscription(organizationId: string) { + try { + const orgSubs = await db + .select() + .from(subscription) + .where(and(eq(subscription.referenceId, organizationId), eq(subscription.status, 'active'))) + .limit(1) + + return orgSubs.length > 0 ? orgSubs[0] : null + } catch (error) { + logger.error('Error getting organization subscription', { error, organizationId }) + return null + } +} + interface OrganizationUsageData { organizationId: string organizationName: string @@ -57,8 +75,8 @@ export async function getOrganizationBillingData( const organizationData = orgRecord[0] - // Get organization subscription - const subscription = await getHighestPrioritySubscription(organizationId) + // Get organization subscription directly (referenceId = organizationId) + const subscription = await getOrganizationSubscription(organizationId) if (!subscription) { logger.warn('No subscription found for organization', { organizationId }) @@ -191,7 +209,7 @@ export async function updateMemberUsageLimit( } // Get organization subscription to validate limit - const subscription = await getHighestPrioritySubscription(organizationId) + const subscription = await getOrganizationSubscription(organizationId) if (!subscription) { throw new Error('No active subscription found') } diff --git a/apps/sim/lib/billing/team-management.ts b/apps/sim/lib/billing/team-management.ts new file mode 100644 index 0000000000..40e70b3b11 --- /dev/null +++ b/apps/sim/lib/billing/team-management.ts @@ -0,0 +1,181 @@ +import { eq } from 'drizzle-orm' +import { createLogger } from '@/lib/logs/console/logger' +import { db } from '@/db' +import { member, organization, session, subscription, user } from '@/db/schema' + +const logger = createLogger('TeamManagement') + +type SubscriptionData = { + id: string + plan: string + referenceId: string + status: string + seats?: number + [key: string]: any +} + +/** + * Auto-create organization for team plan subscriptions + */ +export async function handleTeamPlanOrganization( + subscriptionData: SubscriptionData +): Promise { + if (subscriptionData.plan !== 'team') return + + try { + // For team subscriptions, referenceId should be the user ID initially + // But if the organization has already been created, it might be the org ID + let userId: string = subscriptionData.referenceId + let currentUser: any = null + + // First try to get user directly (most common case) + const users = await db + .select() + .from(user) + .where(eq(user.id, subscriptionData.referenceId)) + .limit(1) + + if (users.length > 0) { + currentUser = users[0] + userId = currentUser.id + } else { + // If referenceId is not a user ID, it might be an organization ID + // In that case, the organization already exists, so we should skip + const existingOrg = await db + .select() + .from(organization) + .where(eq(organization.id, subscriptionData.referenceId)) + .limit(1) + + if (existingOrg.length > 0) { + logger.info('Organization already exists for team subscription, skipping creation', { + organizationId: subscriptionData.referenceId, + subscriptionId: subscriptionData.id, + }) + return + } + + logger.warn('User not found for team subscription and no existing organization', { + referenceId: subscriptionData.referenceId, + }) + return + } + + // Check if user already has an organization membership + const existingMember = await db.select().from(member).where(eq(member.userId, userId)).limit(1) + + if (existingMember.length > 0) { + logger.info('User already has organization membership, skipping auto-creation', { + userId, + existingOrgId: existingMember[0].organizationId, + }) + return + } + + const orgName = `${currentUser.name || 'User'}'s Team` + const orgSlug = `${currentUser.name?.toLowerCase().replace(/\s+/g, '-') || 'team'}-${Date.now()}` + + // Create organization directly in database + const orgId = `org_${Date.now()}_${Math.random().toString(36).substring(2, 15)}` + + const [createdOrg] = await db + .insert(organization) + .values({ + id: orgId, + name: orgName, + slug: orgSlug, + createdAt: new Date(), + updatedAt: new Date(), + }) + .returning() + + if (!createdOrg) { + throw new Error('Failed to create organization in database') + } + + // Add the user as admin of the organization (owner role for full control) + await db.insert(member).values({ + id: `member_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`, + userId: currentUser.id, + organizationId: orgId, + role: 'owner', // Owner gives full admin privileges + createdAt: new Date(), + }) + + // Update the subscription to reference the organization instead of the user + await db + .update(subscription) + .set({ referenceId: orgId }) + .where(eq(subscription.id, subscriptionData.id)) + + // Update the user's session to set the new organization as active + await db + .update(session) + .set({ activeOrganizationId: orgId }) + .where(eq(session.userId, currentUser.id)) + + logger.info('Auto-created organization for team subscription', { + organizationId: orgId, + userId: currentUser.id, + subscriptionId: subscriptionData.id, + orgName, + userRole: 'owner', + }) + + // Update subscription object for subsequent logic + subscriptionData.referenceId = orgId + } catch (error) { + logger.error('Failed to auto-create organization for team subscription', { + subscriptionId: subscriptionData.id, + referenceId: subscriptionData.referenceId, + error, + }) + throw error + } +} + +/** + * Sync usage limits for user or organization + * Handles the complexity of determining whether to sync for user ID or org members + */ +export async function syncSubscriptionUsageLimits( + subscriptionData: SubscriptionData +): Promise { + try { + const { syncUsageLimitsFromSubscription } = await import('@/lib/billing') + + // For team plans, the referenceId is now an organization ID + // We need to sync limits for the organization members + if (subscriptionData.plan === 'team') { + // Get all members of the organization + const orgMembers = await db + .select({ userId: member.userId }) + .from(member) + .where(eq(member.organizationId, subscriptionData.referenceId)) + + // Sync usage limits for each member + for (const orgMember of orgMembers) { + await syncUsageLimitsFromSubscription(orgMember.userId) + } + + logger.info('Synced usage limits for team organization members', { + organizationId: subscriptionData.referenceId, + memberCount: orgMembers.length, + }) + } else { + // For non-team plans, referenceId is the user ID + await syncUsageLimitsFromSubscription(subscriptionData.referenceId) + logger.info('Synced usage limits for user', { + userId: subscriptionData.referenceId, + plan: subscriptionData.plan, + }) + } + } catch (error) { + logger.error('Failed to sync subscription usage limits', { + subscriptionId: subscriptionData.id, + referenceId: subscriptionData.referenceId, + error, + }) + throw error + } +} diff --git a/apps/sim/lib/billing/validation/seat-management.ts b/apps/sim/lib/billing/validation/seat-management.ts index 988bb59df1..4ff9ac10cf 100644 --- a/apps/sim/lib/billing/validation/seat-management.ts +++ b/apps/sim/lib/billing/validation/seat-management.ts @@ -1,5 +1,5 @@ import { and, count, eq } from 'drizzle-orm' -import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription' +import { getOrganizationSubscription } from '@/lib/billing/core/billing' import { quickValidateEmail } from '@/lib/email/validation' import { createLogger } from '@/lib/logs/console/logger' import { db } from '@/db' @@ -33,8 +33,8 @@ export async function validateSeatAvailability( additionalSeats = 1 ): Promise { try { - // Get organization subscription - const subscription = await getHighestPrioritySubscription(organizationId) + // Get organization subscription directly (referenceId = organizationId) + const subscription = await getOrganizationSubscription(organizationId) if (!subscription) { return { @@ -71,7 +71,10 @@ export async function validateSeatAvailability( // For enterprise plans, check metadata for custom seat allowances if (subscription.plan === 'enterprise' && subscription.metadata) { try { - const metadata = JSON.parse(subscription.metadata) + const metadata = + typeof subscription.metadata === 'string' + ? JSON.parse(subscription.metadata) + : subscription.metadata if (metadata.maxSeats) { maxSeats = metadata.maxSeats } @@ -142,8 +145,8 @@ export async function getOrganizationSeatInfo( return null } - // Get subscription - const subscription = await getHighestPrioritySubscription(organizationId) + // Get organization subscription directly (referenceId = organizationId) + const subscription = await getOrganizationSubscription(organizationId) if (!subscription) { return null @@ -163,7 +166,10 @@ export async function getOrganizationSeatInfo( if (subscription.plan === 'enterprise' && subscription.metadata) { try { - const metadata = JSON.parse(subscription.metadata) + const metadata = + typeof subscription.metadata === 'string' + ? JSON.parse(subscription.metadata) + : subscription.metadata if (metadata.maxSeats) { maxSeats = metadata.maxSeats } @@ -282,8 +288,8 @@ export async function updateOrganizationSeats( updatedBy: string ): Promise<{ success: boolean; error?: string }> { try { - // Get current subscription - const subscriptionRecord = await getHighestPrioritySubscription(organizationId) + // Get current organization subscription directly (referenceId = organizationId) + const subscriptionRecord = await getOrganizationSubscription(organizationId) if (!subscriptionRecord) { return { success: false, error: 'No active subscription found' } From 9ffaf305bda87f6720a607159b35baac0d5ffc4d Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Wed, 20 Aug 2025 18:03:47 -0700 Subject: [PATCH 07/22] feat(input-format): add value field to test input formats (#1059) * feat(input-format): add value field to test input formats * fix lint * fix typing issue * change to dropdown for boolean --- .../components/starter/input-format.tsx | 231 ++++++++---------- .../components/sub-block/sub-block.tsx | 1 + apps/sim/blocks/blocks/starter.ts | 2 + apps/sim/executor/index.ts | 24 +- 4 files changed, 121 insertions(+), 137 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/starter/input-format.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/starter/input-format.tsx index 684381858b..9a0a4e697b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/starter/input-format.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/starter/input-format.tsx @@ -1,4 +1,4 @@ -import { useRef, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { ChevronDown, Plus, Trash } from 'lucide-react' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' @@ -8,10 +8,16 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' -import { formatDisplayText } from '@/components/ui/formatted-text' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' -import { checkTagTrigger, TagDropdown } from '@/components/ui/tag-dropdown' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import { Textarea } from '@/components/ui/textarea' import { cn } from '@/lib/utils' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value' @@ -64,22 +70,26 @@ export function FieldFormat({ config, }: FieldFormatProps) { const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId) - const [tagDropdownStates, setTagDropdownStates] = useState< - Record< - string, - { - visible: boolean - cursorPosition: number - } - > - >({}) const [dragHighlight, setDragHighlight] = useState>({}) - const valueInputRefs = useRef>({}) + const valueInputRefs = useRef>({}) + const [localValues, setLocalValues] = useState>({}) // Use preview value when in preview mode, otherwise use store value const value = isPreview ? previewValue : storeValue const fields: Field[] = value || [] + useEffect(() => { + const initial: Record = {} + ;(fields || []).forEach((f) => { + if (localValues[f.id] === undefined) { + initial[f.id] = (f.value as string) || '' + } + }) + if (Object.keys(initial).length > 0) { + setLocalValues((prev) => ({ ...prev, ...initial })) + } + }, [fields]) + // Field operations const addField = () => { if (isPreview || disabled) return @@ -88,12 +98,12 @@ export function FieldFormat({ ...DEFAULT_FIELD, id: crypto.randomUUID(), } - setStoreValue([...fields, newField]) + setStoreValue([...(fields || []), newField]) } const removeField = (id: string) => { if (isPreview || disabled) return - setStoreValue(fields.filter((field: Field) => field.id !== id)) + setStoreValue((fields || []).filter((field: Field) => field.id !== id)) } // Validate field name for API safety @@ -103,38 +113,22 @@ export function FieldFormat({ return name.replace(/[\x00-\x1F"\\]/g, '').trim() } - // Tag dropdown handlers const handleValueInputChange = (fieldId: string, newValue: string) => { - const input = valueInputRefs.current[fieldId] - if (!input) return - - const cursorPosition = input.selectionStart || 0 - const shouldShow = checkTagTrigger(newValue, cursorPosition) + setLocalValues((prev) => ({ ...prev, [fieldId]: newValue })) + } - setTagDropdownStates((prev) => ({ - ...prev, - [fieldId]: { - visible: shouldShow.show, - cursorPosition, - }, - })) + // Value normalization: keep it simple for string types - updateField(fieldId, 'value', newValue) - } + const handleValueInputBlur = (field: Field) => { + if (isPreview || disabled) return - const handleTagSelect = (fieldId: string, newValue: string) => { - updateField(fieldId, 'value', newValue) - setTagDropdownStates((prev) => ({ - ...prev, - [fieldId]: { ...prev[fieldId], visible: false }, - })) - } + const inputEl = valueInputRefs.current[field.id] + if (!inputEl) return - const handleTagDropdownClose = (fieldId: string) => { - setTagDropdownStates((prev) => ({ - ...prev, - [fieldId]: { ...prev[fieldId], visible: false }, - })) + const current = localValues[field.id] ?? inputEl.value ?? '' + const trimmed = current.trim() + if (!trimmed) return + updateField(field.id, 'value', current) } // Drag and drop handlers for connection blocks @@ -152,47 +146,8 @@ export function FieldFormat({ const handleDrop = (e: React.DragEvent, fieldId: string) => { e.preventDefault() setDragHighlight((prev) => ({ ...prev, [fieldId]: false })) - - try { - const data = JSON.parse(e.dataTransfer.getData('application/json')) - if (data.type === 'connectionBlock' && data.connectionData) { - const input = valueInputRefs.current[fieldId] - if (!input) return - - // Focus the input first - input.focus() - - // Get current cursor position or use end of field - const dropPosition = input.selectionStart ?? (input.value?.length || 0) - - // Insert '<' at drop position to trigger the dropdown - const currentValue = input.value || '' - const newValue = `${currentValue.slice(0, dropPosition)}<${currentValue.slice(dropPosition)}` - - // Update the field value - updateField(fieldId, 'value', newValue) - - // Set cursor position and show dropdown - setTimeout(() => { - input.selectionStart = dropPosition + 1 - input.selectionEnd = dropPosition + 1 - - // Trigger dropdown by simulating the tag check - const cursorPosition = dropPosition + 1 - const shouldShow = checkTagTrigger(newValue, cursorPosition) - - setTagDropdownStates((prev) => ({ - ...prev, - [fieldId]: { - visible: shouldShow.show, - cursorPosition, - }, - })) - }, 0) - } - } catch (error) { - console.error('Error handling drop:', error) - } + const input = valueInputRefs.current[fieldId] + input?.focus() } // Update handlers @@ -204,12 +159,14 @@ export function FieldFormat({ value = validateFieldName(value) } - setStoreValue(fields.map((f: Field) => (f.id === id ? { ...f, [field]: value } : f))) + setStoreValue((fields || []).map((f: Field) => (f.id === id ? { ...f, [field]: value } : f))) } const toggleCollapse = (id: string) => { if (isPreview || disabled) return - setStoreValue(fields.map((f: Field) => (f.id === id ? { ...f, collapsed: !f.collapsed } : f))) + setStoreValue( + (fields || []).map((f: Field) => (f.id === id ? { ...f, collapsed: !f.collapsed } : f)) + ) } // Field header @@ -371,54 +328,66 @@ export function FieldFormat({
- { - if (el) valueInputRefs.current[field.id] = el - }} - name='value' - value={field.value || ''} - onChange={(e) => handleValueInputChange(field.id, e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Escape') { - handleTagDropdownClose(field.id) + {field.type === 'boolean' ? ( + + ) : field.type === 'object' || field.type === 'array' ? ( +