Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0c0b6bf
fix(oauth): gdrive picker race condition, token route cleanup
icecrasher321 Aug 20, 2025
9a5b035
fix test
icecrasher321 Aug 20, 2025
7530fb9
Merge pull request #1055 from simstudioai/fix/picker-race-cond
icecrasher321 Aug 20, 2025
6fd6f92
feat(mailer): consolidated all emailing to mailer service, added supp…
waleedlatif1 Aug 20, 2025
cea42f5
improvement(gpt-5): added reasoning level and verbosity to gpt-5 mode…
waleedlatif1 Aug 21, 2025
c795fc8
feat(azure-openai): allow usage of azure-openai for knowledgebase upl…
waleedlatif1 Aug 21, 2025
26e6286
fix(billing): fix team plan upgrade (#1053)
waleedlatif1 Aug 21, 2025
9ffaf30
feat(input-format): add value field to test input formats (#1059)
icecrasher321 Aug 21, 2025
da707fa
improvement(gh-action): add gh action to deploy to correct environmen…
icecrasher321 Aug 21, 2025
1db72dc
pin version
icecrasher321 Aug 21, 2025
dd74267
feat(nextjs): upgrade nextjs to 15.5 (#1062)
waleedlatif1 Aug 21, 2025
d9e5777
use personal access token
icecrasher321 Aug 21, 2025
9973b2c
Merge branch 'staging' of github.com:simstudioai/sim into staging
icecrasher321 Aug 21, 2025
71e2994
improvement(trigger): upgrade trigger (#1063)
icecrasher321 Aug 21, 2025
07b0597
improvement(trigger): upgrade import path for trigger (#1065)
icecrasher321 Aug 21, 2025
a6888da
fix(semantics): fix incorrect imports (#1066)
waleedlatif1 Aug 21, 2025
5caef3a
fix(input-format): first time execution bug (#1068)
icecrasher321 Aug 21, 2025
cb7ce86
fix(msverify): changed consent for microsoft (#1057)
aadamgough Aug 21, 2025
692ba69
fix type
icecrasher321 Aug 21, 2025
ff43528
fix(gpt-5): fixed verbosity and reasoning params (#1069)
waleedlatif1 Aug 21, 2025
c2ded1f
fix(theme-provider): preventing flash on page load (#1067)
emir-karabeg Aug 21, 2025
154d9ee
fix(gpt-5): fix chat-completions api (#1070)
waleedlatif1 Aug 21, 2025
c691209
fix placeholder text
icecrasher321 Aug 21, 2025
db1cf8a
fix(placeholder): fix starter block placeholder (#1071)
waleedlatif1 Aug 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .github/workflows/trigger-deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Trigger.dev Deploy

on:
push:
branches:
- main
- staging

jobs:
deploy:
name: Trigger.dev Deploy
runs-on: ubuntu-latest
concurrency:
group: trigger-deploy-${{ github.ref }}
cancel-in-progress: false
env:
TRIGGER_ACCESS_TOKEN: ${{ secrets.TRIGGER_ACCESS_TOKEN }}
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 'lts/*'

- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- name: Install dependencies
run: bun install

- name: Deploy to Staging
if: github.ref == 'refs/heads/staging'
working-directory: ./apps/sim
run: npx --yes trigger.dev@4.0.0 deploy -e staging

- name: Deploy to Production
if: github.ref == 'refs/heads/main'
working-directory: ./apps/sim
run: npx --yes trigger.dev@4.0.0 deploy

15 changes: 9 additions & 6 deletions apps/docs/content/docs/tools/microsoft_excel.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,7 @@ Read data from a Microsoft Excel spreadsheet

| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Excel spreadsheet data and metadata |
| `data` | object | Range data from the spreadsheet |

### `microsoft_excel_write`

Expand All @@ -136,8 +135,11 @@ Write data to a Microsoft Excel spreadsheet

| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Write operation results and metadata |
| `updatedRange` | string | The range that was updated |
| `updatedRows` | number | Number of rows that were updated |
| `updatedColumns` | number | Number of columns that were updated |
| `updatedCells` | number | Number of cells that were updated |
| `metadata` | object | Spreadsheet metadata |

### `microsoft_excel_table_add`

Expand All @@ -155,8 +157,9 @@ Add new rows to a Microsoft Excel table

| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Table add operation results and metadata |
| `index` | number | Index of the first row that was added |
| `values` | array | Array of rows that were added to the table |
| `metadata` | object | Spreadsheet metadata |



Expand Down
2 changes: 0 additions & 2 deletions apps/sim/app/api/auth/oauth/token/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,12 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
}

// Check if the access token is valid
if (!credential.accessToken) {
logger.warn(`[${requestId}] No access token available for credential`)
return NextResponse.json({ error: 'No access token available' }, { status: 400 })
}

try {
// Refresh the token if needed
const { accessToken } = await refreshTokenIfNeeded(requestId, credential, credentialId)
return NextResponse.json({ accessToken }, { status: 200 })
} catch (_error) {
Expand Down
54 changes: 29 additions & 25 deletions apps/sim/app/api/auth/oauth/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { and, eq } from 'drizzle-orm'
import { and, desc, eq } from 'drizzle-orm'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { refreshOAuthToken } from '@/lib/oauth/oauth'
Expand Down Expand Up @@ -70,7 +70,8 @@ export async function getOAuthToken(userId: string, providerId: string): Promise
})
.from(account)
.where(and(eq(account.userId, userId), eq(account.providerId, providerId)))
.orderBy(account.createdAt)
// Always use the most recently updated credential for this provider
.orderBy(desc(account.updatedAt))
.limit(1)

if (connections.length === 0) {
Expand All @@ -80,19 +81,13 @@ export async function getOAuthToken(userId: string, providerId: string): Promise

const credential = connections[0]

// Check if we have a valid access token
if (!credential.accessToken) {
logger.warn(`Access token is null for user ${userId}, provider ${providerId}`)
return null
}

// Check if the token is expired and needs refreshing
// Determine whether we should refresh: missing token OR expired token
const now = new Date()
const tokenExpiry = credential.accessTokenExpiresAt
// Only refresh if we have an expiration time AND it's expired AND we have a refresh token
const needsRefresh = tokenExpiry && tokenExpiry < now && !!credential.refreshToken
const shouldAttemptRefresh =
!!credential.refreshToken && (!credential.accessToken || (tokenExpiry && tokenExpiry < now))

if (needsRefresh) {
if (shouldAttemptRefresh) {
logger.info(
`Access token expired for user ${userId}, provider ${providerId}. Attempting to refresh.`
)
Expand Down Expand Up @@ -141,6 +136,13 @@ export async function getOAuthToken(userId: string, providerId: string): Promise
}
}

if (!credential.accessToken) {
logger.warn(
`Access token is null and no refresh attempted or available for user ${userId}, provider ${providerId}`
)
return null
}

logger.info(`Found valid OAuth token for user ${userId}, provider ${providerId}`)
return credential.accessToken
}
Expand All @@ -164,19 +166,21 @@ export async function refreshAccessTokenIfNeeded(
return null
}

// Check if we need to refresh the token
// Decide if we should refresh: token missing OR expired
const expiresAt = credential.accessTokenExpiresAt
const now = new Date()
// Only refresh if we have an expiration time AND it's expired
// If no expiration time is set (newly created credentials), assume token is valid
const needsRefresh = expiresAt && expiresAt <= now
const shouldRefresh =
!!credential.refreshToken && (!credential.accessToken || (expiresAt && expiresAt <= now))

const accessToken = credential.accessToken

if (needsRefresh && credential.refreshToken) {
if (shouldRefresh) {
logger.info(`[${requestId}] Token expired, attempting to refresh for credential`)
try {
const refreshedToken = await refreshOAuthToken(credential.providerId, credential.refreshToken)
const refreshedToken = await refreshOAuthToken(
credential.providerId,
credential.refreshToken!
)

if (!refreshedToken) {
logger.error(`[${requestId}] Failed to refresh token for credential: ${credentialId}`, {
Expand Down Expand Up @@ -217,6 +221,7 @@ export async function refreshAccessTokenIfNeeded(
return null
}
} else if (!accessToken) {
// We have no access token and either no refresh token or not eligible to refresh
logger.error(`[${requestId}] Missing access token for credential`)
return null
}
Expand All @@ -233,21 +238,20 @@ export async function refreshTokenIfNeeded(
credential: any,
credentialId: string
): Promise<{ accessToken: string; refreshed: boolean }> {
// Check if we need to refresh the token
// Decide if we should refresh: token missing OR expired
const expiresAt = credential.accessTokenExpiresAt
const now = new Date()
// Only refresh if we have an expiration time AND it's expired
// If no expiration time is set (newly created credentials), assume token is valid
const needsRefresh = expiresAt && expiresAt <= now
const shouldRefresh =
!!credential.refreshToken && (!credential.accessToken || (expiresAt && expiresAt <= now))

// If token is still valid, return it directly
if (!needsRefresh || !credential.refreshToken) {
// If token appears valid and present, return it directly
if (!shouldRefresh) {
logger.info(`[${requestId}] Access token is valid`)
return { accessToken: credential.accessToken, refreshed: false }
}

try {
const refreshResult = await refreshOAuthToken(credential.providerId, credential.refreshToken)
const refreshResult = await refreshOAuthToken(credential.providerId, credential.refreshToken!)

if (!refreshResult) {
logger.error(`[${requestId}] Failed to refresh token for credential`)
Expand Down
81 changes: 33 additions & 48 deletions apps/sim/app/api/help/route.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { type NextRequest, NextResponse } from 'next/server'
import { Resend } from 'resend'
import { z } from 'zod'
import { renderHelpConfirmationEmail } from '@/components/emails'
import { getSession } from '@/lib/auth'
import { sendEmail } from '@/lib/email/mailer'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'
import { getEmailDomain } from '@/lib/urls/utils'

const resend = env.RESEND_API_KEY ? new Resend(env.RESEND_API_KEY) : null
const logger = createLogger('HelpAPI')

const helpFormSchema = z.object({
Expand All @@ -28,18 +28,6 @@ export async function POST(req: NextRequest) {

const email = session.user.email

// Check if Resend API key is configured
if (!resend) {
logger.error(`[${requestId}] RESEND_API_KEY not configured`)
return NextResponse.json(
{
error:
'Email service not configured. Please set RESEND_API_KEY in environment variables.',
},
{ status: 500 }
)
}

// Handle multipart form data
const formData = await req.formData()

Expand All @@ -54,18 +42,18 @@ export async function POST(req: NextRequest) {
})

// Validate the form data
const result = helpFormSchema.safeParse({
const validationResult = helpFormSchema.safeParse({
subject,
message,
type,
})

if (!result.success) {
if (!validationResult.success) {
logger.warn(`[${requestId}] Invalid help request data`, {
errors: result.error.format(),
errors: validationResult.error.format(),
})
return NextResponse.json(
{ error: 'Invalid request data', details: result.error.format() },
{ error: 'Invalid request data', details: validationResult.error.format() },
{ status: 400 }
)
}
Expand Down Expand Up @@ -103,63 +91,60 @@ ${message}
emailText += `\n\n${images.length} image(s) attached.`
}

// Send email using Resend
const { error } = await resend.emails.send({
from: `Sim <noreply@${env.EMAIL_DOMAIN || getEmailDomain()}>`,
const emailResult = await sendEmail({
to: [`help@${env.EMAIL_DOMAIN || getEmailDomain()}`],
subject: `[${type.toUpperCase()}] ${subject}`,
replyTo: email,
text: emailText,
from: `${env.SENDER_NAME || 'Sim'} <noreply@${env.EMAIL_DOMAIN || getEmailDomain()}>`,
replyTo: email,
emailType: 'transactional',
attachments: images.map((image) => ({
filename: image.filename,
content: image.content.toString('base64'),
contentType: image.contentType,
disposition: 'attachment', // Explicitly set as attachment
disposition: 'attachment',
})),
})

if (error) {
logger.error(`[${requestId}] Error sending help request email`, error)
if (!emailResult.success) {
logger.error(`[${requestId}] Error sending help request email`, emailResult.message)
return NextResponse.json({ error: 'Failed to send email' }, { status: 500 })
}

logger.info(`[${requestId}] Help request email sent successfully`)

// Send confirmation email to the user
await resend.emails
.send({
from: `Sim <noreply@${env.EMAIL_DOMAIN || getEmailDomain()}>`,
try {
const confirmationHtml = await renderHelpConfirmationEmail(
email,
type as 'bug' | 'feedback' | 'feature_request' | 'other',
images.length
)

await sendEmail({
to: [email],
subject: `Your ${type} request has been received: ${subject}`,
text: `
Hello,

Thank you for your ${type} submission. We've received your request and will get back to you as soon as possible.

Your message:
${message}

${images.length > 0 ? `You attached ${images.length} image(s).` : ''}

Best regards,
The Sim Team
`,
html: confirmationHtml,
from: `${env.SENDER_NAME || 'Sim'} <noreply@${env.EMAIL_DOMAIN || getEmailDomain()}>`,
replyTo: `help@${env.EMAIL_DOMAIN || getEmailDomain()}`,
emailType: 'transactional',
})
.catch((err) => {
logger.warn(`[${requestId}] Failed to send confirmation email`, err)
})
} catch (err) {
logger.warn(`[${requestId}] Failed to send confirmation email`, err)
}

return NextResponse.json(
{ success: true, message: 'Help request submitted successfully' },
{ status: 200 }
)
} catch (error) {
// Check if error is related to missing API key
if (error instanceof Error && error.message.includes('API key')) {
logger.error(`[${requestId}] API key configuration error`, error)
if (error instanceof Error && error.message.includes('not configured')) {
logger.error(`[${requestId}] Email service configuration error`, error)
return NextResponse.json(
{ error: 'Email service configuration error. Please check your RESEND_API_KEY.' },
{
error:
'Email service configuration error. Please check your email service configuration.',
},
{ status: 500 }
)
}
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/app/api/jobs/[jobId]/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { runs } from '@trigger.dev/sdk/v3'
import { runs } from '@trigger.dev/sdk'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
Expand Down
Loading