fix(permissions): make permissions check case insensitive, resolve hydration issues by consolidating environment checking function (#678)

* fix(permissions): make permissions check case insensitive

* consolidated use of helpers to fetch environment

* use import aliases

* fix(voice): added voice functionality back to chat client (#676)

* fix(voice): added voice functioanlity back to chat clinet

* add logic to support deployed chat in staging

* fix(api-timeout): increase timeout for API block to 2 min (#677)

* feat(wealthbox): added wealthbox crm (#669)

* feat: wealthbox

* feat: added tools

* feat: tested and finished tools

* feat: tested and finished tools

* feat: added refresh token

* fix: added docs

* bun lint

* feat: removed files #669

* fix: greptile comments

* fix: stringified messages #669

* add visibilty to params

---------

Co-authored-by: Adam Gough <adamgough@Adams-MacBook-Pro.local>
Co-authored-by: Adam Gough <adamgough@Mac.attlocal.net>
Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>

---------

Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
Co-authored-by: Adam Gough <77861281+aadamgough@users.noreply.github.com>
Co-authored-by: Adam Gough <adamgough@Adams-MacBook-Pro.local>
Co-authored-by: Adam Gough <adamgough@Mac.attlocal.net>
Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
This commit is contained in:
Waleed Latif
2025-07-14 13:42:06 -07:00
committed by GitHub
parent d65bdaf546
commit 8a9bc4e929
47 changed files with 145 additions and 181 deletions

View File

@@ -2,7 +2,7 @@ import { and, eq } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { env } from '@/lib/env'
import { isDev } from '@/lib/environment'
import { createLogger } from '@/lib/logs/console-logger'
import { getBaseDomain } from '@/lib/urls/utils'
import { encryptSecret } from '@/lib/utils'
@@ -71,9 +71,7 @@ export async function GET(_request: NextRequest, { params }: { params: Promise<{
// Create a new result object without the password
const { password, ...safeData } = chatInstance[0]
const isDevelopment = env.NODE_ENV === 'development'
const chatUrl = isDevelopment
const chatUrl = isDev
? `http://${chatInstance[0].subdomain}.${getBaseDomain()}`
: `https://${chatInstance[0].subdomain}.simstudio.ai`
@@ -221,9 +219,7 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
const updatedSubdomain = subdomain || existingChat[0].subdomain
const isDevelopment = env.NODE_ENV === 'development'
const chatUrl = isDevelopment
const chatUrl = isDev
? `http://${updatedSubdomain}.${getBaseDomain()}`
: `https://${updatedSubdomain}.simstudio.ai`

View File

@@ -4,6 +4,7 @@ import { v4 as uuidv4 } from 'uuid'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { env } from '@/lib/env'
import { isDev } from '@/lib/environment'
import { createLogger } from '@/lib/logs/console-logger'
import { encryptSecret } from '@/lib/utils'
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
@@ -169,11 +170,10 @@ export async function POST(request: NextRequest) {
// Return successful response with chat URL
// Check if we're in development or production
const isDevelopment = env.NODE_ENV === 'development'
const baseUrl = env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
let chatUrl: string
if (isDevelopment) {
if (isDev) {
try {
const url = new URL(baseUrl)
chatUrl = `${url.protocol}//${subdomain}.${url.host}`

View File

@@ -1,7 +1,7 @@
import { eq, sql } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { v4 as uuidv4 } from 'uuid'
import { env } from '@/lib/env'
import { isDev } from '@/lib/environment'
import { createLogger } from '@/lib/logs/console-logger'
import { EnhancedLoggingSession } from '@/lib/logs/enhanced-logging-session'
import { buildTraceSpans } from '@/lib/logs/trace-spans'
@@ -20,7 +20,6 @@ declare global {
}
const logger = createLogger('ChatAuthUtils')
const isDevelopment = env.NODE_ENV === 'development'
export const encryptAuthToken = (subdomainId: string, type: string): string => {
return Buffer.from(`${subdomainId}:${type}:${Date.now()}`).toString('base64')
@@ -63,11 +62,11 @@ export const setChatAuthCookie = (
name: `chat_auth_${subdomainId}`,
value: token,
httpOnly: true,
secure: !isDevelopment,
secure: !isDev,
sameSite: 'lax',
path: '/',
// Using subdomain for the domain in production
domain: isDevelopment ? undefined : '.simstudio.ai',
domain: isDev ? undefined : '.simstudio.ai',
maxAge: 60 * 60 * 24, // 24 hours
})
}
@@ -78,7 +77,7 @@ export function addCorsHeaders(response: NextResponse, request: NextRequest) {
const origin = request.headers.get('origin') || ''
// In development, allow any localhost subdomain
if (isDevelopment && origin.includes('localhost')) {
if (isDev && origin.includes('localhost')) {
response.headers.set('Access-Control-Allow-Origin', origin)
response.headers.set('Access-Control-Allow-Credentials', 'true')
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')

View File

@@ -18,7 +18,7 @@ import {
isBlobPath,
isCloudPath,
isS3Path,
} from '../utils'
} from '@/app/api/files/utils'
export const dynamic = 'force-dynamic'

View File

@@ -447,7 +447,7 @@ async function handleCsvBuffer(
logger.info(`Parsing CSV in memory: ${filename}`)
// Use the parseBuffer function from our library
const { parseBuffer } = await import('../../../../lib/file-parsers')
const { parseBuffer } = await import('@/lib/file-parsers')
const result = await parseBuffer(fileBuffer, 'csv')
return {
@@ -492,7 +492,7 @@ async function handleGenericTextBuffer(
// Try to use a specialized parser if available
try {
const { parseBuffer, isSupportedFileType } = await import('../../../../lib/file-parsers')
const { parseBuffer, isSupportedFileType } = await import('@/lib/file-parsers')
if (isSupportedFileType(extension)) {
const result = await parseBuffer(fileBuffer, extension)
@@ -578,7 +578,7 @@ async function parseBufferAsPdf(buffer: Buffer) {
// Import parsers dynamically to avoid initialization issues in tests
// First try to use the main PDF parser
try {
const { PdfParser } = await import('../../../../lib/file-parsers/pdf-parser')
const { PdfParser } = await import('@/lib/file-parsers/pdf-parser')
const parser = new PdfParser()
logger.info('Using main PDF parser for buffer')
@@ -589,7 +589,7 @@ async function parseBufferAsPdf(buffer: Buffer) {
} catch (error) {
// Fallback to raw PDF parser
logger.warn('Main PDF parser failed, using raw parser for buffer:', error)
const { RawPdfParser } = await import('../../../../lib/file-parsers/raw-pdf-parser')
const { RawPdfParser } = await import('@/lib/file-parsers/raw-pdf-parser')
const rawParser = new RawPdfParser()
return await rawParser.parseBuffer(buffer)

View File

@@ -7,7 +7,7 @@ import { getStorageProvider, isUsingCloudStorage } from '@/lib/uploads'
import { getBlobServiceClient } from '@/lib/uploads/blob/blob-client'
import { getS3Client, sanitizeFilenameForMetadata } from '@/lib/uploads/s3/s3-client'
import { BLOB_CONFIG, BLOB_KB_CONFIG, S3_CONFIG, S3_KB_CONFIG } from '@/lib/uploads/setup'
import { createErrorResponse, createOptionsResponse } from '../utils'
import { createErrorResponse, createOptionsResponse } from '@/app/api/files/utils'
const logger = createLogger('PresignedUploadAPI')

View File

@@ -11,7 +11,7 @@ import {
FileNotFoundError,
findLocalFile,
getContentType,
} from '../../utils'
} from '@/app/api/files/utils'
export const dynamic = 'force-dynamic'

View File

@@ -13,6 +13,7 @@ import {
} from '@/lib/billing/validation/seat-management'
import { sendEmail } from '@/lib/email/mailer'
import { validateAndNormalizeEmail } from '@/lib/email/utils'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console-logger'
import { hasWorkspaceAdminAccess } from '@/lib/permissions/utils'
import { db } from '@/db'
@@ -344,7 +345,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
organizationEntry[0]?.name || 'organization',
role,
workspaceInvitationsWithNames,
`${process.env.NEXT_PUBLIC_BASE_URL}/api/organizations/invitations/accept?id=${orgInvitation.id}`
`${env.NEXT_PUBLIC_APP_URL}/api/organizations/invitations/accept?id=${orgInvitation.id}`
)
emailResult = await sendEmail({
@@ -357,7 +358,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
const emailHtml = await renderInvitationEmail(
inviter[0]?.name || 'Someone',
organizationEntry[0]?.name || 'organization',
`${process.env.NEXT_PUBLIC_BASE_URL}/api/organizations/invitations/accept?id=${orgInvitation.id}`,
`${env.NEXT_PUBLIC_APP_URL}/api/organizations/invitations/accept?id=${orgInvitation.id}`,
email
)

View File

@@ -6,6 +6,7 @@ import { getSession } from '@/lib/auth'
import { validateSeatAvailability } from '@/lib/billing/validation/seat-management'
import { sendEmail } from '@/lib/email/mailer'
import { validateAndNormalizeEmail } from '@/lib/email/utils'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console-logger'
import { db } from '@/db'
import { invitation, member, organization, user, userStats } from '@/db/schema'
@@ -246,7 +247,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
const emailHtml = await renderInvitationEmail(
inviter[0]?.name || 'Someone',
organizationEntry[0]?.name || 'organization',
`${process.env.NEXT_PUBLIC_BASE_URL}/api/organizations/invitations/accept?id=${invitationId}`,
`${env.NEXT_PUBLIC_APP_URL}/api/organizations/invitations/accept?id=${invitationId}`,
normalizedEmail
)

View File

@@ -1,4 +1,5 @@
import { NextResponse } from 'next/server'
import { isDev } from '@/lib/environment'
import { createLogger } from '@/lib/logs/console-logger'
import { executeTool } from '@/tools'
import { getTool, validateToolRequest } from '@/tools/utils'
@@ -51,14 +52,14 @@ const createErrorResponse = (error: any, status = 500, additionalData = {}) => {
logger.error('Creating error response', {
errorMessage,
status,
stack: process.env.NODE_ENV === 'development' ? errorStack : undefined,
stack: isDev ? errorStack : undefined,
})
return formatResponse(
{
success: false,
error: errorMessage,
stack: process.env.NODE_ENV === 'development' ? errorStack : undefined,
stack: isDev ? errorStack : undefined,
...additionalData,
},
status

View File

@@ -1,5 +1,6 @@
import { type NextRequest, NextResponse } from 'next/server'
import { env } from '@/lib/env'
import { isProd } from '@/lib/environment'
import { createLogger } from '@/lib/logs/console-logger'
const logger = createLogger('TelemetryAPI')
@@ -101,7 +102,7 @@ async function forwardToCollector(data: any): Promise<boolean> {
},
{
key: 'deployment.environment',
value: { stringValue: env.NODE_ENV || 'production' },
value: { stringValue: isProd ? 'production' : 'development' },
},
]

View File

@@ -1,6 +1,7 @@
import crypto from 'crypto'
import { eq } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console-logger'
import { saveWorkflowToNormalizedTables } from '@/lib/workflows/db-helpers'
import { db } from '@/db'
@@ -88,7 +89,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
// Notify socket server about the revert operation for real-time sync
try {
const socketServerUrl = process.env.SOCKET_SERVER_URL || 'http://localhost:3002'
const socketServerUrl = env.SOCKET_SERVER_URL || 'http://localhost:3002'
await fetch(`${socketServerUrl}/api/workflow-reverted`, {
method: 'POST',
headers: {

View File

@@ -11,7 +11,7 @@ import {
AlertDialogTitle,
} from '@/components/ui/alert-dialog'
import { Button } from '@/components/ui/button'
import { getNodeEnv } from '@/lib/environment'
import { isDev } from '@/lib/environment'
import { createLogger } from '@/lib/logs/console-logger'
import { useGeneralStore } from '@/stores/settings/general/store'
@@ -50,7 +50,6 @@ export function TelemetryConsentDialog() {
const loadSettings = useGeneralStore((state) => state.loadSettings)
const hasShownDialogThisSession = useRef(false)
const isDevelopment = getNodeEnv() === 'development'
const isChatSubdomainOrPath =
typeof window !== 'undefined' &&
@@ -116,7 +115,7 @@ export function TelemetryConsentDialog() {
telemetryNotifiedUser,
telemetryEnabled,
hasShownInSession: hasShownDialogThisSession.current,
environment: getNodeEnv(),
environment: isDev,
})
const localStorageNotified =
@@ -134,11 +133,11 @@ export function TelemetryConsentDialog() {
!localStorageNotified &&
telemetryEnabled &&
!hasShownDialogThisSession.current &&
isDevelopment
isDev
) {
setOpen(true)
hasShownDialogThisSession.current = true
} else if (settingsLoaded && !telemetryNotifiedUser && !isDevelopment) {
} else if (settingsLoaded && !telemetryNotifiedUser && !isDev) {
// Auto-notify in non-development environments
setTelemetryNotifiedUser(true)
if (typeof window !== 'undefined') {

View File

@@ -30,7 +30,7 @@ import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Skeleton } from '@/components/ui/skeleton'
import { Textarea } from '@/components/ui/textarea'
import { env } from '@/lib/env'
import { isDev } from '@/lib/environment'
import { createLogger } from '@/lib/logs/console-logger'
import { getBaseDomain } from '@/lib/urls/utils'
import { cn } from '@/lib/utils'
@@ -55,7 +55,7 @@ interface ChatDeployProps {
type AuthType = 'public' | 'password' | 'email'
const getDomainSuffix = (() => {
const suffix = env.NODE_ENV === 'development' ? `.${getBaseDomain()}` : '.simstudio.ai'
const suffix = isDev ? `.${getBaseDomain()}` : '.simstudio.ai'
return () => suffix
})()

View File

@@ -7,7 +7,7 @@ import { useParams, usePathname, useRouter } from 'next/navigation'
import { Skeleton } from '@/components/ui/skeleton'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { useSession } from '@/lib/auth-client'
import { env } from '@/lib/env'
import { isDev } from '@/lib/environment'
import { createLogger } from '@/lib/logs/console-logger'
import {
getKeyboardShortcutText,
@@ -28,8 +28,6 @@ import { WorkspaceHeader } from './components/workspace-header/workspace-header'
const logger = createLogger('Sidebar')
const IS_DEV = env.NODE_ENV === 'development'
export function Sidebar() {
useGlobalShortcuts()
@@ -239,7 +237,7 @@ export function Sidebar() {
{isCollapsed ? (
<div className='flex-shrink-0 px-3 pt-1 pb-3'>
<div className='flex flex-col space-y-[1px]'>
{!IS_DEV && (
{!isDev && (
<Tooltip>
<TooltipTrigger asChild>
<div
@@ -286,7 +284,7 @@ export function Sidebar() {
</div>
) : (
<>
{!IS_DEV && (
{!isDev && (
<div className='flex-shrink-0 px-3 pt-1'>
<Tooltip>
<TooltipTrigger asChild>
@@ -343,7 +341,7 @@ export function Sidebar() {
{/* Modals */}
<SettingsModal open={showSettings} onOpenChange={setShowSettings} />
<HelpModal open={showHelp} onOpenChange={setShowHelp} />
{!IS_DEV && <InviteModal open={showInviteMembers} onOpenChange={setShowInviteMembers} />}
{!isDev && <InviteModal open={showInviteMembers} onOpenChange={setShowInviteMembers} />}
</aside>
)
}

View File

@@ -1,12 +1,12 @@
import { DocumentIcon } from '@/components/icons'
import { env } from '@/lib/env'
import { isProd } from '@/lib/environment'
import { createLogger } from '@/lib/logs/console-logger'
import type { FileParserOutput } from '@/tools/file/types'
import type { BlockConfig, SubBlockConfig, SubBlockLayout, SubBlockType } from '../types'
const logger = createLogger('FileBlock')
const shouldEnableURLInput = env.NODE_ENV === 'production'
const shouldEnableURLInput = isProd
const inputMethodBlock: SubBlockConfig = {
id: 'inputMethod',

View File

@@ -1,9 +1,9 @@
import { MistralIcon } from '@/components/icons'
import { env } from '@/lib/env'
import { isProd } from '@/lib/environment'
import type { MistralParserOutput } from '@/tools/mistral/types'
import type { BlockConfig, SubBlockConfig, SubBlockLayout, SubBlockType } from '../types'
const shouldEnableFileUpload = env.NODE_ENV === 'production'
const shouldEnableFileUpload = isProd
const inputMethodBlock: SubBlockConfig = {
id: 'inputMethod',

View File

@@ -1,6 +1,7 @@
import { drizzle, type PostgresJsDatabase } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
import { env } from '@/lib/env'
import { isDev } from '@/lib/environment'
import * as schema from './schema'
// In production, use the Vercel-generated POSTGRES_URL
@@ -38,4 +39,4 @@ declare global {
}
export const db = global.database || drizzleClient
if (env.NODE_ENV !== 'production') global.database = db
if (isDev) global.database = db

View File

@@ -46,9 +46,9 @@ export function useUserPermissions(
}
}
// Find current user in workspace permissions
// Find current user in workspace permissions (case-insensitive)
const currentUser = workspacePermissions?.users?.find(
(user) => user.email === session.user.email
(user) => user.email.toLowerCase() === session.user.email.toLowerCase()
)
// If user not found in workspace, they have no permissions

View File

@@ -18,18 +18,14 @@ import {
linkedErrorsIntegration,
makeFetchTransport,
} from '@sentry/nextjs'
const clientEnv = {
NODE_ENV: process.env.NODE_ENV,
NEXT_PUBLIC_SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN,
NEXT_TELEMETRY_DISABLED: process.env.NEXT_TELEMETRY_DISABLED,
}
import { env } from './lib/env'
import { isProd } from './lib/environment'
// Only in production
if (typeof window !== 'undefined' && clientEnv.NODE_ENV === 'production') {
if (typeof window !== 'undefined' && isProd) {
const client = new BrowserClient({
dsn: clientEnv.NEXT_PUBLIC_SENTRY_DSN || undefined,
environment: clientEnv.NODE_ENV || 'development',
dsn: env.NEXT_PUBLIC_SENTRY_DSN || undefined,
environment: env.NODE_ENV || 'development',
transport: makeFetchTransport,
stackParser: defaultStackParser,
integrations: [breadcrumbsIntegration(), dedupeIntegration(), linkedErrorsIntegration()],
@@ -45,15 +41,14 @@ if (typeof window !== 'undefined' && clientEnv.NODE_ENV === 'production') {
client.init()
}
export const onRouterTransitionStart =
clientEnv.NODE_ENV === 'production' ? captureRouterTransitionStart : () => {}
export const onRouterTransitionStart = isProd ? captureRouterTransitionStart : () => {}
if (typeof window !== 'undefined') {
const TELEMETRY_STATUS_KEY = 'simstudio-telemetry-status'
let telemetryEnabled = true
try {
if (clientEnv.NEXT_TELEMETRY_DISABLED === '1') {
if (env.NEXT_TELEMETRY_DISABLED === '1') {
telemetryEnabled = false
} else {
const storedPreference = localStorage.getItem(TELEMETRY_STATUS_KEY)

View File

@@ -3,13 +3,12 @@
*
* This file contains all server-side instrumentation logic.
*/
import { createLogger } from '@/lib/logs/console-logger'
import { env } from './lib/env.ts'
const Sentry =
process.env.NODE_ENV === 'production'
? require('@sentry/nextjs')
: { captureRequestError: () => {} }
import { env } from './lib/env'
import { isProd } from './lib/environment'
import { createLogger } from './lib/logs/console-logger'
const Sentry = isProd ? require('@sentry/nextjs') : { captureRequestError: () => {} }
const logger = createLogger('OtelInstrumentation')
@@ -103,7 +102,7 @@ async function initializeOpenTelemetry() {
}
async function initializeSentry() {
if (env.NODE_ENV !== 'production') return
if (!isProd) return
try {
const Sentry = await import('@sentry/nextjs')

View File

@@ -2,26 +2,19 @@ import { stripeClient } from '@better-auth/stripe/client'
import { emailOTPClient, genericOAuthClient, organizationClient } from 'better-auth/client/plugins'
import { createAuthClient } from 'better-auth/react'
import { env } from './env'
const clientEnv = {
NEXT_PUBLIC_VERCEL_URL: env.NEXT_PUBLIC_VERCEL_URL,
NEXT_PUBLIC_APP_URL: env.NEXT_PUBLIC_APP_URL,
NODE_ENV: env.NODE_ENV,
VERCEL_ENV: env.VERCEL_ENV || '',
BETTER_AUTH_URL: env.BETTER_AUTH_URL,
}
import { isDev, isProd } from './environment'
export function getBaseURL() {
let baseURL
if (clientEnv.VERCEL_ENV === 'preview') {
baseURL = `https://${clientEnv.NEXT_PUBLIC_VERCEL_URL}`
} else if (clientEnv.VERCEL_ENV === 'development') {
baseURL = `https://${clientEnv.NEXT_PUBLIC_VERCEL_URL}`
} else if (clientEnv.VERCEL_ENV === 'production') {
baseURL = clientEnv.BETTER_AUTH_URL || clientEnv.NEXT_PUBLIC_APP_URL
} else if (clientEnv.NODE_ENV === 'development') {
baseURL = clientEnv.NEXT_PUBLIC_APP_URL || clientEnv.BETTER_AUTH_URL || 'http://localhost:3000'
if (env.VERCEL_ENV === 'preview') {
baseURL = `https://${env.NEXT_PUBLIC_VERCEL_URL}`
} else if (env.VERCEL_ENV === 'development') {
baseURL = `https://${env.NEXT_PUBLIC_VERCEL_URL}`
} else if (env.VERCEL_ENV === 'production') {
baseURL = env.BETTER_AUTH_URL || env.NEXT_PUBLIC_APP_URL
} else if (env.NODE_ENV === 'development') {
baseURL = env.NEXT_PUBLIC_APP_URL || env.BETTER_AUTH_URL || 'http://localhost:3000'
}
return baseURL
@@ -33,7 +26,7 @@ export const client = createAuthClient({
emailOTPClient(),
genericOAuthClient(),
// Only include Stripe client in production
...(clientEnv.NODE_ENV === 'production'
...(isProd
? [
stripeClient({
subscription: true, // Enable subscription management
@@ -48,7 +41,7 @@ export const { useSession, useActiveOrganization } = client
export const useSubscription = () => {
// In development, provide mock implementations
if (clientEnv.NODE_ENV === 'development') {
if (isDev) {
return {
list: async () => ({ data: [] }),
upgrade: async () => ({

View File

@@ -19,17 +19,16 @@ import {
renderOTPEmail,
renderPasswordResetEmail,
} from '@/components/emails/render-email'
import { env, isTruthy } from '@/lib/env'
import { isProd } from '@/lib/environment'
import { createLogger } from '@/lib/logs/console-logger'
import { getEmailDomain } from '@/lib/urls/utils'
import { db } from '@/db'
import * as schema from '@/db/schema'
import { getBaseURL } from './auth-client'
import { env, isTruthy } from './env'
import { getEmailDomain } from './urls/utils'
const logger = createLogger('Auth')
const isProd = env.NODE_ENV === 'production'
// Only initialize Stripe if the key is provided
// This allows local development without a Stripe account
const validStripeKey =

View File

@@ -1,5 +1,6 @@
import fs from 'fs/promises'
import path from 'path'
import { isDev } from '@/lib/environment'
import { createLogger } from '@/lib/logs/console-logger'
import { generateEmbeddings } from '@/app/api/knowledge/utils'
import { TextChunker } from './chunker'
@@ -28,7 +29,6 @@ export class DocsChunker {
overlap: options.overlap ?? 50,
})
// Use localhost docs in development, production docs otherwise
const isDev = process.env.NODE_ENV === 'development'
this.baseUrl =
options.baseUrl ?? (isDev ? 'http://localhost:3001' : 'https://docs.simstudio.ai')
}

View File

@@ -1,9 +1,6 @@
import { createEnv } from '@t3-oss/env-nextjs'
import { env as runtimeEnv } from 'next-runtime-env'
import { z } from 'zod'
const getEnv = (variable: string) => runtimeEnv(variable) ?? process.env[variable]
export const env = createEnv({
skipValidation: true,
@@ -48,7 +45,6 @@ export const env = createEnv({
SENTRY_PROJECT: z.string().optional(),
SENTRY_AUTH_TOKEN: z.string().optional(),
REDIS_URL: z.string().url().optional(),
NEXT_TELEMETRY_DISABLED: z.string().optional(),
NEXT_RUNTIME: z.string().optional(),
VERCEL_ENV: z.string().optional(),
@@ -68,7 +64,6 @@ export const env = createEnv({
// Miscellaneous
CRON_SECRET: z.string().optional(),
FREE_PLAN_LOG_RETENTION_DAYS: z.string().optional(),
NODE_ENV: z.string().optional(),
GITHUB_TOKEN: z.string().optional(),
ELEVENLABS_API_KEY: z.string().min(1).optional(),
AZURE_OPENAI_ENDPOINT: z.string().url().optional(),
@@ -111,6 +106,7 @@ export const env = createEnv({
SOCKET_SERVER_URL: z.string().url().optional(),
SOCKET_PORT: z.number().optional(),
PORT: z.number().optional(),
ALLOWED_ORIGINS: z.string().optional(),
},
client: {
@@ -123,15 +119,22 @@ export const env = createEnv({
NEXT_PUBLIC_SOCKET_URL: z.string().url().optional(),
},
// Only need to define client variables, server variables are automatically handled
// Variables available on both server and client
shared: {
NODE_ENV: z.enum(['development', 'test', 'production']).optional(),
NEXT_TELEMETRY_DISABLED: z.string().optional(),
},
experimental__runtimeEnv: {
NEXT_PUBLIC_APP_URL: getEnv('NEXT_PUBLIC_APP_URL'),
NEXT_PUBLIC_VERCEL_URL: getEnv('NEXT_PUBLIC_VERCEL_URL'),
NEXT_PUBLIC_SENTRY_DSN: getEnv('NEXT_PUBLIC_SENTRY_DSN'),
NEXT_PUBLIC_GOOGLE_CLIENT_ID: getEnv('NEXT_PUBLIC_GOOGLE_CLIENT_ID'),
NEXT_PUBLIC_GOOGLE_API_KEY: getEnv('NEXT_PUBLIC_GOOGLE_API_KEY'),
NEXT_PUBLIC_GOOGLE_PROJECT_NUMBER: getEnv('NEXT_PUBLIC_GOOGLE_PROJECT_NUMBER'),
NEXT_PUBLIC_SOCKET_URL: getEnv('NEXT_PUBLIC_SOCKET_URL'),
NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
NEXT_PUBLIC_VERCEL_URL: process.env.VERCEL_URL,
NEXT_PUBLIC_SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN,
NEXT_PUBLIC_GOOGLE_CLIENT_ID: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID,
NEXT_PUBLIC_GOOGLE_API_KEY: process.env.NEXT_PUBLIC_GOOGLE_API_KEY,
NEXT_PUBLIC_GOOGLE_PROJECT_NUMBER: process.env.NEXT_PUBLIC_GOOGLE_PROJECT_NUMBER,
NEXT_PUBLIC_SOCKET_URL: process.env.NEXT_PUBLIC_SOCKET_URL,
NODE_ENV: process.env.NODE_ENV,
NEXT_TELEMETRY_DISABLED: process.env.NEXT_TELEMETRY_DISABLED,
},
})

View File

@@ -3,28 +3,20 @@
*/
import { env } from './env'
export const getNodeEnv = () => {
try {
return env.NODE_ENV
} catch {
return process.env.NODE_ENV
}
}
/**
* Is the application running in production mode
*/
export const isProd = getNodeEnv() === 'production'
export const isProd = env.NODE_ENV === 'production'
/**
* Is the application running in development mode
*/
export const isDev = getNodeEnv() === 'development'
export const isDev = env.NODE_ENV === 'development'
/**
* Is the application running in test mode
*/
export const isTest = getNodeEnv() === 'test'
export const isTest = env.NODE_ENV === 'test'
/**
* Is this the hosted version of the application

View File

@@ -1,6 +1,6 @@
import { FreestyleSandboxes } from 'freestyle-sandboxes'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console-logger'
import { env } from './env'
const logger = createLogger('Freestyle')

View File

@@ -1,5 +1,5 @@
import OpenAI from 'openai'
import { env } from './env'
import { env } from '@/lib/env'
/**
* Generates a short title for a chat based on the first message

View File

@@ -44,8 +44,8 @@ const LOG_CONFIG = {
colorize: true,
},
production: {
enabled: true,
minLevel: LogLevel.INFO, // Only show INFO and above in production
enabled: false, // Disable all console logs in production
minLevel: LogLevel.ERROR,
colorize: false,
},
test: {

View File

@@ -23,8 +23,8 @@ import {
WealthboxIcon,
xIcon,
} from '@/components/icons'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console-logger'
import { env } from '../env'
const logger = createLogger('OAuth')

View File

@@ -1,6 +1,6 @@
import Redis from 'ioredis'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console-logger'
import { env } from './env'
const logger = createLogger('Redis')

View File

@@ -9,8 +9,9 @@
* Please maintain ethical telemetry practices if modified.
*/
import { DiagConsoleLogger, DiagLogLevel, diag } from '@opentelemetry/api'
import { env } from '@/lib/env'
import { isProd } from '@/lib/environment'
import { createLogger } from '@/lib/logs/console-logger'
import { env } from './env'
diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.ERROR)
@@ -142,7 +143,7 @@ function initializeClientTelemetry(): void {
return
}
if (env.NODE_ENV === 'production') {
if (isProd) {
trackEvent('page_view', window.location.pathname)
if (typeof window.history !== 'undefined') {
@@ -265,7 +266,7 @@ export async function trackEvent(
if (!status.enabled) return
try {
if (env.NODE_ENV === 'production') {
if (isProd) {
await fetch('/api/telemetry', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },

View File

@@ -5,8 +5,8 @@ import {
S3Client,
} from '@aws-sdk/client-s3'
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
import { env } from '../../env'
import { S3_CONFIG } from '../setup'
import { env } from '@/lib/env'
import { S3_CONFIG } from '@/lib/uploads/setup'
// Lazily create a single S3 client instance.
let _s3Client: S3Client | null = null

View File

@@ -1,5 +1,5 @@
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console-logger'
import { env } from '../env'
import {
ensureUploadsDirectory,
getStorageProvider,

View File

@@ -1,8 +1,8 @@
import { existsSync } from 'fs'
import { mkdir } from 'fs/promises'
import path, { join } from 'path'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console-logger'
import { env } from '../env'
const logger = createLogger('UploadsSetup')

View File

@@ -1,4 +1,5 @@
import { env } from '../env'
import { env } from '@/lib/env'
import { isProd } from '@/lib/environment'
/**
* Returns the base URL of the application, respecting environment variables for deployment environments
@@ -15,7 +16,6 @@ export function getBaseUrl(): string {
return baseUrl
}
const isProd = env.NODE_ENV === 'production'
const protocol = isProd ? 'https://' : 'http://'
return `${protocol}${baseUrl}`
}
@@ -36,7 +36,6 @@ export function getBaseDomain(): string {
try {
return new URL(fallbackUrl).host
} catch {
const isProd = env.NODE_ENV === 'production'
return isProd ? 'simstudio.ai' : 'localhost:3000'
}
}
@@ -51,7 +50,6 @@ export function getEmailDomain(): string {
const baseDomain = getBaseDomain()
return baseDomain.startsWith('www.') ? baseDomain.substring(4) : baseDomain
} catch (_e) {
const isProd = env.NODE_ENV === 'production'
return isProd ? 'simstudio.ai' : 'localhost:3000'
}
}

View File

@@ -2,8 +2,8 @@ import { createCipheriv, createDecipheriv, randomBytes } from 'crypto'
import { type ClassValue, clsx } from 'clsx'
import { nanoid } from 'nanoid'
import { twMerge } from 'tailwind-merge'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console-logger'
import { env } from './env'
const logger = createLogger('Utils')

View File

@@ -1,11 +1,11 @@
import { eq } from 'drizzle-orm'
import { NextResponse } from 'next/server'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console-logger'
import { db } from '@/db'
import { userStats, workflow as workflowTable } from '@/db/schema'
import type { ExecutionResult } from '@/executor/types'
import type { WorkflowState } from '@/stores/workflows/workflow/types'
import { env } from '../env'
const logger = createLogger('WorkflowUtils')

View File

@@ -1,14 +1,11 @@
import { getSessionCookie } from 'better-auth/cookies'
import { type NextRequest, NextResponse } from 'next/server'
import { createLogger } from '@/lib/logs/console-logger'
import { getBaseDomain } from '@/lib/urls/utils'
import { env } from './lib/env'
import { isDev } from './lib/environment'
import { createLogger } from './lib/logs/console-logger'
import { getBaseDomain } from './lib/urls/utils'
const logger = createLogger('Middleware')
// Environment flag to check if we're in development mode
const isDevelopment = env.NODE_ENV === 'development'
const SUSPICIOUS_UA_PATTERNS = [
/^\s*$/, // Empty user agents
/\.\./, // Path traversal attempt
@@ -36,7 +33,7 @@ export async function middleware(request: NextRequest) {
// Extract root domain from BASE_DOMAIN (e.g., "simstudio.ai" from "staging.simstudio.ai")
const baseParts = BASE_DOMAIN.split('.')
const rootDomain = isDevelopment
const rootDomain = isDev
? 'localhost'
: baseParts.length >= 2
? baseParts
@@ -129,16 +126,6 @@ export async function middleware(request: NextRequest) {
return NextResponse.next()
}
// If self-hosted skip waitlist
if (env.DOCKER_BUILD) {
return NextResponse.next()
}
// Skip waitlist protection for development environment
if (isDevelopment) {
return NextResponse.next()
}
const userAgent = request.headers.get('user-agent') || ''
// Check if this is a webhook endpoint that should be exempt from User-Agent validation

View File

@@ -2,6 +2,7 @@ import path from 'path'
import { withSentryConfig } from '@sentry/nextjs'
import type { NextConfig } from 'next'
import { env, isTruthy } from './lib/env'
import { isDev, isProd } from './lib/environment'
const nextConfig: NextConfig = {
devIndicators: false,
@@ -26,7 +27,7 @@ const nextConfig: NextConfig = {
optimizeCss: true,
turbopackSourceMaps: false,
},
...(env.NODE_ENV === 'development' && {
...(isDev && {
allowedDevOrigins: [
...(env.NEXT_PUBLIC_APP_URL
? (() => {
@@ -152,8 +153,8 @@ const sentryConfig = {
org: env.SENTRY_ORG || '',
project: env.SENTRY_PROJECT || '',
authToken: env.SENTRY_AUTH_TOKEN || undefined,
disableSourceMapUpload: env.NODE_ENV !== 'production',
autoInstrumentServerFunctions: env.NODE_ENV === 'production',
disableSourceMapUpload: !isProd,
autoInstrumentServerFunctions: isProd,
bundleSizeOptimizations: {
excludeDebugStatements: true,
excludePerformanceMonitoring: true,
@@ -163,6 +164,4 @@ const sentryConfig = {
},
}
export default env.NODE_ENV === 'development'
? nextConfig
: withSentryConfig(nextConfig, sentryConfig)
export default isDev ? nextConfig : withSentryConfig(nextConfig, sentryConfig)

View File

@@ -3,6 +3,7 @@
import path from 'path'
import { sql } from 'drizzle-orm'
import { DocsChunker } from '@/lib/documents/docs-chunker'
import { isDev } from '@/lib/environment'
import { createLogger } from '@/lib/logs/console-logger'
import { db } from '@/db'
import { docsEmbeddings } from '@/db/schema'
@@ -38,11 +39,7 @@ async function processDocsEmbeddings(options: ProcessingOptions = {}) {
clearExisting: options.clearExisting ?? false,
docsPath: options.docsPath ?? path.join(process.cwd(), '../../apps/docs/content/docs'),
// Use localhost docs in development, production docs otherwise
baseUrl:
options.baseUrl ??
(process.env.NODE_ENV === 'development'
? 'http://localhost:3001'
: 'https://docs.simstudio.ai'),
baseUrl: options.baseUrl ?? (isDev ? 'http://localhost:3001' : 'https://docs.simstudio.ai'),
chunkSize: options.chunkSize ?? 300, // Max 300 tokens per chunk
minChunkSize: options.minChunkSize ?? 100,
overlap: options.overlap ?? 50,

View File

@@ -1,5 +1,7 @@
import type { Server as HttpServer } from 'http'
import { Server } from 'socket.io'
import { env } from '@/lib/env'
import { isProd } from '@/lib/environment'
import { createLogger } from '../../lib/logs/console-logger'
const logger = createLogger('SocketIOConfig')
@@ -9,11 +11,11 @@ const logger = createLogger('SocketIOConfig')
*/
function getAllowedOrigins(): string[] {
const allowedOrigins = [
process.env.NEXT_PUBLIC_APP_URL,
process.env.VERCEL_URL,
env.NEXT_PUBLIC_APP_URL,
env.NEXT_PUBLIC_VERCEL_URL,
'http://localhost:3000',
'http://localhost:3001',
...(process.env.ALLOWED_ORIGINS?.split(',') || []),
...(env.ALLOWED_ORIGINS?.split(',') || []),
].filter((url): url is string => Boolean(url))
logger.info('Socket.IO CORS configuration:', { allowedOrigins })
@@ -46,7 +48,7 @@ export function createSocketIOServer(httpServer: HttpServer): Server {
path: '/',
httpOnly: true,
sameSite: 'none', // Required for cross-origin cookies
secure: process.env.NODE_ENV === 'production', // HTTPS in production
secure: isProd, // HTTPS in production
},
})
@@ -56,7 +58,7 @@ export function createSocketIOServer(httpServer: HttpServer): Server {
pingTimeout: 60000,
pingInterval: 25000,
maxHttpBufferSize: 1e6,
cookieSecure: process.env.NODE_ENV === 'production',
cookieSecure: isProd,
corsCredentials: true,
})

View File

@@ -1,11 +1,11 @@
import { and, eq, or } from 'drizzle-orm'
import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console-logger'
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/db-helpers'
import * as schema from '../../db/schema'
import { workflow, workflowBlocks, workflowEdges, workflowSubflows } from '../../db/schema'
import { env } from '../../lib/env'
import { createLogger } from '../../lib/logs/console-logger'
import { loadWorkflowFromNormalizedTables } from '../../lib/workflows/db-helpers'
const logger = createLogger('SocketDatabase')

View File

@@ -1,4 +1,5 @@
import { createServer } from 'http'
import { env } from '@/lib/env'
import { createLogger } from '../lib/logs/console-logger'
import { createSocketIOServer } from './config/socket'
import { setupAllHandlers } from './handlers'
@@ -75,13 +76,13 @@ io.engine.on('connection_error', (err) => {
})
})
const PORT = Number(process.env.PORT || process.env.SOCKET_PORT || 3002)
const PORT = Number(env.PORT || env.SOCKET_PORT || 3002)
logger.info('Starting Socket.IO server...', {
port: PORT,
nodeEnv: process.env.NODE_ENV,
hasDatabase: !!process.env.DATABASE_URL,
hasAuth: !!process.env.BETTER_AUTH_SECRET,
nodeEnv: env.NODE_ENV,
hasDatabase: !!env.DATABASE_URL,
hasAuth: !!env.BETTER_AUTH_SECRET,
})
httpServer.listen(PORT, '0.0.0.0', () => {

View File

@@ -1,6 +1,6 @@
import type { Socket } from 'socket.io'
import { auth } from '../../lib/auth'
import { createLogger } from '../../lib/logs/console-logger'
import { auth } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
const logger = createLogger('SocketAuth')

View File

@@ -2,10 +2,10 @@ import { and, eq, isNull } from 'drizzle-orm'
import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
import type { Server } from 'socket.io'
import * as schema from '../../db/schema'
import { workflowBlocks, workflowEdges } from '../../db/schema'
import { env } from '../../lib/env'
import { createLogger } from '../../lib/logs/console-logger'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console-logger'
import * as schema from '@/db/schema'
import { workflowBlocks, workflowEdges } from '@/db/schema'
// Create dedicated database connection for room manager
const connectionString = env.POSTGRES_URL ?? env.DATABASE_URL

View File

@@ -1,5 +1,5 @@
import { env } from '@/lib/env'
import { getNodeEnv } from '@/lib/environment'
import { isTest } from '@/lib/environment'
import { createLogger } from '@/lib/logs/console-logger'
import { getBaseUrl } from '@/lib/urls/utils'
import type { HttpMethod, TableRow, ToolConfig } from '../types'
@@ -109,7 +109,7 @@ const processUrl = (
// Check if a URL needs proxy to avoid CORS/method restrictions
const shouldUseProxy = (url: string): boolean => {
// Skip proxying in test environment
if (getNodeEnv() === 'test') {
if (isTest) {
return false
}