mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-09 23:17:59 -05:00
feat(bun): upgrade to bun, reduce docker image size by 95%, upgrade docs & ci (#371)
* migrate to bun * added envvars to drizzle * upgrade bun devcontainer feature to a valid one * added bun, docker not working * updated envvars, updated to bunder and esnext modules * fixed build, reinstated otel * feat: optimized multi-stage docker images * add coerce for boolean envvar * feat: add docker-compose configuration for local LLM services and remove legacy Dockerfile and entrypoint script * feat: add docker-compose files for local and production environments, and implement GitHub Actions for Docker image build and publish * refactor: remove unused generateStaticParams function from various API routes and maintain dynamic rendering * cleanup * upgraded bun * updated ci * fixed build --------- Co-authored-by: Aditya Tripathi <aditya@climactic.co>
This commit is contained in:
18
apps/docs/.gitignore
vendored
18
apps/docs/.gitignore
vendored
@@ -2,13 +2,11 @@
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# bun specific
|
||||
.bun
|
||||
bun.lockb
|
||||
bun-debug.log*
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
@@ -22,12 +20,6 @@
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files
|
||||
.env
|
||||
*.env
|
||||
|
||||
@@ -6,11 +6,7 @@ This is a Next.js application generated with
|
||||
Run development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
yarn dev
|
||||
bun run dev
|
||||
```
|
||||
|
||||
Open http://localhost:3000 with your browser to see the result.
|
||||
@@ -24,3 +20,4 @@ resources:
|
||||
features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
- [Fumadocs](https://fumadocs.vercel.app) - learn about Fumadocs
|
||||
- [Bun Documentation](https://bun.sh/docs) - learn about Bun features and API
|
||||
|
||||
@@ -3,7 +3,7 @@ import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/page
|
||||
import mdxComponents from '@/components/mdx-components'
|
||||
import { source } from '@/lib/source'
|
||||
|
||||
export const dynamic = 'force-static'
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export default async function Page(props: { params: Promise<{ slug?: string[] }> }) {
|
||||
const params = await props.params
|
||||
|
||||
6105
apps/docs/package-lock.json
generated
6105
apps/docs/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,9 +4,9 @@
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"dev": "dotenv -- next dev --port 3001",
|
||||
"build": "dotenv -- next build",
|
||||
"start": "dotenv -- next start",
|
||||
"dev": "next dev --port 3001",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"postinstall": "fumadocs-mdx"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
31
apps/sim/.env.example
Normal file
31
apps/sim/.env.example
Normal file
@@ -0,0 +1,31 @@
|
||||
# Database (Required)
|
||||
DATABASE_URL="postgresql://postgres:password@localhost:5432/postgres"
|
||||
|
||||
# Authentication (Required)
|
||||
BETTER_AUTH_SECRET=your_secret_key # Use `openssl rand -hex 32` to generate, or visit https://www.better-auth.com/docs/installation
|
||||
BETTER_AUTH_URL=http://localhost:3000
|
||||
|
||||
## Security (Required)
|
||||
ENCRYPTION_KEY=your_encryption_key # Use `openssl rand -hex 32` to generate
|
||||
|
||||
# Email Provider (Optional)
|
||||
# RESEND_API_KEY= # Uncomment and add your key from https://resend.com to send actual emails
|
||||
# If left commented out, emails will be logged to console instead
|
||||
|
||||
# Freestyle API Key (Required for sandboxed code execution for functions/custom-tools)
|
||||
# FREESTYLE_API_KEY= # Uncomment and add your key from https://docs.freestyle.sh/Getting-Started/run
|
||||
|
||||
# S3 Storage Configuration (Optional)
|
||||
# Set USE_S3=true to enable S3 storage in development
|
||||
# USE_S3=true
|
||||
|
||||
# AWS Credentials (Required when USE_S3=true)
|
||||
# AWS_ACCESS_KEY_ID=your-access-key-id
|
||||
# AWS_SECRET_ACCESS_KEY=your-secret-access-key
|
||||
|
||||
# S3 Configuration (Required when USE_S3=true)
|
||||
# S3_BUCKET_NAME=your-bucket-name
|
||||
# AWS_REGION=us-east-1
|
||||
|
||||
# Optional: Custom S3 Base URL (for custom domains or non-AWS S3-compatible storage)
|
||||
# S3_BASE_URL=https://your-custom-domain.com
|
||||
18
apps/sim/.gitignore
vendored
18
apps/sim/.gitignore
vendored
@@ -3,13 +3,11 @@
|
||||
# dependencies
|
||||
/node_modules
|
||||
/packages/**/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# bun specific
|
||||
.bun
|
||||
bun.lockb
|
||||
bun-debug.log*
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
@@ -29,12 +27,6 @@ sim-standalone.tar.gz
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files
|
||||
.env
|
||||
*.env
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
'use server'
|
||||
|
||||
import { env } from '@/lib/env'
|
||||
import { isProd } from '@/lib/environment'
|
||||
|
||||
export async function getOAuthProviderStatus() {
|
||||
const githubAvailable = !!(
|
||||
process.env.GITHUB_CLIENT_ID &&
|
||||
process.env.GITHUB_CLIENT_SECRET &&
|
||||
process.env.GITHUB_CLIENT_ID !== 'placeholder' &&
|
||||
process.env.GITHUB_CLIENT_SECRET !== 'placeholder'
|
||||
env.GITHUB_CLIENT_ID &&
|
||||
env.GITHUB_CLIENT_SECRET &&
|
||||
env.GITHUB_CLIENT_ID !== 'placeholder' &&
|
||||
env.GITHUB_CLIENT_SECRET !== 'placeholder'
|
||||
)
|
||||
|
||||
const googleAvailable = !!(
|
||||
process.env.GOOGLE_CLIENT_ID &&
|
||||
process.env.GOOGLE_CLIENT_SECRET &&
|
||||
process.env.GOOGLE_CLIENT_ID !== 'placeholder' &&
|
||||
process.env.GOOGLE_CLIENT_SECRET !== 'placeholder'
|
||||
env.GOOGLE_CLIENT_ID &&
|
||||
env.GOOGLE_CLIENT_SECRET &&
|
||||
env.GOOGLE_CLIENT_ID !== 'placeholder' &&
|
||||
env.GOOGLE_CLIENT_SECRET !== 'placeholder'
|
||||
)
|
||||
|
||||
return { githubAvailable, googleAvailable, isProduction: isProd }
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { env } from '@/lib/env'
|
||||
import { isProd } from '@/lib/environment'
|
||||
import { getBaseUrl } from '@/lib/urls/utils'
|
||||
import { VerifyContent } from './verify-content'
|
||||
@@ -8,9 +9,7 @@ export const dynamic = 'force-dynamic'
|
||||
export default function VerifyPage() {
|
||||
const baseUrl = getBaseUrl()
|
||||
|
||||
const hasResendKey = Boolean(
|
||||
process.env.RESEND_API_KEY && process.env.RESEND_API_KEY !== 'placeholder'
|
||||
)
|
||||
const hasResendKey = Boolean(env.RESEND_API_KEY && env.RESEND_API_KEY !== 'placeholder')
|
||||
|
||||
return (
|
||||
<main className="flex min-h-screen flex-col items-center justify-center bg-gray-50">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { GithubIcon } from '@/components/icons'
|
||||
import { env } from '@/lib/env'
|
||||
|
||||
/**
|
||||
* Format a number to a human-readable format (e.g., 1000 -> 1k, 1100 -> 1.1k)
|
||||
@@ -16,7 +17,7 @@ function formatNumber(num: number): string {
|
||||
}
|
||||
|
||||
async function getGitHubStars() {
|
||||
const token = process.env.GITHUB_TOKEN
|
||||
const token = env.GITHUB_TOKEN
|
||||
|
||||
try {
|
||||
const response = await fetch('https://api.github.com/repos/simstudioai/sim', {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { toNextJsHandler } from 'better-auth/next-js'
|
||||
import { auth } from '@/lib/auth'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export const { GET, POST } = toNextJsHandler(auth.handler)
|
||||
|
||||
@@ -2,6 +2,8 @@ import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/lib/auth'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('ForgetPasswordAPI')
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { getConfluenceCloudId } from '@/tools/confluence/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const { domain, accessToken, pageId, cloudId: providedCloudId } = await request.json()
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { getConfluenceCloudId } from '@/tools/confluence/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const {
|
||||
|
||||
@@ -6,6 +6,8 @@ import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { db } from '@/db'
|
||||
import { account, user } from '@/db/schema'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('OAuthConnectionsAPI')
|
||||
|
||||
interface GoogleIdToken {
|
||||
@@ -40,7 +42,7 @@ export async function GET(request: NextRequest) {
|
||||
.where(eq(user.id, session.user.id))
|
||||
.limit(1)
|
||||
|
||||
const userEmail = userRecord.length > 0 ? userRecord[0].email : null
|
||||
const userEmail = userRecord.length > 0 ? userRecord[0]?.email : null
|
||||
|
||||
// Process accounts to determine connections
|
||||
const connections: any[] = []
|
||||
|
||||
@@ -8,6 +8,8 @@ import { OAuthService } from '@/lib/oauth'
|
||||
import { db } from '@/db'
|
||||
import { account, user } from '@/db/schema'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('OAuthCredentialsAPI')
|
||||
|
||||
interface GoogleIdToken {
|
||||
|
||||
@@ -5,6 +5,8 @@ import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { db } from '@/db'
|
||||
import { account } from '@/db/schema'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('OAuthDisconnectAPI')
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,6 +8,8 @@ interface DiscordChannel {
|
||||
guild_id?: string
|
||||
}
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('DiscordChannelsAPI')
|
||||
|
||||
export async function POST(request: Request) {
|
||||
|
||||
@@ -7,6 +7,8 @@ interface DiscordServer {
|
||||
icon: string | null
|
||||
}
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('DiscordServersAPI')
|
||||
|
||||
export async function POST(request: Request) {
|
||||
|
||||
@@ -6,6 +6,8 @@ import { db } from '@/db'
|
||||
import { account } from '@/db/schema'
|
||||
import { refreshAccessTokenIfNeeded } from '../../utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('GoogleDriveFileAPI')
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,6 +6,8 @@ import { db } from '@/db'
|
||||
import { account } from '@/db/schema'
|
||||
import { refreshAccessTokenIfNeeded } from '../../utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('GoogleDriveFilesAPI')
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,6 +6,8 @@ import { db } from '@/db'
|
||||
import { account } from '@/db/schema'
|
||||
import { refreshAccessTokenIfNeeded } from '../../utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('GmailLabelAPI')
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
|
||||
@@ -6,6 +6,8 @@ import { db } from '@/db'
|
||||
import { account } from '@/db/schema'
|
||||
import { refreshAccessTokenIfNeeded } from '../../utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('GmailLabelsAPI')
|
||||
|
||||
interface GmailLabel {
|
||||
|
||||
@@ -2,6 +2,8 @@ import { NextResponse } from 'next/server'
|
||||
import { Logger } from '@/lib/logs/console-logger'
|
||||
import { getJiraCloudId } from '@/tools/jira/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = new Logger('jira_issue')
|
||||
|
||||
export async function POST(request: Request) {
|
||||
|
||||
@@ -2,6 +2,8 @@ import { NextResponse } from 'next/server'
|
||||
import { Logger } from '@/lib/logs/console-logger'
|
||||
import { getJiraCloudId } from '@/tools/jira/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = new Logger('jira_issues')
|
||||
|
||||
export async function POST(request: Request) {
|
||||
|
||||
@@ -2,6 +2,8 @@ import { NextResponse } from 'next/server'
|
||||
import { Logger } from '@/lib/logs/console-logger'
|
||||
import { getJiraCloudId } from '@/tools/jira/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = new Logger('jira_projects')
|
||||
|
||||
export async function GET(request: Request) {
|
||||
|
||||
@@ -2,6 +2,8 @@ import { NextRequest, NextResponse } from 'next/server'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { getCredential, getUserId, refreshTokenIfNeeded } from '../utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('OAuthTokenAPI')
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,8 @@ import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/lib/auth'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('PasswordReset')
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
|
||||
@@ -3,6 +3,8 @@ import { Logger } from '@/lib/logs/console-logger'
|
||||
import { markWaitlistUserAsSignedUp } from '@/lib/waitlist/service'
|
||||
import { verifyToken } from '@/lib/waitlist/token'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = new Logger('VerifyWaitlistToken')
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { NextRequest } from 'next/server'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { z } from 'zod'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { env } from '@/lib/env'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { getBaseDomain } from '@/lib/urls/utils'
|
||||
import { encryptSecret } from '@/lib/utils'
|
||||
@@ -70,7 +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 = process.env.NODE_ENV === 'development'
|
||||
const isDevelopment = env.NODE_ENV === 'development'
|
||||
|
||||
const chatUrl = isDevelopment
|
||||
? `http://${chatInstance[0].subdomain}.${getBaseDomain()}`
|
||||
@@ -220,7 +221,7 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
|
||||
|
||||
const updatedSubdomain = subdomain || existingChat[0].subdomain
|
||||
|
||||
const isDevelopment = process.env.NODE_ENV === 'development'
|
||||
const isDevelopment = env.NODE_ENV === 'development'
|
||||
|
||||
const chatUrl = isDevelopment
|
||||
? `http://${updatedSubdomain}.${getBaseDomain()}`
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { NextRequest } from 'next/server'
|
||||
import { env } from '@/lib/env'
|
||||
|
||||
describe('Chat API Route', () => {
|
||||
const mockSelect = vi.fn()
|
||||
@@ -273,7 +274,7 @@ describe('Chat API Route', () => {
|
||||
vi.stubGlobal('process', {
|
||||
...process,
|
||||
env: {
|
||||
...process.env,
|
||||
...env,
|
||||
NODE_ENV: 'development',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -3,6 +3,7 @@ import { and, eq } from 'drizzle-orm'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { z } from 'zod'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { env } from '@/lib/env'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { encryptSecret } from '@/lib/utils'
|
||||
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
|
||||
@@ -168,7 +169,7 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
// Return successful response with chat URL
|
||||
// Check if we're in development or production
|
||||
const isDevelopment = process.env.NODE_ENV === 'development'
|
||||
const isDevelopment = env.NODE_ENV === 'development'
|
||||
const chatUrl = isDevelopment
|
||||
? `http://${subdomain}.localhost:3000`
|
||||
: `https://${subdomain}.simstudio.ai`
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { env } from '@/lib/env'
|
||||
|
||||
describe('Chat API Utils', () => {
|
||||
beforeEach(() => {
|
||||
@@ -22,7 +23,7 @@ describe('Chat API Utils', () => {
|
||||
vi.stubGlobal('process', {
|
||||
...process,
|
||||
env: {
|
||||
...process.env,
|
||||
...env,
|
||||
NODE_ENV: 'development',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { eq, sql } from 'drizzle-orm'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { env } from '@/lib/env'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { persistExecutionLogs } from '@/lib/logs/execution-logger'
|
||||
import { buildTraceSpans } from '@/lib/logs/trace-spans'
|
||||
@@ -18,7 +19,7 @@ declare global {
|
||||
}
|
||||
|
||||
const logger = createLogger('ChatAuthUtils')
|
||||
const isDevelopment = process.env.NODE_ENV === 'development'
|
||||
const isDevelopment = env.NODE_ENV === 'development'
|
||||
|
||||
// Simple encryption for the auth token
|
||||
export const encryptAuthToken = (subdomainId: string, type: string): string => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { unstable_noStore as noStore } from 'next/cache'
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import OpenAI from 'openai'
|
||||
import { env } from '@/lib/env'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
@@ -9,13 +10,13 @@ export const maxDuration = 60
|
||||
|
||||
const logger = createLogger('GenerateCodeAPI')
|
||||
|
||||
const openai = process.env.OPENAI_API_KEY
|
||||
const openai = env.OPENAI_API_KEY
|
||||
? new OpenAI({
|
||||
apiKey: process.env.OPENAI_API_KEY,
|
||||
apiKey: env.OPENAI_API_KEY,
|
||||
})
|
||||
: null
|
||||
|
||||
if (!process.env.OPENAI_API_KEY) {
|
||||
if (!env.OPENAI_API_KEY) {
|
||||
logger.warn('OPENAI_API_KEY not found. Code generation API will not function.')
|
||||
}
|
||||
|
||||
@@ -222,7 +223,7 @@ Example 3 (Array Input):
|
||||
Generate ONLY the raw body of a JavaScript function based on the user's request.
|
||||
The code should be executable within an 'async function(params, environmentVariables) {...}' context.
|
||||
- 'params' (object): Contains input parameters derived from the JSON schema. Access these directly using the parameter name wrapped in angle brackets, e.g., '<paramName>'. Do NOT use 'params.paramName'.
|
||||
- 'environmentVariables' (object): Contains environment variables. Reference these using the double curly brace syntax: '{{ENV_VAR_NAME}}'. Do NOT use 'environmentVariables.VAR_NAME' or process.env.
|
||||
- 'environmentVariables' (object): Contains environment variables. Reference these using the double curly brace syntax: '{{ENV_VAR_NAME}}'. Do NOT use 'environmentVariables.VAR_NAME' or env.
|
||||
|
||||
IMPORTANT FORMATTING RULES:
|
||||
1. Reference Environment Variables: Use the exact syntax {{VARIABLE_NAME}}. Do NOT wrap it in quotes (e.g., use 'apiKey = {{SERVICE_API_KEY}}' not 'apiKey = "{{SERVICE_API_KEY}}"'). Our system replaces these placeholders before execution.
|
||||
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
isS3Path,
|
||||
} from '../utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('FilesDeleteAPI')
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,6 +11,8 @@ import { downloadFromS3 } from '@/lib/uploads/s3-client'
|
||||
import { UPLOAD_DIR, USE_S3_STORAGE } from '@/lib/uploads/setup'
|
||||
import '@/lib/uploads/setup.server'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('FilesParseAPI')
|
||||
|
||||
// Constants for URL downloads
|
||||
|
||||
@@ -3,7 +3,7 @@ import { PutObjectCommand } from '@aws-sdk/client-s3'
|
||||
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { s3Client } from '@/lib/uploads/s3-client'
|
||||
import { getS3Client } from '@/lib/uploads/s3-client'
|
||||
import { S3_CONFIG, USE_S3_STORAGE } from '@/lib/uploads/setup'
|
||||
import { createErrorResponse, createOptionsResponse } from '../utils'
|
||||
|
||||
@@ -52,7 +52,7 @@ export async function POST(request: NextRequest) {
|
||||
})
|
||||
|
||||
// Generate the presigned URL
|
||||
const presignedUrl = await getSignedUrl(s3Client, command, { expiresIn: 3600 })
|
||||
const presignedUrl = await getSignedUrl(getS3Client(), command, { expiresIn: 3600 })
|
||||
|
||||
// Create a path for API to serve the file
|
||||
const servePath = `/api/files/serve/s3/${encodeURIComponent(uniqueKey)}`
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
getContentType,
|
||||
} from '../../utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('FilesServeAPI')
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,6 +9,8 @@ import { UPLOAD_DIR, USE_S3_STORAGE } from '@/lib/uploads/setup'
|
||||
import '@/lib/uploads/setup.server'
|
||||
import { createErrorResponse, createOptionsResponse, InvalidRequestError } from '../utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('FilesUploadAPI')
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { FreestyleSandboxes } from 'freestyle-sandboxes'
|
||||
import { createContext, Script } from 'vm'
|
||||
import { env } from '@/lib/env'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
|
||||
// Explicitly export allowed methods
|
||||
@@ -27,8 +28,8 @@ function resolveCodeVariables(
|
||||
const envVarMatches = resolvedCode.match(/\{\{([^}]+)\}\}/g) || []
|
||||
for (const match of envVarMatches) {
|
||||
const varName = match.slice(2, -2).trim()
|
||||
// Priority: 1. Environment variables from workflow, 2. Params, 3. process.env
|
||||
const varValue = envVars[varName] || params[varName] || process.env[varName] || ''
|
||||
// Priority: 1. Environment variables from workflow, 2. Params
|
||||
const varValue = envVars[varName] || params[varName] || ''
|
||||
// Wrap the value in quotes to ensure it's treated as a string literal
|
||||
resolvedCode = resolvedCode.replace(match, JSON.stringify(varValue))
|
||||
}
|
||||
@@ -72,7 +73,7 @@ export async function POST(req: NextRequest) {
|
||||
let executionMethod = 'vm' // Default execution method
|
||||
|
||||
// Try to use Freestyle if the API key is available
|
||||
if (process.env.FREESTYLE_API_KEY) {
|
||||
if (env.FREESTYLE_API_KEY) {
|
||||
try {
|
||||
logger.info(`[${requestId}] Using Freestyle for code execution`)
|
||||
executionMethod = 'freestyle'
|
||||
@@ -99,7 +100,7 @@ export async function POST(req: NextRequest) {
|
||||
}
|
||||
|
||||
const freestyle = new FreestyleSandboxes({
|
||||
apiKey: process.env.FREESTYLE_API_KEY,
|
||||
apiKey: env.FREESTYLE_API_KEY,
|
||||
})
|
||||
|
||||
// Wrap code in export default to match Freestyle's expectations
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { Resend } from 'resend'
|
||||
import { z } from 'zod'
|
||||
import { env } from '@/lib/env'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
|
||||
const resend = process.env.RESEND_API_KEY ? new Resend(process.env.RESEND_API_KEY) : null
|
||||
const resend = env.RESEND_API_KEY ? new Resend(env.RESEND_API_KEY) : null
|
||||
const logger = createLogger('HelpAPI')
|
||||
|
||||
// Define schema for validation
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { PutObjectCommand } from '@aws-sdk/client-s3'
|
||||
import { and, eq, inArray, lt, sql } from 'drizzle-orm'
|
||||
import { env } from '@/lib/env'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { s3Client } from '@/lib/uploads/s3-client'
|
||||
import { getS3Client } from '@/lib/uploads/s3-client'
|
||||
import { db } from '@/db'
|
||||
import { subscription, user, workflow, workflowLogs } from '@/db/schema'
|
||||
|
||||
@@ -12,19 +13,19 @@ const logger = createLogger('LogsCleanup')
|
||||
|
||||
const BATCH_SIZE = 2000
|
||||
const S3_CONFIG = {
|
||||
bucket: process.env.S3_LOGS_BUCKET_NAME || '',
|
||||
region: process.env.AWS_REGION || '',
|
||||
bucket: env.S3_LOGS_BUCKET_NAME || '',
|
||||
region: env.AWS_REGION || '',
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const authHeader = request.headers.get('authorization')
|
||||
|
||||
if (!process.env.CRON_SECRET) {
|
||||
if (!env.CRON_SECRET) {
|
||||
return new NextResponse('Configuration error: Cron secret is not set', { status: 500 })
|
||||
}
|
||||
|
||||
if (!authHeader || authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
|
||||
if (!authHeader || authHeader !== `Bearer ${env.CRON_SECRET}`) {
|
||||
logger.warn(`Unauthorized access attempt to logs cleanup endpoint`)
|
||||
return new NextResponse('Unauthorized', { status: 401 })
|
||||
}
|
||||
@@ -34,9 +35,7 @@ export async function GET(request: Request) {
|
||||
}
|
||||
|
||||
const retentionDate = new Date()
|
||||
retentionDate.setDate(
|
||||
retentionDate.getDate() - Number(process.env.FREE_PLAN_LOG_RETENTION_DAYS || '7')
|
||||
)
|
||||
retentionDate.setDate(retentionDate.getDate() - Number(env.FREE_PLAN_LOG_RETENTION_DAYS || '7'))
|
||||
|
||||
const freeUsers = await db
|
||||
.select({ userId: user.id })
|
||||
@@ -111,7 +110,7 @@ export async function GET(request: Request) {
|
||||
const logData = JSON.stringify(log)
|
||||
|
||||
try {
|
||||
await s3Client.send(
|
||||
await getS3Client().send(
|
||||
new PutObjectCommand({
|
||||
Bucket: S3_CONFIG.bucket,
|
||||
Key: logKey,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { env } from '@/lib/env'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
|
||||
const logger = createLogger('TelemetryAPI')
|
||||
@@ -84,8 +85,8 @@ async function forwardToCollector(data: any): Promise<boolean> {
|
||||
return false
|
||||
}
|
||||
|
||||
const endpoint = process.env.TELEMETRY_ENDPOINT || 'https://telemetry.simstudio.ai/v1/traces'
|
||||
const timeout = parseInt(process.env.TELEMETRY_TIMEOUT || '') || DEFAULT_TIMEOUT
|
||||
const endpoint = env.TELEMETRY_ENDPOINT || 'https://telemetry.simstudio.ai/v1/traces'
|
||||
const timeout = DEFAULT_TIMEOUT
|
||||
|
||||
try {
|
||||
const timestamp = Date.now() * 1000000
|
||||
@@ -96,11 +97,11 @@ async function forwardToCollector(data: any): Promise<boolean> {
|
||||
{ key: 'service.name', value: { stringValue: 'sim-studio' } },
|
||||
{
|
||||
key: 'service.version',
|
||||
value: { stringValue: process.env.NEXT_PUBLIC_APP_VERSION || '0.1.0' },
|
||||
value: { stringValue: '0.1.0' },
|
||||
},
|
||||
{
|
||||
key: 'deployment.environment',
|
||||
value: { stringValue: process.env.NODE_ENV || 'production' },
|
||||
value: { stringValue: env.NODE_ENV || 'production' },
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { Stagehand } from '@browserbasehq/stagehand'
|
||||
import { z } from 'zod'
|
||||
import { env } from '@/lib/env'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { ensureZodObject, normalizeUrl } from '../utils'
|
||||
|
||||
const logger = createLogger('StagehandAgentAPI')
|
||||
|
||||
const BROWSERBASE_API_KEY = process.env.BROWSERBASE_API_KEY
|
||||
const BROWSERBASE_PROJECT_ID = process.env.BROWSERBASE_PROJECT_ID
|
||||
// Environment variables for Browserbase
|
||||
const BROWSERBASE_API_KEY = env.BROWSERBASE_API_KEY
|
||||
const BROWSERBASE_PROJECT_ID = env.BROWSERBASE_PROJECT_ID
|
||||
|
||||
const requestSchema = z.object({
|
||||
task: z.string().min(1),
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { Stagehand } from '@browserbasehq/stagehand'
|
||||
import { z } from 'zod'
|
||||
import { env } from '@/lib/env'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { ensureZodObject, normalizeUrl } from '../utils'
|
||||
|
||||
const logger = createLogger('StagehandExtractAPI')
|
||||
|
||||
const BROWSERBASE_API_KEY = process.env.BROWSERBASE_API_KEY
|
||||
const BROWSERBASE_PROJECT_ID = process.env.BROWSERBASE_PROJECT_ID
|
||||
// Environment variables for Browserbase
|
||||
const BROWSERBASE_API_KEY = env.BROWSERBASE_API_KEY
|
||||
const BROWSERBASE_PROJECT_ID = env.BROWSERBASE_PROJECT_ID
|
||||
|
||||
const requestSchema = z.object({
|
||||
instruction: z.string(),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { env } from '@/lib/env'
|
||||
import { Logger } from '@/lib/logs/console-logger'
|
||||
import { acquireLock, releaseLock } from '@/lib/redis'
|
||||
import { pollGmailWebhooks } from '@/lib/webhooks/gmail-polling-service'
|
||||
@@ -20,7 +21,7 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
try {
|
||||
const authHeader = request.headers.get('authorization')
|
||||
const webhookSecret = process.env.CRON_SECRET || process.env.WEBHOOK_POLLING_SECRET
|
||||
const webhookSecret = env.CRON_SECRET
|
||||
|
||||
if (!webhookSecret) {
|
||||
return new NextResponse('Configuration error: Webhook secret is not set', { status: 500 })
|
||||
|
||||
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { env } from '@/lib/env'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { db } from '@/db'
|
||||
import { webhook, workflow } from '@/db/schema'
|
||||
@@ -254,7 +255,7 @@ async function createAirtableWebhookSubscription(
|
||||
const requestOrigin = new URL(request.url).origin
|
||||
// Ensure origin does not point to localhost for external API calls
|
||||
const effectiveOrigin = requestOrigin.includes('localhost')
|
||||
? process.env.NEXT_PUBLIC_APP_URL || requestOrigin // Use env var if available, fallback to original
|
||||
? env.NEXT_PUBLIC_APP_URL || requestOrigin // Use env var if available, fallback to original
|
||||
: requestOrigin
|
||||
|
||||
const notificationUrl = `${effectiveOrigin}/api/webhooks/trigger/${path}`
|
||||
@@ -366,7 +367,7 @@ async function createTelegramWebhookSubscription(
|
||||
const requestOrigin = new URL(request.url).origin
|
||||
// Ensure origin does not point to localhost for external API calls
|
||||
const effectiveOrigin = requestOrigin.includes('localhost')
|
||||
? process.env.NEXT_PUBLIC_APP_URL || requestOrigin // Use env var if available, fallback to original
|
||||
? env.NEXT_PUBLIC_APP_URL || requestOrigin // Use env var if available, fallback to original
|
||||
: requestOrigin
|
||||
|
||||
const notificationUrl = `${effectiveOrigin}/api/webhooks/trigger/${path}`
|
||||
|
||||
@@ -113,7 +113,7 @@ export default function ChatClient({ subdomain }: { subdomain: string }) {
|
||||
// Fetch chat config function
|
||||
const fetchChatConfig = async () => {
|
||||
try {
|
||||
// Use relative URL instead of absolute URL with process.env.NEXT_PUBLIC_APP_URL
|
||||
// Use relative URL instead of absolute URL with env.NEXT_PUBLIC_APP_URL
|
||||
const response = await fetch(`/api/chat/${subdomain}`, {
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Metadata, Viewport } from 'next'
|
||||
import { Analytics } from '@vercel/analytics/next'
|
||||
import { SpeedInsights } from '@vercel/speed-insights/next'
|
||||
import { PublicEnvScript } from 'next-runtime-env'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { TelemetryConsentDialog } from '@/app/telemetry-consent-dialog'
|
||||
import './globals.css'
|
||||
@@ -8,7 +9,6 @@ import { ZoomPrevention } from './zoom-prevention'
|
||||
|
||||
const logger = createLogger('RootLayout')
|
||||
|
||||
// Add browser extension attributes that we want to ignore
|
||||
const BROWSER_EXTENSION_ATTRIBUTES = [
|
||||
'data-new-gr-c-s-check-loaded',
|
||||
'data-gr-ext-installed',
|
||||
@@ -16,7 +16,6 @@ const BROWSER_EXTENSION_ATTRIBUTES = [
|
||||
'data-grammarly',
|
||||
'data-fgm',
|
||||
'data-lt-installed',
|
||||
// Add other known extension attributes here
|
||||
]
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
@@ -156,6 +155,8 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
||||
<meta property="og:image" content="https://simstudio.ai/social/instagram.png" />
|
||||
<meta property="og:image:width" content="1080" />
|
||||
<meta property="og:image:height" content="1080" />
|
||||
|
||||
<PublicEnvScript />
|
||||
</head>
|
||||
<body suppressHydrationWarning>
|
||||
<ZoomPrevention />
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { getNodeEnv } from '@/lib/environment'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { useGeneralStore } from '@/stores/settings/general/store'
|
||||
|
||||
@@ -49,7 +50,7 @@ export function TelemetryConsentDialog() {
|
||||
const loadSettings = useGeneralStore((state) => state.loadSettings)
|
||||
|
||||
const hasShownDialogThisSession = useRef(false)
|
||||
const isDevelopment = process.env.NODE_ENV === 'development'
|
||||
const isDevelopment = getNodeEnv() === 'development'
|
||||
|
||||
// Check localStorage for saved preferences
|
||||
useEffect(() => {
|
||||
@@ -101,7 +102,7 @@ export function TelemetryConsentDialog() {
|
||||
telemetryNotifiedUser,
|
||||
telemetryEnabled,
|
||||
hasShownInSession: hasShownDialogThisSession.current,
|
||||
environment: process.env.NODE_ENV,
|
||||
environment: getNodeEnv(),
|
||||
})
|
||||
|
||||
const localStorageNotified =
|
||||
|
||||
@@ -30,6 +30,8 @@ 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 { getNodeEnv } from '@/lib/environment'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { getBaseDomain } from '@/lib/urls/utils'
|
||||
import { cn } from '@/lib/utils'
|
||||
@@ -53,7 +55,7 @@ interface ChatDeployProps {
|
||||
|
||||
type AuthType = 'public' | 'password' | 'email'
|
||||
|
||||
const isDevelopment = process.env.NODE_ENV === 'development'
|
||||
const isDevelopment = getNodeEnv() === 'development'
|
||||
|
||||
const getDomainSuffix = (() => {
|
||||
const suffix = isDevelopment ? `.${getBaseDomain()}` : '.simstudio.ai'
|
||||
|
||||
@@ -18,6 +18,7 @@ import { Card, CardContent } from '@/components/ui/card'
|
||||
import { CopyButton } from '@/components/ui/copy-button'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { TabsContent } from '@/components/ui/tabs'
|
||||
import { env } from '@/lib/env'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useNotificationStore } from '@/stores/notifications/store'
|
||||
@@ -217,7 +218,7 @@ export function DeployModal({
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const endpoint = `${process.env.NEXT_PUBLIC_APP_URL}/api/workflows/${workflowId}/execute`
|
||||
const endpoint = `${env.NEXT_PUBLIC_APP_URL}/api/workflows/${workflowId}/execute`
|
||||
const inputFormatExample = getInputFormatExample()
|
||||
|
||||
setDeploymentInfo({
|
||||
@@ -278,7 +279,7 @@ export function DeployModal({
|
||||
setNeedsRedeployment(false)
|
||||
|
||||
// Update the local deployment info
|
||||
const endpoint = `${process.env.NEXT_PUBLIC_APP_URL}/api/workflows/${workflowId}/execute`
|
||||
const endpoint = `${env.NEXT_PUBLIC_APP_URL}/api/workflows/${workflowId}/execute`
|
||||
const inputFormatExample = getInputFormatExample()
|
||||
|
||||
const newDeploymentInfo = {
|
||||
@@ -603,7 +604,7 @@ export function DeployModal({
|
||||
<DeployForm
|
||||
apiKeys={apiKeys}
|
||||
keysLoaded={keysLoaded}
|
||||
endpointUrl={`${process.env.NEXT_PUBLIC_APP_URL}/api/workflows/${workflowId}/execute`}
|
||||
endpointUrl={`${env.NEXT_PUBLIC_APP_URL}/api/workflows/${workflowId}/execute`}
|
||||
workflowId={workflowId || ''}
|
||||
onSubmit={onDeploy}
|
||||
getInputFormatExample={getInputFormatExample}
|
||||
|
||||
@@ -286,18 +286,6 @@ export function ControlBar() {
|
||||
async function checkStatus() {
|
||||
if (!activeWorkflowId) return
|
||||
|
||||
// Skip API call in localStorage mode
|
||||
if (
|
||||
typeof window !== 'undefined' &&
|
||||
(localStorage.getItem('USE_LOCAL_STORAGE') === 'true' ||
|
||||
process.env.NEXT_PUBLIC_USE_LOCAL_STORAGE === 'true' ||
|
||||
process.env.NEXT_PUBLIC_DISABLE_DB_SYNC === 'true')
|
||||
) {
|
||||
// For localStorage mode, we already have the status in the workflow store
|
||||
// Nothing more to do as the useWorkflowStore already has this information
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/workflows/${activeWorkflowId}/status`)
|
||||
if (response.ok) {
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
CommandList,
|
||||
} from '@/components/ui/command'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import { env } from '@/lib/env'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import {
|
||||
Credential,
|
||||
@@ -248,7 +249,7 @@ export function GoogleDrivePicker({
|
||||
showUploadFolders: true,
|
||||
supportDrives: true,
|
||||
multiselect: false,
|
||||
appId: process.env.NEXT_PUBLIC_GOOGLE_PROJECT_NUMBER,
|
||||
appId: env.NEXT_PUBLIC_GOOGLE_PROJECT_NUMBER,
|
||||
// Enable folder selection when mimeType is folder
|
||||
setSelectFolderEnabled: mimeTypeFilter?.includes('folder') ? true : false,
|
||||
callbackFunction: (data) => {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { env } from '@/lib/env'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
import { SubBlockConfig } from '@/blocks/types'
|
||||
import { ConfluenceFileInfo, ConfluenceFileSelector } from './components/confluence-file-selector'
|
||||
@@ -80,8 +81,8 @@ export function FileSelectorInput({ blockId, subBlock, disabled = false }: FileS
|
||||
}
|
||||
|
||||
// For Google Drive
|
||||
const clientId = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || ''
|
||||
const apiKey = process.env.NEXT_PUBLIC_GOOGLE_API_KEY || ''
|
||||
const clientId = env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || ''
|
||||
const apiKey = env.NEXT_PUBLIC_GOOGLE_API_KEY || ''
|
||||
|
||||
// Render Discord channel selector
|
||||
if (isDiscord) {
|
||||
|
||||
@@ -6,7 +6,12 @@ import { BlockConfig, SubBlockConfig, SubBlockLayout, SubBlockType } from '../ty
|
||||
|
||||
const logger = createLogger('FileBlock')
|
||||
|
||||
const isS3Enabled = process.env.USE_S3 === 'true'
|
||||
// Create a safe client-only env subset to avoid server-side env access errors
|
||||
const clientEnv = {
|
||||
USE_S3: process.env.USE_S3,
|
||||
}
|
||||
|
||||
const isS3Enabled = clientEnv.USE_S3
|
||||
const shouldEnableURLInput = isProd || isS3Enabled
|
||||
|
||||
// Define sub-blocks conditionally
|
||||
|
||||
@@ -3,7 +3,12 @@ import { isProd } from '@/lib/environment'
|
||||
import { MistralParserOutput } from '@/tools/mistral/types'
|
||||
import { BlockConfig, SubBlockConfig, SubBlockLayout, SubBlockType } from '../types'
|
||||
|
||||
const isS3Enabled = process.env.USE_S3 === 'true'
|
||||
// Create a safe client-only env subset to avoid server-side env access errors
|
||||
const clientEnv = {
|
||||
USE_S3: process.env.USE_S3,
|
||||
}
|
||||
|
||||
const isS3Enabled = clientEnv.USE_S3
|
||||
const shouldEnableFileUpload = isProd || isS3Enabled
|
||||
|
||||
// Define the input method selector block when needed
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import * as React from 'react'
|
||||
import { Container, Img, Link, Section, Text } from '@react-email/components'
|
||||
import { env } from '@/lib/env'
|
||||
|
||||
interface EmailFooterProps {
|
||||
baseUrl?: string
|
||||
}
|
||||
|
||||
export const EmailFooter = ({
|
||||
baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'https://simstudio.ai',
|
||||
baseUrl = env.NEXT_PUBLIC_APP_URL || 'https://simstudio.ai',
|
||||
}: EmailFooterProps) => {
|
||||
return (
|
||||
<Container>
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
Text,
|
||||
} from '@react-email/components'
|
||||
import { format } from 'date-fns'
|
||||
import { env } from '@/lib/env'
|
||||
import { baseStyles } from './base-styles'
|
||||
import EmailFooter from './footer'
|
||||
|
||||
@@ -24,7 +25,7 @@ interface InvitationEmailProps {
|
||||
updatedDate?: Date
|
||||
}
|
||||
|
||||
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'https://simstudio.ai'
|
||||
const baseUrl = env.NEXT_PUBLIC_APP_URL || 'https://simstudio.ai'
|
||||
|
||||
export const InvitationEmail = ({
|
||||
inviterName = 'A team member',
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
Section,
|
||||
Text,
|
||||
} from '@react-email/components'
|
||||
import { env } from '@/lib/env'
|
||||
import { baseStyles } from './base-styles'
|
||||
import EmailFooter from './footer'
|
||||
|
||||
@@ -21,7 +22,7 @@ interface OTPVerificationEmailProps {
|
||||
chatTitle?: string
|
||||
}
|
||||
|
||||
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'https://simstudio.ai'
|
||||
const baseUrl = env.NEXT_PUBLIC_APP_URL || 'https://simstudio.ai'
|
||||
|
||||
const getSubjectByType = (type: string, chatTitle?: string) => {
|
||||
switch (type) {
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
Text,
|
||||
} from '@react-email/components'
|
||||
import { format } from 'date-fns'
|
||||
import { env } from '@/lib/env'
|
||||
import { baseStyles } from './base-styles'
|
||||
import EmailFooter from './footer'
|
||||
|
||||
@@ -22,7 +23,7 @@ interface ResetPasswordEmailProps {
|
||||
updatedDate?: Date
|
||||
}
|
||||
|
||||
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'https://simstudio.ai'
|
||||
const baseUrl = env.NEXT_PUBLIC_APP_URL || 'https://simstudio.ai'
|
||||
|
||||
export const ResetPasswordEmail = ({
|
||||
username = '',
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
Section,
|
||||
Text,
|
||||
} from '@react-email/components'
|
||||
import { env } from '@/lib/env'
|
||||
import { baseStyles } from './base-styles'
|
||||
import EmailFooter from './footer'
|
||||
|
||||
@@ -20,7 +21,7 @@ interface WaitlistApprovalEmailProps {
|
||||
signupLink?: string
|
||||
}
|
||||
|
||||
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'https://simstudio.ai'
|
||||
const baseUrl = env.NEXT_PUBLIC_APP_URL || 'https://simstudio.ai'
|
||||
|
||||
export const WaitlistApprovalEmail = ({
|
||||
email = '',
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
Section,
|
||||
Text,
|
||||
} from '@react-email/components'
|
||||
import { env } from '@/lib/env'
|
||||
import { baseStyles } from './base-styles'
|
||||
import EmailFooter from './footer'
|
||||
|
||||
@@ -19,7 +20,7 @@ interface WaitlistConfirmationEmailProps {
|
||||
email?: string
|
||||
}
|
||||
|
||||
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'https://simstudio.ai'
|
||||
const baseUrl = env.NEXT_PUBLIC_APP_URL || 'https://simstudio.ai'
|
||||
const typeformLink = 'https://form.typeform.com/to/jqCO12pF'
|
||||
|
||||
export const WaitlistConfirmationEmail = ({ email = '' }: WaitlistConfirmationEmailProps) => {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { drizzle } from 'drizzle-orm/postgres-js'
|
||||
import postgres from 'postgres'
|
||||
import { env } from '@/lib/env'
|
||||
|
||||
// In production, use the Vercel-generated POSTGRES_URL
|
||||
// In development, use the direct DATABASE_URL
|
||||
const connectionString = process.env.POSTGRES_URL || process.env.DATABASE_URL!
|
||||
const connectionString = env.POSTGRES_URL ?? env.DATABASE_URL
|
||||
|
||||
// Disable prefetch as it is not supported for "Transaction" pool mode
|
||||
const client = postgres(connectionString, {
|
||||
|
||||
@@ -205,13 +205,7 @@
|
||||
"tag": "0028_absent_triton",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 29,
|
||||
"version": "7",
|
||||
"when": 1745211620858,
|
||||
"tag": "0029_grey_barracuda",
|
||||
"breakpoints": true
|
||||
},
|
||||
|
||||
{
|
||||
"idx": 30,
|
||||
"version": "7",
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import * as dotenv from 'dotenv'
|
||||
import type { Config } from 'drizzle-kit'
|
||||
|
||||
dotenv.config({ path: '../../.env' })
|
||||
import { env } from './lib/env'
|
||||
|
||||
export default {
|
||||
schema: './db/schema.ts',
|
||||
out: './db/migrations',
|
||||
dialect: 'postgresql',
|
||||
dbCredentials: {
|
||||
url: process.env.DATABASE_URL!,
|
||||
url: env.DATABASE_URL,
|
||||
},
|
||||
} satisfies Config
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { env } from '@/lib/env'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { getAllBlocks } from '@/blocks'
|
||||
import { BlockOutput } from '@/blocks/types'
|
||||
@@ -218,7 +219,7 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
hasOutgoingConnections,
|
||||
})
|
||||
|
||||
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || ''
|
||||
const baseUrl = env.NEXT_PUBLIC_APP_URL || ''
|
||||
const url = new URL('/api/providers', baseUrl)
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { env } from '@/lib/env'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { BlockOutput } from '@/blocks/types'
|
||||
import { getProviderFromModel } from '@/providers/utils'
|
||||
@@ -102,7 +103,7 @@ export class EvaluatorBlockHandler implements BlockHandler {
|
||||
}
|
||||
|
||||
try {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || ''
|
||||
const baseUrl = env.NEXT_PUBLIC_APP_URL || ''
|
||||
const url = new URL('/api/providers', baseUrl)
|
||||
|
||||
// Make sure we force JSON output in the request
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { env } from '@/lib/env'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { generateRouterPrompt } from '@/blocks/blocks/router'
|
||||
import { BlockOutput } from '@/blocks/types'
|
||||
@@ -38,7 +39,7 @@ export class RouterBlockHandler implements BlockHandler {
|
||||
const providerId = getProviderFromModel(routerConfig.model)
|
||||
|
||||
try {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || ''
|
||||
const baseUrl = env.NEXT_PUBLIC_APP_URL || ''
|
||||
const url = new URL('/api/providers', baseUrl)
|
||||
|
||||
// Create the provider request with proper message formatting
|
||||
|
||||
@@ -19,11 +19,17 @@ import {
|
||||
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,
|
||||
}
|
||||
|
||||
// Only in production
|
||||
if (typeof window !== 'undefined' && process.env.NODE_ENV === 'production') {
|
||||
if (typeof window !== 'undefined' && clientEnv.NODE_ENV === 'production') {
|
||||
const client = new BrowserClient({
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN || undefined,
|
||||
environment: process.env.NODE_ENV || 'development',
|
||||
dsn: clientEnv.NEXT_PUBLIC_SENTRY_DSN || undefined,
|
||||
environment: clientEnv.NODE_ENV || 'development',
|
||||
transport: makeFetchTransport,
|
||||
stackParser: defaultStackParser,
|
||||
integrations: [breadcrumbsIntegration(), dedupeIntegration(), linkedErrorsIntegration()],
|
||||
@@ -40,14 +46,14 @@ if (typeof window !== 'undefined' && process.env.NODE_ENV === 'production') {
|
||||
}
|
||||
|
||||
export const onRouterTransitionStart =
|
||||
process.env.NODE_ENV === 'production' ? captureRouterTransitionStart : () => {}
|
||||
clientEnv.NODE_ENV === 'production' ? captureRouterTransitionStart : () => {}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
const TELEMETRY_STATUS_KEY = 'simstudio-telemetry-status'
|
||||
let telemetryEnabled = true
|
||||
|
||||
try {
|
||||
if (process.env.NEXT_TELEMETRY_DISABLED === '1') {
|
||||
if (clientEnv.NEXT_TELEMETRY_DISABLED === '1') {
|
||||
telemetryEnabled = false
|
||||
} else {
|
||||
const storedPreference = localStorage.getItem(TELEMETRY_STATUS_KEY)
|
||||
|
||||
143
apps/sim/instrumentation-server.ts
Normal file
143
apps/sim/instrumentation-server.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* Sim Studio Telemetry - Server-side Instrumentation
|
||||
*
|
||||
* 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: () => {} }
|
||||
|
||||
const logger = createLogger('OtelInstrumentation')
|
||||
|
||||
const DEFAULT_TELEMETRY_CONFIG = {
|
||||
endpoint: env.TELEMETRY_ENDPOINT || 'https://telemetry.simstudio.ai/v1/traces',
|
||||
serviceName: 'sim-studio',
|
||||
serviceVersion: '0.1.0',
|
||||
serverSide: { enabled: true },
|
||||
batchSettings: {
|
||||
maxQueueSize: 100,
|
||||
maxExportBatchSize: 10,
|
||||
scheduledDelayMillis: 5000,
|
||||
exportTimeoutMillis: 30000,
|
||||
},
|
||||
}
|
||||
|
||||
// Initialize OpenTelemetry
|
||||
async function initializeOpenTelemetry() {
|
||||
try {
|
||||
if (env.NEXT_TELEMETRY_DISABLED === '1') {
|
||||
logger.info('OpenTelemetry telemetry disabled via environment variable')
|
||||
return
|
||||
}
|
||||
|
||||
let telemetryConfig
|
||||
try {
|
||||
// Use dynamic import for ES modules
|
||||
telemetryConfig = (await import('./telemetry.config.ts')).default
|
||||
} catch (e) {
|
||||
telemetryConfig = DEFAULT_TELEMETRY_CONFIG
|
||||
}
|
||||
|
||||
if (telemetryConfig.serverSide?.enabled === false) {
|
||||
logger.info('Server-side OpenTelemetry instrumentation is disabled in config')
|
||||
return
|
||||
}
|
||||
|
||||
// Dynamic imports for server-side libraries
|
||||
const { NodeSDK } = await import('@opentelemetry/sdk-node')
|
||||
const { resourceFromAttributes } = await import('@opentelemetry/resources')
|
||||
const { SemanticResourceAttributes } = await import('@opentelemetry/semantic-conventions')
|
||||
const { BatchSpanProcessor } = await import('@opentelemetry/sdk-trace-node')
|
||||
const { OTLPTraceExporter } = await import('@opentelemetry/exporter-trace-otlp-http')
|
||||
|
||||
const exporter = new OTLPTraceExporter({
|
||||
url: telemetryConfig.endpoint,
|
||||
})
|
||||
|
||||
const spanProcessor = new BatchSpanProcessor(exporter, {
|
||||
maxQueueSize:
|
||||
telemetryConfig.batchSettings?.maxQueueSize ||
|
||||
DEFAULT_TELEMETRY_CONFIG.batchSettings.maxQueueSize,
|
||||
maxExportBatchSize:
|
||||
telemetryConfig.batchSettings?.maxExportBatchSize ||
|
||||
DEFAULT_TELEMETRY_CONFIG.batchSettings.maxExportBatchSize,
|
||||
scheduledDelayMillis:
|
||||
telemetryConfig.batchSettings?.scheduledDelayMillis ||
|
||||
DEFAULT_TELEMETRY_CONFIG.batchSettings.scheduledDelayMillis,
|
||||
exportTimeoutMillis:
|
||||
telemetryConfig.batchSettings?.exportTimeoutMillis ||
|
||||
DEFAULT_TELEMETRY_CONFIG.batchSettings.exportTimeoutMillis,
|
||||
})
|
||||
|
||||
const configResource = resourceFromAttributes({
|
||||
[SemanticResourceAttributes.SERVICE_NAME]: telemetryConfig.serviceName,
|
||||
[SemanticResourceAttributes.SERVICE_VERSION]: telemetryConfig.serviceVersion,
|
||||
[SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: env.NODE_ENV,
|
||||
})
|
||||
|
||||
const sdk = new NodeSDK({
|
||||
resource: configResource,
|
||||
spanProcessors: [spanProcessor],
|
||||
})
|
||||
|
||||
sdk.start()
|
||||
|
||||
const shutdownHandler = async () => {
|
||||
await sdk
|
||||
.shutdown()
|
||||
.then(() => logger.info('OpenTelemetry SDK shut down successfully'))
|
||||
.catch((err) => logger.error('Error shutting down OpenTelemetry SDK', err))
|
||||
}
|
||||
|
||||
process.on('SIGTERM', shutdownHandler)
|
||||
process.on('SIGINT', shutdownHandler)
|
||||
|
||||
logger.info('OpenTelemetry instrumentation initialized for server-side telemetry')
|
||||
} catch (error) {
|
||||
logger.error('Failed to initialize OpenTelemetry instrumentation', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function initializeSentry() {
|
||||
if (env.NODE_ENV !== 'production') return
|
||||
|
||||
try {
|
||||
const Sentry = await import('@sentry/nextjs')
|
||||
|
||||
// Skip initialization if Sentry appears to be already configured
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore accessing internal API
|
||||
if ((Sentry as any).Hub?.current?.getClient()) {
|
||||
logger.debug('Sentry already initialized, skipping duplicate init')
|
||||
return
|
||||
}
|
||||
|
||||
Sentry.init({
|
||||
dsn: env.NEXT_PUBLIC_SENTRY_DSN || undefined,
|
||||
enabled: true,
|
||||
environment: env.NODE_ENV || 'development',
|
||||
tracesSampleRate: 0.2,
|
||||
beforeSend(event) {
|
||||
if (event.request && typeof event.request === 'object') {
|
||||
;(event.request as any).ip = null
|
||||
}
|
||||
return event
|
||||
},
|
||||
})
|
||||
|
||||
logger.info('Sentry initialized (server-side)')
|
||||
} catch (error) {
|
||||
logger.error('Failed to initialize Sentry', error as Error)
|
||||
}
|
||||
}
|
||||
|
||||
export async function register() {
|
||||
await initializeSentry()
|
||||
await initializeOpenTelemetry()
|
||||
}
|
||||
|
||||
export const onRequestError = Sentry.captureRequestError
|
||||
@@ -1,122 +1,9 @@
|
||||
/**
|
||||
* Sim Studio Telemetry - Server-side Instrumentation
|
||||
*
|
||||
* This file can be customized in forked repositories:
|
||||
* - Set TELEMETRY_ENDPOINT env var to your collector
|
||||
* - Modify exporter configuration as needed
|
||||
*
|
||||
* Please maintain ethical telemetry practices if modified.
|
||||
*/
|
||||
// This file enables OpenTelemetry instrumentation for Next.js
|
||||
// See: https://nextjs.org/docs/app/building-your-application/optimizing/open-telemetry
|
||||
// Set experimental.instrumentationHook = true in next.config.ts to enable this
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
|
||||
const Sentry =
|
||||
process.env.NODE_ENV === 'production'
|
||||
? require('@sentry/nextjs')
|
||||
: { captureRequestError: () => {} }
|
||||
|
||||
const logger = createLogger('OtelInstrumentation')
|
||||
|
||||
const DEFAULT_TELEMETRY_CONFIG = {
|
||||
endpoint: process.env.TELEMETRY_ENDPOINT || 'https://telemetry.simstudio.ai/v1/traces',
|
||||
serviceName: 'sim-studio',
|
||||
serviceVersion: process.env.NEXT_PUBLIC_APP_VERSION || '0.1.0',
|
||||
serverSide: { enabled: true },
|
||||
batchSettings: {
|
||||
maxQueueSize: 100,
|
||||
maxExportBatchSize: 10,
|
||||
scheduledDelayMillis: 5000,
|
||||
exportTimeoutMillis: 30000,
|
||||
},
|
||||
}
|
||||
|
||||
export async function register() {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
||||
await import('./sentry.server.config')
|
||||
}
|
||||
|
||||
if (process.env.NEXT_RUNTIME === 'edge') {
|
||||
await import('./sentry.edge.config')
|
||||
}
|
||||
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
||||
await import('./instrumentation-server')
|
||||
}
|
||||
|
||||
// OpenTelemetry instrumentation
|
||||
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
||||
try {
|
||||
if (process.env.NEXT_TELEMETRY_DISABLED === '1') {
|
||||
logger.info('OpenTelemetry telemetry disabled via environment variable')
|
||||
return
|
||||
}
|
||||
|
||||
let telemetryConfig
|
||||
try {
|
||||
// Use dynamic import instead of require for ES modules
|
||||
telemetryConfig = (await import('./telemetry.config.ts')).default
|
||||
} catch (e) {
|
||||
telemetryConfig = DEFAULT_TELEMETRY_CONFIG
|
||||
}
|
||||
|
||||
if (telemetryConfig.serverSide?.enabled === false) {
|
||||
logger.info('Server-side OpenTelemetry instrumentation is disabled in config')
|
||||
return
|
||||
}
|
||||
|
||||
const { NodeSDK } = await import('@opentelemetry/sdk-node')
|
||||
const { resourceFromAttributes } = await import('@opentelemetry/resources')
|
||||
const { SemanticResourceAttributes } = await import('@opentelemetry/semantic-conventions')
|
||||
const { BatchSpanProcessor } = await import('@opentelemetry/sdk-trace-node')
|
||||
const { OTLPTraceExporter } = await import('@opentelemetry/exporter-trace-otlp-http')
|
||||
|
||||
const exporter = new OTLPTraceExporter({
|
||||
url: telemetryConfig.endpoint,
|
||||
})
|
||||
|
||||
const spanProcessor = new BatchSpanProcessor(exporter, {
|
||||
maxQueueSize:
|
||||
telemetryConfig.batchSettings?.maxQueueSize ||
|
||||
DEFAULT_TELEMETRY_CONFIG.batchSettings.maxQueueSize,
|
||||
maxExportBatchSize:
|
||||
telemetryConfig.batchSettings?.maxExportBatchSize ||
|
||||
DEFAULT_TELEMETRY_CONFIG.batchSettings.maxExportBatchSize,
|
||||
scheduledDelayMillis:
|
||||
telemetryConfig.batchSettings?.scheduledDelayMillis ||
|
||||
DEFAULT_TELEMETRY_CONFIG.batchSettings.scheduledDelayMillis,
|
||||
exportTimeoutMillis:
|
||||
telemetryConfig.batchSettings?.exportTimeoutMillis ||
|
||||
DEFAULT_TELEMETRY_CONFIG.batchSettings.exportTimeoutMillis,
|
||||
})
|
||||
|
||||
const configResource = resourceFromAttributes({
|
||||
[SemanticResourceAttributes.SERVICE_NAME]: telemetryConfig.serviceName,
|
||||
[SemanticResourceAttributes.SERVICE_VERSION]: telemetryConfig.serviceVersion,
|
||||
[SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: process.env.NODE_ENV,
|
||||
})
|
||||
|
||||
const sdk = new NodeSDK({
|
||||
resource: configResource,
|
||||
spanProcessors: [spanProcessor],
|
||||
})
|
||||
|
||||
sdk.start()
|
||||
|
||||
const shutdownHandler = async () => {
|
||||
await sdk
|
||||
.shutdown()
|
||||
.then(() => logger.info('OpenTelemetry SDK shut down successfully'))
|
||||
.catch((err) => logger.error('Error shutting down OpenTelemetry SDK', err))
|
||||
}
|
||||
|
||||
process.on('SIGTERM', shutdownHandler)
|
||||
process.on('SIGINT', shutdownHandler)
|
||||
|
||||
logger.info('OpenTelemetry instrumentation initialized for server-side telemetry')
|
||||
} catch (error) {
|
||||
logger.error('Failed to initialize OpenTelemetry instrumentation', error)
|
||||
}
|
||||
if (typeof window !== 'undefined') {
|
||||
await import('./instrumentation-client')
|
||||
}
|
||||
}
|
||||
|
||||
export const onRequestError = Sentry.captureRequestError
|
||||
|
||||
@@ -4,17 +4,24 @@ import { organizationClient } from 'better-auth/client/plugins'
|
||||
import { createAuthClient } from 'better-auth/react'
|
||||
import { isProd } from '@/lib/environment'
|
||||
|
||||
const clientEnv = {
|
||||
NEXT_PUBLIC_VERCEL_URL: process.env.NEXT_PUBLIC_VERCEL_URL,
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
VERCEL_ENV: process.env.VERCEL_ENV || '',
|
||||
BETTER_AUTH_URL: process.env.BETTER_AUTH_URL || 'http://localhost:3000',
|
||||
}
|
||||
|
||||
export function getBaseURL() {
|
||||
let baseURL
|
||||
|
||||
if (process.env.VERCEL_ENV === 'preview') {
|
||||
baseURL = `https://${process.env.NEXT_PUBLIC_VERCEL_URL}`
|
||||
} else if (process.env.VERCEL_ENV === 'development') {
|
||||
baseURL = `https://${process.env.NEXT_PUBLIC_VERCEL_URL}`
|
||||
} else if (process.env.VERCEL_ENV === 'production') {
|
||||
baseURL = process.env.BETTER_AUTH_URL
|
||||
} else if (process.env.NODE_ENV === 'development') {
|
||||
baseURL = process.env.BETTER_AUTH_URL
|
||||
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
|
||||
} else if (clientEnv.NODE_ENV === 'development') {
|
||||
baseURL = clientEnv.BETTER_AUTH_URL
|
||||
}
|
||||
|
||||
return baseURL
|
||||
|
||||
@@ -16,21 +16,22 @@ import {
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { db } from '@/db'
|
||||
import * as schema from '@/db/schema'
|
||||
import { env } from './env'
|
||||
|
||||
const logger = createLogger('Auth')
|
||||
|
||||
const isProd = process.env.NODE_ENV === 'production'
|
||||
const isProd = env.NODE_ENV === 'production'
|
||||
|
||||
// Only initialize Stripe if the key is provided
|
||||
// This allows local development without a Stripe account
|
||||
const validStripeKey =
|
||||
process.env.STRIPE_SECRET_KEY &&
|
||||
process.env.STRIPE_SECRET_KEY.trim() !== '' &&
|
||||
process.env.STRIPE_SECRET_KEY !== 'placeholder'
|
||||
env.STRIPE_SECRET_KEY &&
|
||||
env.STRIPE_SECRET_KEY.trim() !== '' &&
|
||||
env.STRIPE_SECRET_KEY !== 'placeholder'
|
||||
|
||||
let stripeClient = null
|
||||
if (validStripeKey) {
|
||||
stripeClient = new Stripe(process.env.STRIPE_SECRET_KEY || '', {
|
||||
stripeClient = new Stripe(env.STRIPE_SECRET_KEY || '', {
|
||||
apiVersion: '2025-02-24.acacia',
|
||||
})
|
||||
}
|
||||
@@ -39,12 +40,10 @@ if (validStripeKey) {
|
||||
// In that case, we don't want to send emails and just log them
|
||||
|
||||
const validResendAPIKEY =
|
||||
process.env.RESEND_API_KEY &&
|
||||
process.env.RESEND_API_KEY.trim() !== '' &&
|
||||
process.env.RESEND_API_KEY !== 'placeholder'
|
||||
env.RESEND_API_KEY && env.RESEND_API_KEY.trim() !== '' && env.RESEND_API_KEY !== 'placeholder'
|
||||
|
||||
const resend = validResendAPIKEY
|
||||
? new Resend(process.env.RESEND_API_KEY)
|
||||
? new Resend(env.RESEND_API_KEY)
|
||||
: {
|
||||
emails: {
|
||||
send: async (...args: any[]) => {
|
||||
@@ -121,13 +120,13 @@ export const auth = betterAuth({
|
||||
},
|
||||
socialProviders: {
|
||||
github: {
|
||||
clientId: process.env.GITHUB_CLIENT_ID as string,
|
||||
clientSecret: process.env.GITHUB_CLIENT_SECRET as string,
|
||||
clientId: env.GITHUB_CLIENT_ID as string,
|
||||
clientSecret: env.GITHUB_CLIENT_SECRET as string,
|
||||
scopes: ['user:email', 'repo'],
|
||||
},
|
||||
google: {
|
||||
clientId: process.env.GOOGLE_CLIENT_ID as string,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
|
||||
clientId: env.GOOGLE_CLIENT_ID as string,
|
||||
clientSecret: env.GOOGLE_CLIENT_SECRET as string,
|
||||
scopes: [
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
@@ -209,15 +208,15 @@ export const auth = betterAuth({
|
||||
config: [
|
||||
{
|
||||
providerId: 'github-repo',
|
||||
clientId: process.env.GITHUB_REPO_CLIENT_ID as string,
|
||||
clientSecret: process.env.GITHUB_REPO_CLIENT_SECRET as string,
|
||||
clientId: env.GITHUB_REPO_CLIENT_ID as string,
|
||||
clientSecret: env.GITHUB_REPO_CLIENT_SECRET as string,
|
||||
authorizationUrl: 'https://github.com/login/oauth/authorize',
|
||||
accessType: 'offline',
|
||||
prompt: 'consent',
|
||||
tokenUrl: 'https://github.com/login/oauth/access_token',
|
||||
userInfoUrl: 'https://api.github.com/user',
|
||||
scopes: ['user:email', 'repo', 'read:user', 'workflow'],
|
||||
redirectURI: `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/github-repo`,
|
||||
redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/github-repo`,
|
||||
getUserInfo: async (tokens) => {
|
||||
try {
|
||||
// Fetch user profile
|
||||
@@ -290,8 +289,8 @@ export const auth = betterAuth({
|
||||
// Google providers for different purposes
|
||||
{
|
||||
providerId: 'google-email',
|
||||
clientId: process.env.GOOGLE_CLIENT_ID as string,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
|
||||
clientId: env.GOOGLE_CLIENT_ID as string,
|
||||
clientSecret: env.GOOGLE_CLIENT_SECRET as string,
|
||||
discoveryUrl: 'https://accounts.google.com/.well-known/openid-configuration',
|
||||
accessType: 'offline',
|
||||
scopes: [
|
||||
@@ -303,12 +302,12 @@ export const auth = betterAuth({
|
||||
'https://www.googleapis.com/auth/gmail.labels',
|
||||
],
|
||||
prompt: 'consent',
|
||||
redirectURI: `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/google-email`,
|
||||
redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/google-email`,
|
||||
},
|
||||
{
|
||||
providerId: 'google-calendar',
|
||||
clientId: process.env.GOOGLE_CLIENT_ID as string,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
|
||||
clientId: env.GOOGLE_CLIENT_ID as string,
|
||||
clientSecret: env.GOOGLE_CLIENT_SECRET as string,
|
||||
discoveryUrl: 'https://accounts.google.com/.well-known/openid-configuration',
|
||||
accessType: 'offline',
|
||||
scopes: [
|
||||
@@ -317,12 +316,12 @@ export const auth = betterAuth({
|
||||
'https://www.googleapis.com/auth/calendar',
|
||||
],
|
||||
prompt: 'consent',
|
||||
redirectURI: `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/google-calendar`,
|
||||
redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/google-calendar`,
|
||||
},
|
||||
{
|
||||
providerId: 'google-drive',
|
||||
clientId: process.env.GOOGLE_CLIENT_ID as string,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
|
||||
clientId: env.GOOGLE_CLIENT_ID as string,
|
||||
clientSecret: env.GOOGLE_CLIENT_SECRET as string,
|
||||
discoveryUrl: 'https://accounts.google.com/.well-known/openid-configuration',
|
||||
accessType: 'offline',
|
||||
scopes: [
|
||||
@@ -331,12 +330,12 @@ export const auth = betterAuth({
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
],
|
||||
prompt: 'consent',
|
||||
redirectURI: `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/google-drive`,
|
||||
redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/google-drive`,
|
||||
},
|
||||
{
|
||||
providerId: 'google-docs',
|
||||
clientId: process.env.GOOGLE_CLIENT_ID as string,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
|
||||
clientId: env.GOOGLE_CLIENT_ID as string,
|
||||
clientSecret: env.GOOGLE_CLIENT_SECRET as string,
|
||||
discoveryUrl: 'https://accounts.google.com/.well-known/openid-configuration',
|
||||
accessType: 'offline',
|
||||
scopes: [
|
||||
@@ -345,12 +344,12 @@ export const auth = betterAuth({
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
],
|
||||
prompt: 'consent',
|
||||
redirectURI: `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/google-docs`,
|
||||
redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/google-docs`,
|
||||
},
|
||||
{
|
||||
providerId: 'google-sheets',
|
||||
clientId: process.env.GOOGLE_CLIENT_ID as string,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
|
||||
clientId: env.GOOGLE_CLIENT_ID as string,
|
||||
clientSecret: env.GOOGLE_CLIENT_SECRET as string,
|
||||
discoveryUrl: 'https://accounts.google.com/.well-known/openid-configuration',
|
||||
accessType: 'offline',
|
||||
scopes: [
|
||||
@@ -360,14 +359,14 @@ export const auth = betterAuth({
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
],
|
||||
prompt: 'consent',
|
||||
redirectURI: `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/google-sheets`,
|
||||
redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/google-sheets`,
|
||||
},
|
||||
|
||||
// Supabase provider
|
||||
{
|
||||
providerId: 'supabase',
|
||||
clientId: process.env.SUPABASE_CLIENT_ID as string,
|
||||
clientSecret: process.env.SUPABASE_CLIENT_SECRET as string,
|
||||
clientId: env.SUPABASE_CLIENT_ID as string,
|
||||
clientSecret: env.SUPABASE_CLIENT_SECRET as string,
|
||||
authorizationUrl: 'https://api.supabase.com/v1/oauth/authorize',
|
||||
tokenUrl: 'https://api.supabase.com/v1/oauth/token',
|
||||
// Supabase doesn't have a standard userInfo endpoint that works with our flow,
|
||||
@@ -376,7 +375,7 @@ export const auth = betterAuth({
|
||||
scopes: ['database.read', 'database.write', 'projects.read'],
|
||||
responseType: 'code',
|
||||
pkce: true,
|
||||
redirectURI: `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/supabase`,
|
||||
redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/supabase`,
|
||||
getUserInfo: async (tokens) => {
|
||||
try {
|
||||
logger.info('Creating Supabase user profile from token data')
|
||||
@@ -422,8 +421,8 @@ export const auth = betterAuth({
|
||||
// X provider
|
||||
{
|
||||
providerId: 'x',
|
||||
clientId: process.env.X_CLIENT_ID as string,
|
||||
clientSecret: process.env.X_CLIENT_SECRET as string,
|
||||
clientId: env.X_CLIENT_ID as string,
|
||||
clientSecret: env.X_CLIENT_SECRET as string,
|
||||
authorizationUrl: 'https://x.com/i/oauth2/authorize',
|
||||
tokenUrl: 'https://api.x.com/2/oauth2/token',
|
||||
userInfoUrl: 'https://api.x.com/2/users/me',
|
||||
@@ -433,7 +432,7 @@ export const auth = betterAuth({
|
||||
responseType: 'code',
|
||||
prompt: 'consent',
|
||||
authentication: 'basic',
|
||||
redirectURI: `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/x`,
|
||||
redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/x`,
|
||||
getUserInfo: async (tokens) => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
@@ -481,8 +480,8 @@ export const auth = betterAuth({
|
||||
// Confluence provider
|
||||
{
|
||||
providerId: 'confluence',
|
||||
clientId: process.env.CONFLUENCE_CLIENT_ID as string,
|
||||
clientSecret: process.env.CONFLUENCE_CLIENT_SECRET as string,
|
||||
clientId: env.CONFLUENCE_CLIENT_ID as string,
|
||||
clientSecret: env.CONFLUENCE_CLIENT_SECRET as string,
|
||||
authorizationUrl: 'https://auth.atlassian.com/authorize',
|
||||
tokenUrl: 'https://auth.atlassian.com/oauth/token',
|
||||
userInfoUrl: 'https://api.atlassian.com/me',
|
||||
@@ -492,7 +491,7 @@ export const auth = betterAuth({
|
||||
accessType: 'offline',
|
||||
authentication: 'basic',
|
||||
prompt: 'consent',
|
||||
redirectURI: `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/confluence`,
|
||||
redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/confluence`,
|
||||
getUserInfo: async (tokens) => {
|
||||
try {
|
||||
const response = await fetch('https://api.atlassian.com/me', {
|
||||
@@ -532,8 +531,8 @@ export const auth = betterAuth({
|
||||
// Discord provider
|
||||
{
|
||||
providerId: 'discord',
|
||||
clientId: process.env.DISCORD_CLIENT_ID as string,
|
||||
clientSecret: process.env.DISCORD_CLIENT_SECRET as string,
|
||||
clientId: env.DISCORD_CLIENT_ID as string,
|
||||
clientSecret: env.DISCORD_CLIENT_SECRET as string,
|
||||
authorizationUrl: 'https://discord.com/api/oauth2/authorize',
|
||||
tokenUrl: 'https://discord.com/api/oauth2/token',
|
||||
userInfoUrl: 'https://discord.com/api/users/@me',
|
||||
@@ -542,7 +541,7 @@ export const auth = betterAuth({
|
||||
accessType: 'offline',
|
||||
authentication: 'basic',
|
||||
prompt: 'consent',
|
||||
redirectURI: `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/discord`,
|
||||
redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/discord`,
|
||||
getUserInfo: async (tokens) => {
|
||||
try {
|
||||
const response = await fetch('https://discord.com/api/users/@me', {
|
||||
@@ -583,8 +582,8 @@ export const auth = betterAuth({
|
||||
// Jira provider
|
||||
{
|
||||
providerId: 'jira',
|
||||
clientId: process.env.JIRA_CLIENT_ID as string,
|
||||
clientSecret: process.env.JIRA_CLIENT_SECRET as string,
|
||||
clientId: env.JIRA_CLIENT_ID as string,
|
||||
clientSecret: env.JIRA_CLIENT_SECRET as string,
|
||||
authorizationUrl: 'https://auth.atlassian.com/authorize',
|
||||
tokenUrl: 'https://auth.atlassian.com/oauth/token',
|
||||
userInfoUrl: 'https://api.atlassian.com/me',
|
||||
@@ -613,7 +612,7 @@ export const auth = betterAuth({
|
||||
accessType: 'offline',
|
||||
authentication: 'basic',
|
||||
prompt: 'consent',
|
||||
redirectURI: `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/jira`,
|
||||
redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/jira`,
|
||||
getUserInfo: async (tokens) => {
|
||||
try {
|
||||
const response = await fetch('https://api.atlassian.com/me', {
|
||||
@@ -653,8 +652,8 @@ export const auth = betterAuth({
|
||||
// Airtable provider
|
||||
{
|
||||
providerId: 'airtable',
|
||||
clientId: process.env.AIRTABLE_CLIENT_ID as string,
|
||||
clientSecret: process.env.AIRTABLE_CLIENT_SECRET as string,
|
||||
clientId: env.AIRTABLE_CLIENT_ID as string,
|
||||
clientSecret: env.AIRTABLE_CLIENT_SECRET as string,
|
||||
authorizationUrl: 'https://airtable.com/oauth2/v1/authorize',
|
||||
tokenUrl: 'https://airtable.com/oauth2/v1/token',
|
||||
userInfoUrl: 'https://api.airtable.com/v0/meta/whoami',
|
||||
@@ -664,14 +663,14 @@ export const auth = betterAuth({
|
||||
accessType: 'offline',
|
||||
authentication: 'basic',
|
||||
prompt: 'consent',
|
||||
redirectURI: `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/airtable`,
|
||||
redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/airtable`,
|
||||
},
|
||||
|
||||
// Notion provider
|
||||
{
|
||||
providerId: 'notion',
|
||||
clientId: process.env.NOTION_CLIENT_ID as string,
|
||||
clientSecret: process.env.NOTION_CLIENT_SECRET as string,
|
||||
clientId: env.NOTION_CLIENT_ID as string,
|
||||
clientSecret: env.NOTION_CLIENT_SECRET as string,
|
||||
authorizationUrl: 'https://api.notion.com/v1/oauth/authorize',
|
||||
tokenUrl: 'https://api.notion.com/v1/oauth/token',
|
||||
userInfoUrl: 'https://api.notion.com/v1/users/me',
|
||||
@@ -681,7 +680,7 @@ export const auth = betterAuth({
|
||||
accessType: 'offline',
|
||||
authentication: 'basic',
|
||||
prompt: 'consent',
|
||||
redirectURI: `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/notion`,
|
||||
redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/notion`,
|
||||
getUserInfo: async (tokens) => {
|
||||
try {
|
||||
const response = await fetch('https://api.notion.com/v1/users/me', {
|
||||
@@ -724,7 +723,7 @@ export const auth = betterAuth({
|
||||
? [
|
||||
stripe({
|
||||
stripeClient,
|
||||
stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET || '',
|
||||
stripeWebhookSecret: env.STRIPE_WEBHOOK_SECRET || '',
|
||||
createCustomerOnSignUp: true,
|
||||
onCustomerCreate: async ({ customer, stripeCustomer, user }, request) => {
|
||||
logger.info('Stripe customer created', {
|
||||
@@ -737,11 +736,9 @@ export const auth = betterAuth({
|
||||
plans: [
|
||||
{
|
||||
name: 'free',
|
||||
priceId: process.env.STRIPE_FREE_PRICE_ID || '',
|
||||
priceId: env.STRIPE_FREE_PRICE_ID || '',
|
||||
limits: {
|
||||
cost: process.env.FREE_TIER_COST_LIMIT
|
||||
? parseInt(process.env.FREE_TIER_COST_LIMIT)
|
||||
: 5,
|
||||
cost: env.FREE_TIER_COST_LIMIT ? parseInt(env.FREE_TIER_COST_LIMIT) : 5,
|
||||
sharingEnabled: 0,
|
||||
multiplayerEnabled: 0,
|
||||
workspaceCollaborationEnabled: 0,
|
||||
@@ -749,11 +746,9 @@ export const auth = betterAuth({
|
||||
},
|
||||
{
|
||||
name: 'pro',
|
||||
priceId: process.env.STRIPE_PRO_PRICE_ID || '',
|
||||
priceId: env.STRIPE_PRO_PRICE_ID || '',
|
||||
limits: {
|
||||
cost: process.env.PRO_TIER_COST_LIMIT
|
||||
? parseInt(process.env.PRO_TIER_COST_LIMIT)
|
||||
: 20,
|
||||
cost: env.PRO_TIER_COST_LIMIT ? parseInt(env.PRO_TIER_COST_LIMIT) : 20,
|
||||
sharingEnabled: 1,
|
||||
multiplayerEnabled: 0,
|
||||
workspaceCollaborationEnabled: 0,
|
||||
@@ -761,11 +756,9 @@ export const auth = betterAuth({
|
||||
},
|
||||
{
|
||||
name: 'team',
|
||||
priceId: process.env.STRIPE_TEAM_PRICE_ID || '',
|
||||
priceId: env.STRIPE_TEAM_PRICE_ID || '',
|
||||
limits: {
|
||||
cost: process.env.TEAM_TIER_COST_LIMIT
|
||||
? parseInt(process.env.TEAM_TIER_COST_LIMIT)
|
||||
: 40, // $40 per seat
|
||||
cost: env.TEAM_TIER_COST_LIMIT ? parseInt(env.TEAM_TIER_COST_LIMIT) : 40, // $40 per seat
|
||||
sharingEnabled: 1,
|
||||
multiplayerEnabled: 1,
|
||||
workspaceCollaborationEnabled: 1,
|
||||
@@ -926,7 +919,7 @@ export const auth = betterAuth({
|
||||
try {
|
||||
const { invitation, organization, inviter } = data
|
||||
|
||||
const inviteUrl = `${process.env.NEXT_PUBLIC_APP_URL}/invite/${invitation.id}`
|
||||
const inviteUrl = `${env.NEXT_PUBLIC_APP_URL}/invite/${invitation.id}`
|
||||
const inviterName = inviter.user?.name || 'A team member'
|
||||
|
||||
const html = await renderInvitationEmail(
|
||||
|
||||
119
apps/sim/lib/env.ts
Normal file
119
apps/sim/lib/env.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
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,
|
||||
|
||||
server: {
|
||||
DATABASE_URL: z.string().url(),
|
||||
BETTER_AUTH_URL: z.string().url(),
|
||||
BETTER_AUTH_SECRET: z.string().min(32),
|
||||
ENCRYPTION_KEY: z.string().min(32),
|
||||
|
||||
POSTGRES_URL: z.string().url().optional(),
|
||||
STRIPE_SECRET_KEY: z.string().min(1).optional(),
|
||||
STRIPE_WEBHOOK_SECRET: z.string().min(1).optional(),
|
||||
STRIPE_FREE_PRICE_ID: z.string().min(1).optional(),
|
||||
FREE_TIER_COST_LIMIT: z
|
||||
.string()
|
||||
.regex(/^\d+(\.\d+)?$/)
|
||||
.optional(),
|
||||
STRIPE_PRO_PRICE_ID: z.string().min(1).optional(),
|
||||
PRO_TIER_COST_LIMIT: z
|
||||
.string()
|
||||
.regex(/^\d+(\.\d+)?$/)
|
||||
.optional(),
|
||||
STRIPE_TEAM_PRICE_ID: z.string().min(1).optional(),
|
||||
TEAM_TIER_COST_LIMIT: z
|
||||
.string()
|
||||
.regex(/^\d+(\.\d+)?$/)
|
||||
.optional(),
|
||||
STRIPE_ENTERPRISE_PRICE_ID: z.string().min(1).optional(),
|
||||
ENTERPRISE_TIER_COST_LIMIT: z
|
||||
.string()
|
||||
.regex(/^\d+(\.\d+)?$/)
|
||||
.optional(),
|
||||
RESEND_API_KEY: z.string().min(1).optional(),
|
||||
OPENAI_API_KEY: z.string().min(1).optional(),
|
||||
OPENAI_API_KEY_1: z.string().min(1).optional(),
|
||||
OPENAI_API_KEY_2: z.string().min(1).optional(),
|
||||
OPENAI_API_KEY_3: z.string().min(1).optional(),
|
||||
ANTHROPIC_API_KEY_1: z.string().min(1).optional(),
|
||||
ANTHROPIC_API_KEY_2: z.string().min(1).optional(),
|
||||
ANTHROPIC_API_KEY_3: z.string().min(1).optional(),
|
||||
FREESTYLE_API_KEY: z.string().min(1).optional(),
|
||||
TELEMETRY_ENDPOINT: z.string().url().optional(),
|
||||
COST_MULTIPLIER: z
|
||||
.string()
|
||||
.regex(/^\d+(\.\d+)?$/)
|
||||
.optional(),
|
||||
JWT_SECRET: z.string().min(1).optional(),
|
||||
BROWSERBASE_API_KEY: z.string().min(1).optional(),
|
||||
BROWSERBASE_PROJECT_ID: z.string().min(1).optional(),
|
||||
OLLAMA_HOST: z.string().url().optional(),
|
||||
SENTRY_ORG: z.string().optional(),
|
||||
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(),
|
||||
AWS_REGION: z.string().optional(),
|
||||
AWS_ACCESS_KEY_ID: z.string().optional(),
|
||||
AWS_SECRET_ACCESS_KEY: z.string().optional(),
|
||||
S3_BUCKET_NAME: z.string().optional(),
|
||||
S3_LOGS_BUCKET_NAME: z.string().optional(),
|
||||
USE_S3: z.coerce.boolean().optional(),
|
||||
CRON_SECRET: z.string().optional(),
|
||||
FREE_PLAN_LOG_RETENTION_DAYS: z.string().optional(),
|
||||
NODE_ENV: z.string().optional(),
|
||||
GITHUB_TOKEN: z.string().optional(),
|
||||
|
||||
// OAuth blocks (all optional)
|
||||
GOOGLE_CLIENT_ID: z.string().optional(),
|
||||
GOOGLE_CLIENT_SECRET: z.string().optional(),
|
||||
GITHUB_CLIENT_ID: z.string().optional(),
|
||||
GITHUB_CLIENT_SECRET: z.string().optional(),
|
||||
GITHUB_REPO_CLIENT_ID: z.string().optional(),
|
||||
GITHUB_REPO_CLIENT_SECRET: z.string().optional(),
|
||||
X_CLIENT_ID: z.string().optional(),
|
||||
X_CLIENT_SECRET: z.string().optional(),
|
||||
CONFLUENCE_CLIENT_ID: z.string().optional(),
|
||||
CONFLUENCE_CLIENT_SECRET: z.string().optional(),
|
||||
JIRA_CLIENT_ID: z.string().optional(),
|
||||
JIRA_CLIENT_SECRET: z.string().optional(),
|
||||
AIRTABLE_CLIENT_ID: z.string().optional(),
|
||||
AIRTABLE_CLIENT_SECRET: z.string().optional(),
|
||||
SUPABASE_CLIENT_ID: z.string().optional(),
|
||||
SUPABASE_CLIENT_SECRET: z.string().optional(),
|
||||
NOTION_CLIENT_ID: z.string().optional(),
|
||||
NOTION_CLIENT_SECRET: z.string().optional(),
|
||||
DISCORD_CLIENT_ID: z.string().optional(),
|
||||
DISCORD_CLIENT_SECRET: z.string().optional(),
|
||||
HUBSPOT_CLIENT_ID: z.string().optional(),
|
||||
HUBSPOT_CLIENT_SECRET: z.string().optional(),
|
||||
DOCKER_BUILD: z.boolean().optional(),
|
||||
},
|
||||
|
||||
client: {
|
||||
NEXT_PUBLIC_APP_URL: z.string().url(),
|
||||
NEXT_PUBLIC_VERCEL_URL: z.string().optional(),
|
||||
NEXT_PUBLIC_SENTRY_DSN: z.string().url().optional(),
|
||||
NEXT_PUBLIC_GOOGLE_CLIENT_ID: z.string().optional(),
|
||||
NEXT_PUBLIC_GOOGLE_API_KEY: z.string().optional(),
|
||||
NEXT_PUBLIC_GOOGLE_PROJECT_NUMBER: z.string().optional(),
|
||||
},
|
||||
|
||||
// Only need to define client variables, server variables are automatically handled
|
||||
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'),
|
||||
},
|
||||
})
|
||||
@@ -1,30 +1,39 @@
|
||||
/**
|
||||
* Environment utility functions for consistent environment detection across the application
|
||||
*/
|
||||
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 = process.env.NODE_ENV === 'production'
|
||||
export const isProd = getNodeEnv() === 'production'
|
||||
|
||||
/**
|
||||
* Is the application running in development mode
|
||||
*/
|
||||
export const isDev = process.env.NODE_ENV === 'development'
|
||||
export const isDev = getNodeEnv() === 'development'
|
||||
|
||||
/**
|
||||
* Is the application running in test mode
|
||||
*/
|
||||
export const isTest = process.env.NODE_ENV === 'test'
|
||||
export const isTest = getNodeEnv() === 'test'
|
||||
|
||||
/**
|
||||
* Is this the hosted version of the application
|
||||
*/
|
||||
export const isHosted = process.env.NEXT_PUBLIC_APP_URL === 'https://www.simstudio.ai'
|
||||
export const isHosted = env.NEXT_PUBLIC_APP_URL === 'https://www.simstudio.ai'
|
||||
|
||||
/**
|
||||
* Get cost multiplier based on environment
|
||||
*/
|
||||
export function getCostMultiplier(): number {
|
||||
return isProd ? parseFloat(process.env.COST_MULTIPLIER!) || 1 : 1
|
||||
return isProd ? parseFloat(env.COST_MULTIPLIER!) || 1 : 1
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { FreestyleSandboxes } from 'freestyle-sandboxes'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { env } from './env'
|
||||
|
||||
const logger = createLogger('Freestyle')
|
||||
|
||||
@@ -16,7 +17,7 @@ export async function getFreestyleClient(): Promise<FreestyleSandboxes> {
|
||||
|
||||
try {
|
||||
freestyleInstance = new FreestyleSandboxes({
|
||||
apiKey: process.env.FREESTYLE_API_KEY!, // make sure to set this
|
||||
apiKey: env.FREESTYLE_API_KEY!, // make sure to set this
|
||||
})
|
||||
|
||||
return freestyleInstance
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import OpenAI from 'openai'
|
||||
import { env } from './env'
|
||||
|
||||
/**
|
||||
* Generates a short title for a chat based on the first message
|
||||
@@ -6,7 +7,7 @@ import OpenAI from 'openai'
|
||||
* @returns A short title or null if API key is not available
|
||||
*/
|
||||
export async function generateChatTitle(message: string): Promise<string | null> {
|
||||
const apiKey = process.env.OPENAI_API_KEY
|
||||
const apiKey = env.OPENAI_API_KEY
|
||||
|
||||
if (!apiKey) {
|
||||
return null
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
* It is separate from the user-facing logging system in logging.ts.
|
||||
*/
|
||||
import chalk from 'chalk'
|
||||
import { env } from '../env'
|
||||
|
||||
/**
|
||||
* LogLevel enum defines the severity levels for logging
|
||||
@@ -55,7 +56,7 @@ const LOG_CONFIG = {
|
||||
}
|
||||
|
||||
// Get current environment
|
||||
const ENV = process.env.NODE_ENV || 'development'
|
||||
const ENV = (process.env.NODE_ENV || 'development') as keyof typeof LOG_CONFIG
|
||||
const config = LOG_CONFIG[ENV] || LOG_CONFIG.development
|
||||
|
||||
// Format objects for logging
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Resend } from 'resend'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { env } from './env'
|
||||
|
||||
interface EmailOptions {
|
||||
to: string
|
||||
@@ -27,7 +28,7 @@ interface BatchSendEmailResult {
|
||||
|
||||
const logger = createLogger('Mailer')
|
||||
|
||||
const resendApiKey = process.env.RESEND_API_KEY
|
||||
const resendApiKey = env.RESEND_API_KEY
|
||||
const resend =
|
||||
resendApiKey && resendApiKey !== 'placeholder' && resendApiKey.trim() !== ''
|
||||
? new Resend(resendApiKey)
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
xIcon,
|
||||
} from '@/components/icons'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { env } from './env'
|
||||
|
||||
const logger = createLogger('OAuth')
|
||||
|
||||
@@ -410,52 +411,52 @@ export async function refreshOAuthToken(
|
||||
switch (provider) {
|
||||
case 'google':
|
||||
tokenEndpoint = 'https://oauth2.googleapis.com/token'
|
||||
clientId = process.env.GOOGLE_CLIENT_ID
|
||||
clientSecret = process.env.GOOGLE_CLIENT_SECRET
|
||||
clientId = env.GOOGLE_CLIENT_ID
|
||||
clientSecret = env.GOOGLE_CLIENT_SECRET
|
||||
break
|
||||
case 'github':
|
||||
tokenEndpoint = 'https://github.com/login/oauth/access_token'
|
||||
clientId = process.env.GITHUB_CLIENT_ID
|
||||
clientSecret = process.env.GITHUB_CLIENT_SECRET
|
||||
clientId = env.GITHUB_CLIENT_ID
|
||||
clientSecret = env.GITHUB_CLIENT_SECRET
|
||||
break
|
||||
case 'x':
|
||||
tokenEndpoint = 'https://api.x.com/2/oauth2/token'
|
||||
clientId = process.env.X_CLIENT_ID
|
||||
clientSecret = process.env.X_CLIENT_SECRET
|
||||
clientId = env.X_CLIENT_ID
|
||||
clientSecret = env.X_CLIENT_SECRET
|
||||
useBasicAuth = true
|
||||
break
|
||||
case 'confluence':
|
||||
tokenEndpoint = 'https://auth.atlassian.com/oauth/token'
|
||||
clientId = process.env.CONFLUENCE_CLIENT_ID
|
||||
clientSecret = process.env.CONFLUENCE_CLIENT_SECRET
|
||||
clientId = env.CONFLUENCE_CLIENT_ID
|
||||
clientSecret = env.CONFLUENCE_CLIENT_SECRET
|
||||
useBasicAuth = true
|
||||
break
|
||||
case 'jira':
|
||||
tokenEndpoint = 'https://auth.atlassian.com/oauth/token'
|
||||
clientId = process.env.JIRA_CLIENT_ID
|
||||
clientSecret = process.env.JIRA_CLIENT_SECRET
|
||||
clientId = env.JIRA_CLIENT_ID
|
||||
clientSecret = env.JIRA_CLIENT_SECRET
|
||||
useBasicAuth = true
|
||||
break
|
||||
case 'airtable':
|
||||
tokenEndpoint = 'https://airtable.com/oauth2/v1/token'
|
||||
clientId = process.env.AIRTABLE_CLIENT_ID
|
||||
clientSecret = process.env.AIRTABLE_CLIENT_SECRET
|
||||
clientId = env.AIRTABLE_CLIENT_ID
|
||||
clientSecret = env.AIRTABLE_CLIENT_SECRET
|
||||
useBasicAuth = true
|
||||
break
|
||||
case 'supabase':
|
||||
tokenEndpoint = 'https://api.supabase.com/v1/oauth/token'
|
||||
clientId = process.env.SUPABASE_CLIENT_ID
|
||||
clientSecret = process.env.SUPABASE_CLIENT_SECRET
|
||||
clientId = env.SUPABASE_CLIENT_ID
|
||||
clientSecret = env.SUPABASE_CLIENT_SECRET
|
||||
break
|
||||
case 'notion':
|
||||
tokenEndpoint = 'https://api.notion.com/v1/oauth/token'
|
||||
clientId = process.env.NOTION_CLIENT_ID
|
||||
clientSecret = process.env.NOTION_CLIENT_SECRET
|
||||
clientId = env.NOTION_CLIENT_ID
|
||||
clientSecret = env.NOTION_CLIENT_SECRET
|
||||
break
|
||||
case 'discord':
|
||||
tokenEndpoint = 'https://discord.com/api/v10/oauth2/token'
|
||||
clientId = process.env.DISCORD_CLIENT_ID
|
||||
clientSecret = process.env.DISCORD_CLIENT_SECRET
|
||||
clientId = env.DISCORD_CLIENT_ID
|
||||
clientSecret = env.DISCORD_CLIENT_SECRET
|
||||
useBasicAuth = true
|
||||
break
|
||||
default:
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import Redis from 'ioredis'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { env } from './env'
|
||||
|
||||
const logger = createLogger('Redis')
|
||||
|
||||
// Default to localhost if REDIS_URL is not provided
|
||||
const redisUrl = process.env.REDIS_URL || 'redis://localhost:6379'
|
||||
const redisUrl = env.REDIS_URL || 'redis://localhost:6379'
|
||||
|
||||
// Global Redis client for connection pooling
|
||||
// This is important for serverless environments like Vercel
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* Storage detection helper that determines if we should use local storage
|
||||
* This is used when running via the CLI with `npx simstudio`
|
||||
*/
|
||||
|
||||
// Check if we should use local storage based on environment variable
|
||||
export const useLocalStorage = () => {
|
||||
// In client components, check for the environment variable in localStorage
|
||||
// This is set by the CLI when running with `npx simstudio`
|
||||
if (typeof window !== 'undefined') {
|
||||
return localStorage.getItem('USE_LOCAL_STORAGE') === 'true'
|
||||
}
|
||||
|
||||
// In server components/API routes, check process.env
|
||||
return process.env.USE_LOCAL_STORAGE === 'true'
|
||||
}
|
||||
|
||||
// Export helpers for components to use
|
||||
export const storageMode = {
|
||||
get isLocal() {
|
||||
return useLocalStorage()
|
||||
},
|
||||
get isDatabase() {
|
||||
return !useLocalStorage()
|
||||
},
|
||||
}
|
||||
346
apps/sim/lib/subscription.ts
Normal file
346
apps/sim/lib/subscription.ts
Normal file
@@ -0,0 +1,346 @@
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { isProd } from '@/lib/environment'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { db } from '@/db'
|
||||
import * as schema from '@/db/schema'
|
||||
import { client } from './auth-client'
|
||||
import { env } from './env'
|
||||
|
||||
const logger = createLogger('Subscription')
|
||||
|
||||
/**
|
||||
* Check if the user is on the Pro plan
|
||||
*/
|
||||
export async function isProPlan(userId: string): Promise<boolean> {
|
||||
try {
|
||||
// In development, enable Pro features for easier testing
|
||||
if (!isProd) {
|
||||
return true
|
||||
}
|
||||
|
||||
// First check organizations the user belongs to (prioritize org subscriptions)
|
||||
const memberships = await db
|
||||
.select()
|
||||
.from(schema.member)
|
||||
.where(eq(schema.member.userId, userId))
|
||||
|
||||
// Check each organization for active Pro or Team subscriptions
|
||||
for (const membership of memberships) {
|
||||
const orgSubscriptions = await db
|
||||
.select()
|
||||
.from(schema.subscription)
|
||||
.where(eq(schema.subscription.referenceId, membership.organizationId))
|
||||
|
||||
const orgHasProPlan = orgSubscriptions.some(
|
||||
(sub) => sub.status === 'active' && (sub.plan === 'pro' || sub.plan === 'team')
|
||||
)
|
||||
|
||||
if (orgHasProPlan) {
|
||||
logger.info('User has pro plan via organization', {
|
||||
userId,
|
||||
orgId: membership.organizationId,
|
||||
})
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// If no org subscriptions, check direct subscriptions
|
||||
const directSubscriptions = await db
|
||||
.select()
|
||||
.from(schema.subscription)
|
||||
.where(eq(schema.subscription.referenceId, userId))
|
||||
|
||||
// Find active pro subscription (either Pro or Team plan)
|
||||
const hasDirectProPlan = directSubscriptions.some(
|
||||
(sub) => sub.status === 'active' && (sub.plan === 'pro' || sub.plan === 'team')
|
||||
)
|
||||
|
||||
if (hasDirectProPlan) {
|
||||
logger.info('User has direct pro plan', { userId })
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
} catch (error) {
|
||||
logger.error('Error checking pro plan status', { error, userId })
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user is on the Team plan
|
||||
*/
|
||||
export async function isTeamPlan(userId: string): Promise<boolean> {
|
||||
try {
|
||||
// In development, enable Team features for easier testing
|
||||
if (!isProd) {
|
||||
return true
|
||||
}
|
||||
|
||||
// First check organizations the user belongs to (prioritize org subscriptions)
|
||||
const memberships = await db
|
||||
.select()
|
||||
.from(schema.member)
|
||||
.where(eq(schema.member.userId, userId))
|
||||
|
||||
// Check each organization for active Team subscriptions
|
||||
for (const membership of memberships) {
|
||||
const orgSubscriptions = await db
|
||||
.select()
|
||||
.from(schema.subscription)
|
||||
.where(eq(schema.subscription.referenceId, membership.organizationId))
|
||||
|
||||
const orgHasTeamPlan = orgSubscriptions.some(
|
||||
(sub) => sub.status === 'active' && sub.plan === 'team'
|
||||
)
|
||||
|
||||
if (orgHasTeamPlan) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// If no org subscriptions found, check direct subscriptions
|
||||
const directSubscriptions = await db
|
||||
.select()
|
||||
.from(schema.subscription)
|
||||
.where(eq(schema.subscription.referenceId, userId))
|
||||
|
||||
// Find active team subscription
|
||||
const hasDirectTeamPlan = directSubscriptions.some(
|
||||
(sub) => sub.status === 'active' && sub.plan === 'team'
|
||||
)
|
||||
|
||||
if (hasDirectTeamPlan) {
|
||||
logger.info('User has direct team plan', { userId })
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
} catch (error) {
|
||||
logger.error('Error checking team plan status', { error, userId })
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user has exceeded their cost limit based on their subscription plan
|
||||
*/
|
||||
export async function hasExceededCostLimit(userId: string): Promise<boolean> {
|
||||
try {
|
||||
// In development, users never exceed their limit
|
||||
if (!isProd) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Get user's direct subscription
|
||||
const { data: directSubscriptions } = await client.subscription.list({
|
||||
query: { referenceId: userId },
|
||||
})
|
||||
|
||||
// Find active direct subscription
|
||||
const activeDirectSubscription = directSubscriptions?.find((sub) => sub.status === 'active')
|
||||
|
||||
// Get organizations the user belongs to
|
||||
const memberships = await db
|
||||
.select()
|
||||
.from(schema.member)
|
||||
.where(eq(schema.member.userId, userId))
|
||||
|
||||
let highestCostLimit = 0
|
||||
|
||||
// Check cost limit from direct subscription
|
||||
if (activeDirectSubscription && typeof activeDirectSubscription.limits?.cost === 'number') {
|
||||
highestCostLimit = activeDirectSubscription.limits.cost
|
||||
}
|
||||
|
||||
// Check cost limits from organization subscriptions
|
||||
for (const membership of memberships) {
|
||||
const { data: orgSubscriptions } = await client.subscription.list({
|
||||
query: { referenceId: membership.organizationId },
|
||||
})
|
||||
|
||||
const activeOrgSubscription = orgSubscriptions?.find((sub) => sub.status === 'active')
|
||||
|
||||
if (
|
||||
activeOrgSubscription &&
|
||||
typeof activeOrgSubscription.limits?.cost === 'number' &&
|
||||
activeOrgSubscription.limits.cost > highestCostLimit
|
||||
) {
|
||||
highestCostLimit = activeOrgSubscription.limits.cost
|
||||
}
|
||||
}
|
||||
|
||||
// If no subscription found, use default free tier limit
|
||||
if (highestCostLimit === 0) {
|
||||
highestCostLimit = env.FREE_TIER_COST_LIMIT ? parseFloat(env.FREE_TIER_COST_LIMIT) : 5
|
||||
}
|
||||
|
||||
logger.info('User cost limit from subscription', { userId, costLimit: highestCostLimit })
|
||||
|
||||
// Get user's actual usage from the database
|
||||
const statsRecords = await db
|
||||
.select()
|
||||
.from(schema.userStats)
|
||||
.where(eq(schema.userStats.userId, userId))
|
||||
|
||||
if (statsRecords.length === 0) {
|
||||
// No usage yet, so they haven't exceeded the limit
|
||||
return false
|
||||
}
|
||||
|
||||
// Get the current cost and compare with the limit
|
||||
const currentCost = parseFloat(statsRecords[0].totalCost.toString())
|
||||
|
||||
return currentCost >= highestCostLimit
|
||||
} catch (error) {
|
||||
logger.error('Error checking cost limit', { error, userId })
|
||||
return false // Be conservative in case of error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user is allowed to share workflows based on their subscription plan
|
||||
*/
|
||||
export async function isSharingEnabled(userId: string): Promise<boolean> {
|
||||
try {
|
||||
// In development, always allow sharing
|
||||
if (!isProd) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check direct subscription
|
||||
const { data: directSubscriptions } = await client.subscription.list({
|
||||
query: { referenceId: userId },
|
||||
})
|
||||
|
||||
const activeDirectSubscription = directSubscriptions?.find((sub) => sub.status === 'active')
|
||||
|
||||
// If user has direct pro/team subscription with sharing enabled
|
||||
if (activeDirectSubscription && activeDirectSubscription.limits?.sharingEnabled) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check organizations the user belongs to
|
||||
const memberships = await db
|
||||
.select()
|
||||
.from(schema.member)
|
||||
.where(eq(schema.member.userId, userId))
|
||||
|
||||
// Check each organization for a subscription with sharing enabled
|
||||
for (const membership of memberships) {
|
||||
const { data: orgSubscriptions } = await client.subscription.list({
|
||||
query: { referenceId: membership.organizationId },
|
||||
})
|
||||
|
||||
const activeOrgSubscription = orgSubscriptions?.find((sub) => sub.status === 'active')
|
||||
|
||||
if (activeOrgSubscription && activeOrgSubscription.limits?.sharingEnabled) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
} catch (error) {
|
||||
logger.error('Error checking sharing permission', { error, userId })
|
||||
return false // Be conservative in case of error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if multiplayer collaboration is enabled for the user
|
||||
*/
|
||||
export async function isMultiplayerEnabled(userId: string): Promise<boolean> {
|
||||
try {
|
||||
// In development, always enable multiplayer
|
||||
if (!isProd) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check direct subscription
|
||||
const { data: directSubscriptions } = await client.subscription.list({
|
||||
query: { referenceId: userId },
|
||||
})
|
||||
|
||||
const activeDirectSubscription = directSubscriptions?.find((sub) => sub.status === 'active')
|
||||
|
||||
// If user has direct team subscription with multiplayer enabled
|
||||
if (activeDirectSubscription && activeDirectSubscription.limits?.multiplayerEnabled) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check organizations the user belongs to
|
||||
const memberships = await db
|
||||
.select()
|
||||
.from(schema.member)
|
||||
.where(eq(schema.member.userId, userId))
|
||||
|
||||
// Check each organization for a subscription with multiplayer enabled
|
||||
for (const membership of memberships) {
|
||||
const { data: orgSubscriptions } = await client.subscription.list({
|
||||
query: { referenceId: membership.organizationId },
|
||||
})
|
||||
|
||||
const activeOrgSubscription = orgSubscriptions?.find((sub) => sub.status === 'active')
|
||||
|
||||
if (activeOrgSubscription && activeOrgSubscription.limits?.multiplayerEnabled) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
} catch (error) {
|
||||
logger.error('Error checking multiplayer permission', { error, userId })
|
||||
return false // Be conservative in case of error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if workspace collaboration is enabled for the user
|
||||
*/
|
||||
export async function isWorkspaceCollaborationEnabled(userId: string): Promise<boolean> {
|
||||
try {
|
||||
// In development, always enable workspace collaboration
|
||||
if (!isProd) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check direct subscription
|
||||
const { data: directSubscriptions } = await client.subscription.list({
|
||||
query: { referenceId: userId },
|
||||
})
|
||||
|
||||
const activeDirectSubscription = directSubscriptions?.find((sub) => sub.status === 'active')
|
||||
|
||||
// If user has direct team subscription with workspace collaboration enabled
|
||||
if (
|
||||
activeDirectSubscription &&
|
||||
activeDirectSubscription.limits?.workspaceCollaborationEnabled
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check organizations the user belongs to
|
||||
const memberships = await db
|
||||
.select()
|
||||
.from(schema.member)
|
||||
.where(eq(schema.member.userId, userId))
|
||||
|
||||
// Check each organization for a subscription with workspace collaboration enabled
|
||||
for (const membership of memberships) {
|
||||
const { data: orgSubscriptions } = await client.subscription.list({
|
||||
query: { referenceId: membership.organizationId },
|
||||
})
|
||||
|
||||
const activeOrgSubscription = orgSubscriptions?.find((sub) => sub.status === 'active')
|
||||
|
||||
if (activeOrgSubscription && activeOrgSubscription.limits?.workspaceCollaborationEnabled) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
} catch (error) {
|
||||
logger.error('Error checking workspace collaboration permission', { error, userId })
|
||||
return false // Be conservative in case of error
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@
|
||||
*/
|
||||
import { diag, DiagConsoleLogger, DiagLogLevel } from '@opentelemetry/api'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { env } from './env'
|
||||
|
||||
diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.ERROR)
|
||||
|
||||
@@ -28,9 +29,9 @@ export type TelemetryStatus = {
|
||||
const TELEMETRY_STATUS_KEY = 'simstudio-telemetry-status'
|
||||
|
||||
let telemetryConfig = {
|
||||
endpoint: process.env.TELEMETRY_ENDPOINT || 'https://telemetry.simstudio.ai/v1/traces',
|
||||
endpoint: env.TELEMETRY_ENDPOINT || 'https://telemetry.simstudio.ai/v1/traces',
|
||||
serviceName: 'sim-studio',
|
||||
serviceVersion: process.env.NEXT_PUBLIC_APP_VERSION || '0.1.0',
|
||||
serviceVersion: '0.1.0',
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined' && (window as any).__SIM_STUDIO_TELEMETRY_CONFIG) {
|
||||
@@ -48,7 +49,7 @@ export function getTelemetryStatus(): TelemetryStatus {
|
||||
}
|
||||
|
||||
try {
|
||||
if (process.env.NEXT_TELEMETRY_DISABLED === '1') {
|
||||
if (env.NEXT_TELEMETRY_DISABLED === '1') {
|
||||
return { enabled: false, notifiedUser: true }
|
||||
}
|
||||
|
||||
@@ -104,7 +105,7 @@ export function disableTelemetry(): void {
|
||||
* Enables telemetry
|
||||
*/
|
||||
export function enableTelemetry(): void {
|
||||
if (process.env.NEXT_TELEMETRY_DISABLED === '1') {
|
||||
if (env.NEXT_TELEMETRY_DISABLED === '1') {
|
||||
logger.info('Telemetry disabled by environment variable, cannot enable')
|
||||
return
|
||||
}
|
||||
@@ -141,7 +142,7 @@ function initializeClientTelemetry(): void {
|
||||
return
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
if (env.NODE_ENV === 'production') {
|
||||
trackEvent('page_view', window.location.pathname)
|
||||
|
||||
if (typeof window.history !== 'undefined') {
|
||||
@@ -264,7 +265,7 @@ export async function trackEvent(
|
||||
if (!status.enabled) return
|
||||
|
||||
try {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
if (env.NODE_ENV === 'production') {
|
||||
await fetch('/api/telemetry', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
downloadFromS3,
|
||||
FileInfo,
|
||||
getPresignedUrl,
|
||||
s3Client,
|
||||
getS3Client,
|
||||
uploadToS3,
|
||||
} from './s3-client'
|
||||
|
||||
@@ -57,6 +57,8 @@ vi.mock('@/lib/logs/console-logger', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
const s3Client = getS3Client()
|
||||
|
||||
describe('S3 Client', () => {
|
||||
let mockDate: Date
|
||||
let originalDateNow: typeof Date.now
|
||||
|
||||
@@ -5,16 +5,33 @@ import {
|
||||
S3Client,
|
||||
} from '@aws-sdk/client-s3'
|
||||
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
|
||||
import { env } from '../env'
|
||||
import { S3_CONFIG } from './setup'
|
||||
|
||||
// Create an S3 client
|
||||
export const s3Client = new S3Client({
|
||||
region: S3_CONFIG.region || '',
|
||||
credentials: {
|
||||
accessKeyId: process.env.AWS_ACCESS_KEY_ID || '',
|
||||
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || '',
|
||||
},
|
||||
})
|
||||
// Lazily create a single S3 client instance.
|
||||
let _s3Client: S3Client | null = null
|
||||
|
||||
export function getS3Client(): S3Client {
|
||||
if (_s3Client) return _s3Client
|
||||
|
||||
const { region } = S3_CONFIG
|
||||
|
||||
if (!region) {
|
||||
throw new Error(
|
||||
'AWS region is missing – set AWS_REGION in your environment or disable S3 uploads.'
|
||||
)
|
||||
}
|
||||
|
||||
_s3Client = new S3Client({
|
||||
region,
|
||||
credentials: {
|
||||
accessKeyId: env.AWS_ACCESS_KEY_ID || '',
|
||||
secretAccessKey: env.AWS_SECRET_ACCESS_KEY || '',
|
||||
},
|
||||
})
|
||||
|
||||
return _s3Client
|
||||
}
|
||||
|
||||
/**
|
||||
* File information structure
|
||||
@@ -46,6 +63,8 @@ export async function uploadToS3(
|
||||
const safeFileName = fileName.replace(/\s+/g, '-') // Replace spaces with hyphens
|
||||
const uniqueKey = `${Date.now()}-${safeFileName}`
|
||||
|
||||
const s3Client = getS3Client()
|
||||
|
||||
// Upload the file to S3
|
||||
await s3Client.send(
|
||||
new PutObjectCommand({
|
||||
@@ -85,7 +104,7 @@ export async function getPresignedUrl(key: string, expiresIn = 3600) {
|
||||
Key: key,
|
||||
})
|
||||
|
||||
return getSignedUrl(s3Client, command, { expiresIn })
|
||||
return getSignedUrl(getS3Client(), command, { expiresIn })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -99,7 +118,7 @@ export async function downloadFromS3(key: string) {
|
||||
Key: key,
|
||||
})
|
||||
|
||||
const response = await s3Client.send(command)
|
||||
const response = await getS3Client().send(command)
|
||||
const stream = response.Body as any
|
||||
|
||||
// Convert stream to buffer
|
||||
@@ -116,7 +135,7 @@ export async function downloadFromS3(key: string) {
|
||||
* @param key S3 object key
|
||||
*/
|
||||
export async function deleteFromS3(key: string) {
|
||||
await s3Client.send(
|
||||
await getS3Client().send(
|
||||
new DeleteObjectCommand({
|
||||
Bucket: S3_CONFIG.bucket,
|
||||
Key: key,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { env } from '../env'
|
||||
import { ensureUploadsDirectory, USE_S3_STORAGE } from './setup'
|
||||
|
||||
const logger = createLogger('UploadsSetup')
|
||||
@@ -10,7 +11,7 @@ if (typeof process !== 'undefined') {
|
||||
|
||||
if (USE_S3_STORAGE) {
|
||||
// Verify AWS credentials
|
||||
if (!process.env.AWS_ACCESS_KEY_ID || !process.env.AWS_SECRET_ACCESS_KEY) {
|
||||
if (!env.AWS_ACCESS_KEY_ID || !env.AWS_SECRET_ACCESS_KEY) {
|
||||
logger.warn('AWS credentials are not set in environment variables.')
|
||||
logger.warn('Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY for S3 storage.')
|
||||
} else {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { mkdir } from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
import path from 'path'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { env } from '../env'
|
||||
|
||||
const logger = createLogger('UploadsSetup')
|
||||
|
||||
@@ -12,11 +13,11 @@ const PROJECT_ROOT = path.resolve(process.cwd())
|
||||
// Define the upload directory path using project root
|
||||
export const UPLOAD_DIR = join(PROJECT_ROOT, 'uploads')
|
||||
|
||||
export const USE_S3_STORAGE = process.env.NODE_ENV === 'production' || process.env.USE_S3 === 'true'
|
||||
export const USE_S3_STORAGE = env.NODE_ENV === 'production' || env.USE_S3
|
||||
|
||||
export const S3_CONFIG = {
|
||||
bucket: process.env.S3_BUCKET_NAME || '',
|
||||
region: process.env.AWS_REGION || '',
|
||||
bucket: env.S3_BUCKET_NAME || '',
|
||||
region: env.AWS_REGION || '',
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { env } from '../env'
|
||||
|
||||
/**
|
||||
* Returns the base URL of the application, respecting environment variables for deployment environments
|
||||
* @returns The base URL string (e.g., 'http://localhost:3000' or 'https://example.com')
|
||||
@@ -7,13 +9,13 @@ export function getBaseUrl(): string {
|
||||
return window.location.origin
|
||||
}
|
||||
|
||||
const baseUrl = process.env.NEXT_PUBLIC_APP_URL
|
||||
const baseUrl = env.NEXT_PUBLIC_APP_URL
|
||||
if (baseUrl) {
|
||||
if (baseUrl.startsWith('http://') || baseUrl.startsWith('https://')) {
|
||||
return baseUrl
|
||||
}
|
||||
|
||||
const isProd = process.env.NODE_ENV === 'production'
|
||||
const isProd = env.NODE_ENV === 'production'
|
||||
const protocol = isProd ? 'https://' : 'http://'
|
||||
return `${protocol}${baseUrl}`
|
||||
}
|
||||
@@ -30,7 +32,7 @@ export function getBaseDomain(): string {
|
||||
const url = new URL(getBaseUrl())
|
||||
return url.host // host includes port if specified
|
||||
} catch (e) {
|
||||
const isProd = process.env.NODE_ENV === 'production'
|
||||
const isProd = env.NODE_ENV === 'production'
|
||||
return isProd ? 'simstudio.ai' : 'localhost:3000'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { userStats } from '@/db/schema'
|
||||
import { createLogger } from './logs/console-logger'
|
||||
import { getHighestPrioritySubscription } from './subscription/subscription'
|
||||
import { calculateUsageLimit } from './subscription/utils'
|
||||
import { env } from './env'
|
||||
|
||||
const logger = createLogger('UsageMonitor')
|
||||
|
||||
@@ -55,7 +56,7 @@ export async function checkUsageStatus(userId: string): Promise<UsageData> {
|
||||
})
|
||||
} else {
|
||||
// Free tier limit
|
||||
limit = parseFloat(process.env.FREE_TIER_COST_LIMIT!)
|
||||
limit = parseFloat(env.FREE_TIER_COST_LIMIT!)
|
||||
logger.info('Using free tier limit', { userId, limit })
|
||||
}
|
||||
|
||||
@@ -227,4 +228,4 @@ export async function checkServerSideUsageLimits(userId: string): Promise<{
|
||||
message: `Error checking usage limits: ${error instanceof Error ? error.message : String(error)}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { createCipheriv, createDecipheriv, randomBytes } from 'crypto'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { env } from './env'
|
||||
|
||||
const logger = createLogger('Utils')
|
||||
|
||||
@@ -11,7 +12,7 @@ export function cn(...inputs: ClassValue[]) {
|
||||
}
|
||||
|
||||
function getEncryptionKey(): Buffer {
|
||||
const key = process.env.ENCRYPTION_KEY
|
||||
const key = env.ENCRYPTION_KEY
|
||||
if (!key || key.length !== 64) {
|
||||
throw new Error('ENCRYPTION_KEY must be set to a 64-character hex string (32 bytes)')
|
||||
}
|
||||
@@ -286,13 +287,13 @@ export function getRotatingApiKey(provider: string): string {
|
||||
const keys = []
|
||||
|
||||
if (provider === 'openai') {
|
||||
if (process.env.OPENAI_API_KEY_1) keys.push(process.env.OPENAI_API_KEY_1)
|
||||
if (process.env.OPENAI_API_KEY_2) keys.push(process.env.OPENAI_API_KEY_2)
|
||||
if (process.env.OPENAI_API_KEY_3) keys.push(process.env.OPENAI_API_KEY_3)
|
||||
if (env.OPENAI_API_KEY_1) keys.push(env.OPENAI_API_KEY_1)
|
||||
if (env.OPENAI_API_KEY_2) keys.push(env.OPENAI_API_KEY_2)
|
||||
if (env.OPENAI_API_KEY_3) keys.push(env.OPENAI_API_KEY_3)
|
||||
} else if (provider === 'anthropic') {
|
||||
if (process.env.ANTHROPIC_API_KEY_1) keys.push(process.env.ANTHROPIC_API_KEY_1)
|
||||
if (process.env.ANTHROPIC_API_KEY_2) keys.push(process.env.ANTHROPIC_API_KEY_2)
|
||||
if (process.env.ANTHROPIC_API_KEY_3) keys.push(process.env.ANTHROPIC_API_KEY_3)
|
||||
if (env.ANTHROPIC_API_KEY_1) keys.push(env.ANTHROPIC_API_KEY_1)
|
||||
if (env.ANTHROPIC_API_KEY_2) keys.push(env.ANTHROPIC_API_KEY_2)
|
||||
if (env.ANTHROPIC_API_KEY_3) keys.push(env.ANTHROPIC_API_KEY_3)
|
||||
}
|
||||
|
||||
if (keys.length === 0) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { sendBatchEmails, sendEmail } from '@/lib/mailer'
|
||||
import { createToken, verifyToken } from '@/lib/waitlist/token'
|
||||
import { db } from '@/db'
|
||||
import { waitlist } from '@/db/schema'
|
||||
import { env } from '../env'
|
||||
|
||||
// Define types for better type safety
|
||||
export type WaitlistStatus = 'pending' | 'approved' | 'rejected' | 'signed_up'
|
||||
@@ -184,7 +185,7 @@ export async function approveWaitlistUser(
|
||||
})
|
||||
|
||||
// Generate signup link with token
|
||||
const signupLink = `${process.env.NEXT_PUBLIC_APP_URL}/signup?token=${token}`
|
||||
const signupLink = `${env.NEXT_PUBLIC_APP_URL}/signup?token=${token}`
|
||||
|
||||
// IMPORTANT: Send approval email BEFORE updating the status
|
||||
// This ensures we don't mark users as approved if email fails
|
||||
@@ -415,7 +416,7 @@ export async function resendApprovalEmail(
|
||||
})
|
||||
|
||||
// Generate signup link with token
|
||||
const signupLink = `${process.env.NEXT_PUBLIC_APP_URL}/signup?token=${token}`
|
||||
const signupLink = `${env.NEXT_PUBLIC_APP_URL}/signup?token=${token}`
|
||||
|
||||
// Send approval email
|
||||
try {
|
||||
@@ -542,7 +543,7 @@ export async function approveBatchWaitlistUsers(emails: string[]): Promise<{
|
||||
})
|
||||
|
||||
// Generate signup link with token
|
||||
const signupLink = `${process.env.NEXT_PUBLIC_APP_URL}/signup?token=${token}`
|
||||
const signupLink = `${env.NEXT_PUBLIC_APP_URL}/signup?token=${token}`
|
||||
|
||||
// Generate email HTML
|
||||
const emailHtml = await renderWaitlistApprovalEmail(user.email, signupLink)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { jwtVerify, SignJWT } from 'jose'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { env } from '../env'
|
||||
|
||||
interface TokenPayload {
|
||||
email: string
|
||||
@@ -17,7 +18,7 @@ interface DecodedToken {
|
||||
|
||||
// Get JWT secret from environment variables
|
||||
const getJwtSecret = () => {
|
||||
const secret = process.env.JWT_SECRET
|
||||
const secret = env.JWT_SECRET
|
||||
if (!secret) {
|
||||
throw new Error('JWT_SECRET environment variable is not set')
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { WorkflowState } from '@/stores/workflows/workflow/types'
|
||||
import { db } from '@/db'
|
||||
import { userStats, workflow as workflowTable } from '@/db/schema'
|
||||
import { env } from '../env'
|
||||
|
||||
const logger = createLogger('WorkflowUtils')
|
||||
|
||||
@@ -21,8 +22,7 @@ export async function updateWorkflowRunCounts(workflowId: string, runs: number =
|
||||
|
||||
// Get the origin from the environment or use direct DB update as fallback
|
||||
const origin =
|
||||
process.env.NEXT_PUBLIC_APP_URL ||
|
||||
(typeof window !== 'undefined' ? window.location.origin : '')
|
||||
env.NEXT_PUBLIC_APP_URL || (typeof window !== 'undefined' ? window.location.origin : '')
|
||||
|
||||
if (origin) {
|
||||
// Use absolute URL with origin
|
||||
|
||||
@@ -2,12 +2,13 @@ import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getSessionCookie } from 'better-auth/cookies'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { getBaseDomain } from '@/lib/urls/utils'
|
||||
import { env } from './lib/env'
|
||||
import { verifyToken } from './lib/waitlist/token'
|
||||
|
||||
const logger = createLogger('Middleware')
|
||||
|
||||
// Environment flag to check if we're in development mode
|
||||
const isDevelopment = process.env.NODE_ENV === 'development'
|
||||
const isDevelopment = env.NODE_ENV === 'development'
|
||||
|
||||
const SUSPICIOUS_UA_PATTERNS = [
|
||||
/^\s*$/, // Empty user agents
|
||||
@@ -103,6 +104,11 @@ 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()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { NextConfig } from 'next'
|
||||
import { withSentryConfig } from '@sentry/nextjs'
|
||||
import path from 'path'
|
||||
import { env } from './lib/env'
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
devIndicators: false,
|
||||
@@ -11,19 +12,25 @@ const nextConfig: NextConfig = {
|
||||
'api.stability.ai',
|
||||
],
|
||||
},
|
||||
output: process.env.NODE_ENV === 'development' ? 'standalone' : undefined,
|
||||
typescript: {
|
||||
ignoreBuildErrors: env.DOCKER_BUILD,
|
||||
},
|
||||
eslint: {
|
||||
ignoreDuringBuilds: env.DOCKER_BUILD,
|
||||
},
|
||||
output: env.DOCKER_BUILD ? 'standalone' : undefined,
|
||||
turbopack: {
|
||||
resolveExtensions: ['.tsx', '.ts', '.jsx', '.js', '.mjs', '.json'],
|
||||
},
|
||||
experimental: {
|
||||
optimizeCss: true,
|
||||
},
|
||||
...(process.env.NODE_ENV === 'development' && {
|
||||
...(env.NODE_ENV === 'development' && {
|
||||
outputFileTracingRoot: path.join(__dirname, '../../'),
|
||||
}),
|
||||
webpack: (config, { isServer, dev }) => {
|
||||
// Skip webpack configuration in development when using Turbopack
|
||||
if (dev && process.env.NEXT_RUNTIME === 'turbopack') {
|
||||
if (dev && env.NEXT_RUNTIME === 'turbopack') {
|
||||
return config
|
||||
}
|
||||
|
||||
@@ -122,11 +129,11 @@ const nextConfig: NextConfig = {
|
||||
|
||||
const sentryConfig = {
|
||||
silent: true,
|
||||
org: process.env.SENTRY_ORG || '',
|
||||
project: process.env.SENTRY_PROJECT || '',
|
||||
authToken: process.env.SENTRY_AUTH_TOKEN || undefined,
|
||||
disableSourceMapUpload: process.env.NODE_ENV !== 'production',
|
||||
autoInstrumentServerFunctions: process.env.NODE_ENV === 'production',
|
||||
org: env.SENTRY_ORG || '',
|
||||
project: env.SENTRY_PROJECT || '',
|
||||
authToken: env.SENTRY_AUTH_TOKEN || undefined,
|
||||
disableSourceMapUpload: env.NODE_ENV !== 'production',
|
||||
autoInstrumentServerFunctions: env.NODE_ENV === 'production',
|
||||
bundleSizeOptimizations: {
|
||||
excludeDebugStatements: true,
|
||||
excludePerformanceMonitoring: true,
|
||||
@@ -136,6 +143,6 @@ const sentryConfig = {
|
||||
},
|
||||
}
|
||||
|
||||
export default process.env.NODE_ENV === 'development'
|
||||
export default env.NODE_ENV === 'development'
|
||||
? nextConfig
|
||||
: withSentryConfig(nextConfig, sentryConfig)
|
||||
|
||||
22956
apps/sim/package-lock.json
generated
22956
apps/sim/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,28 +4,33 @@
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"bun": ">=1.2.13",
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "dotenv -- next dev --turbo --port 3000",
|
||||
"dev:classic": "dotenv -- next dev",
|
||||
"build": "dotenv -- next build",
|
||||
"start": "dotenv -- next start",
|
||||
"lint": "dotenv -- next lint",
|
||||
"dev": "next dev --turbo --port 3000",
|
||||
"dev:classic": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"prepare": "husky",
|
||||
"db:push": "dotenv -- drizzle-kit push",
|
||||
"db:studio": "dotenv -- drizzle-kit studio",
|
||||
"test": "dotenv -- vitest run",
|
||||
"test:watch": "dotenv -- vitest",
|
||||
"test:coverage": "dotenv -- vitest run --coverage",
|
||||
"email:dev": "dotenv -- email dev --dir components/emails",
|
||||
"cli:build": "npm run build -w packages/simstudio",
|
||||
"cli:dev": "npm run build -w packages/simstudio && cd packages/simstudio && node ./dist/index.js",
|
||||
"cli:publish": "cd packages/simstudio && npm publish",
|
||||
"cli:start": "cd packages/simstudio && node ./dist/index.js start",
|
||||
"build:standalone": "node scripts/build-standalone.js",
|
||||
"build:cli": "npm run cli:build && npm run build:standalone",
|
||||
"publish:cli": "npm run build:cli && npm run cli:publish",
|
||||
"prepare": "cd ../.. && bun husky",
|
||||
"db:push": "bunx drizzle-kit push",
|
||||
"db:studio": "bunx drizzle-kit studio",
|
||||
"db:migrate": "bunx drizzle-kit migrate",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"email:dev": "email dev --dir components/emails",
|
||||
"cli:build": "bun run build -w packages/simstudio",
|
||||
"cli:dev": "bun run build -w packages/simstudio && cd packages/simstudio && bun ./dist/index.js",
|
||||
"cli:publish": "cd packages/simstudio && bun publish",
|
||||
"cli:start": "cd packages/simstudio && bun ./dist/index.js start",
|
||||
"build:standalone": "bun scripts/build-standalone.js",
|
||||
"build:cli": "bun run cli:build && bun run build:standalone",
|
||||
"publish:cli": "bun run build:cli && bun run cli:publish",
|
||||
"check-types": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -59,7 +64,7 @@
|
||||
"@radix-ui/react-select": "^2.1.4",
|
||||
"@radix-ui/react-separator": "^1.1.2",
|
||||
"@radix-ui/react-slider": "^1.2.2",
|
||||
"@radix-ui/react-slot": "^1.2.2",
|
||||
"@radix-ui/react-slot": "1.2.2",
|
||||
"@radix-ui/react-switch": "^1.1.2",
|
||||
"@radix-ui/react-tabs": "^1.1.2",
|
||||
"@radix-ui/react-toggle": "^1.1.2",
|
||||
@@ -77,7 +82,7 @@
|
||||
"croner": "^9.0.0",
|
||||
"csv-parse": "^5.6.0",
|
||||
"csv-parser": "^3.2.0",
|
||||
"date-fns": "^3.6.0",
|
||||
"date-fns": "4.1.0",
|
||||
"drizzle-orm": "^0.41.0",
|
||||
"framer-motion": "^12.5.0",
|
||||
"freestyle-sandboxes": "^0.0.38",
|
||||
@@ -89,13 +94,14 @@
|
||||
"lucide-react": "^0.479.0",
|
||||
"mammoth": "^1.9.0",
|
||||
"next": "^15.3.2",
|
||||
"next-runtime-env": "3.3.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"openai": "^4.91.1",
|
||||
"pdf-parse": "^1.1.1",
|
||||
"postgres": "^3.4.5",
|
||||
"prismjs": "^1.30.0",
|
||||
"react": "19.1.0",
|
||||
"react-day-picker": "^8.10.1",
|
||||
"react-day-picker": "8.10.1",
|
||||
"react-dom": "19.1.0",
|
||||
"react-google-drive-picker": "^1.2.2",
|
||||
"react-hook-form": "^7.54.2",
|
||||
@@ -141,5 +147,10 @@
|
||||
"*.{js,jsx,ts,tsx,json,css,scss,md}": [
|
||||
"prettier --write"
|
||||
]
|
||||
}
|
||||
},
|
||||
"trustedDependencies": [
|
||||
"canvas",
|
||||
"better-sqlite3",
|
||||
"sharp"
|
||||
]
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user