Skip to content

Commit fdca736

Browse files
authored
v0.5.93: NextJS config changes, MCP and Blocks whitelisting, copilot keyboard shortcuts, audit logs
2 parents da46a38 + 86ca984 commit fdca736

File tree

126 files changed

+14416
-240
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

126 files changed

+14416
-240
lines changed

apps/sim/app/api/auth/oauth/disconnect/route.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
*
44
* @vitest-environment node
55
*/
6-
import { createMockLogger, createMockRequest } from '@sim/testing'
6+
import { auditMock, createMockLogger, createMockRequest } from '@sim/testing'
77
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
88

99
describe('OAuth Disconnect API Route', () => {
@@ -67,6 +67,8 @@ describe('OAuth Disconnect API Route', () => {
6767
vi.doMock('@/lib/webhooks/utils.server', () => ({
6868
syncAllWebhooksForCredentialSet: mockSyncAllWebhooksForCredentialSet,
6969
}))
70+
71+
vi.doMock('@/lib/audit/log', () => auditMock)
7072
})
7173

7274
afterEach(() => {

apps/sim/app/api/auth/oauth/disconnect/route.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
44
import { and, eq, like, or } from 'drizzle-orm'
55
import { type NextRequest, NextResponse } from 'next/server'
66
import { z } from 'zod'
7+
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
78
import { getSession } from '@/lib/auth'
89
import { generateRequestId } from '@/lib/core/utils/request'
910
import { syncAllWebhooksForCredentialSet } from '@/lib/webhooks/utils.server'
@@ -118,6 +119,20 @@ export async function POST(request: NextRequest) {
118119
}
119120
}
120121

122+
recordAudit({
123+
workspaceId: null,
124+
actorId: session.user.id,
125+
action: AuditAction.OAUTH_DISCONNECTED,
126+
resourceType: AuditResourceType.OAUTH,
127+
resourceId: providerId ?? provider,
128+
actorName: session.user.name ?? undefined,
129+
actorEmail: session.user.email ?? undefined,
130+
resourceName: provider,
131+
description: `Disconnected OAuth provider: ${provider}`,
132+
metadata: { provider, providerId },
133+
request,
134+
})
135+
121136
return NextResponse.json({ success: true }, { status: 200 })
122137
} catch (error) {
123138
logger.error(`[${requestId}] Error disconnecting OAuth provider`, error)

apps/sim/app/api/billing/credits/route.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { createLogger } from '@sim/logger'
22
import { type NextRequest, NextResponse } from 'next/server'
33
import { z } from 'zod'
4+
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
45
import { getSession } from '@/lib/auth'
56
import { getCreditBalance } from '@/lib/billing/credits/balance'
67
import { purchaseCredits } from '@/lib/billing/credits/purchase'
@@ -57,6 +58,17 @@ export async function POST(request: NextRequest) {
5758
return NextResponse.json({ error: result.error }, { status: 400 })
5859
}
5960

61+
recordAudit({
62+
actorId: session.user.id,
63+
actorName: session.user.name,
64+
actorEmail: session.user.email,
65+
action: AuditAction.CREDIT_PURCHASED,
66+
resourceType: AuditResourceType.BILLING,
67+
description: `Purchased $${validation.data.amount} in credits`,
68+
metadata: { amount: validation.data.amount, requestId: validation.data.requestId },
69+
request,
70+
})
71+
6072
return NextResponse.json({ success: true })
6173
} catch (error) {
6274
logger.error('Failed to purchase credits', { error, userId: session.user.id })

apps/sim/app/api/chat/manage/[id]/route.test.ts

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
*
44
* @vitest-environment node
55
*/
6-
import { loggerMock } from '@sim/testing'
6+
import { auditMock, loggerMock } from '@sim/testing'
77
import { NextRequest } from 'next/server'
88
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
99

10+
vi.mock('@/lib/audit/log', () => auditMock)
11+
1012
vi.mock('@/lib/core/config/feature-flags', () => ({
1113
isDev: true,
1214
isHosted: false,
@@ -216,8 +218,11 @@ describe('Chat Edit API Route', () => {
216218
workflowId: 'workflow-123',
217219
}
218220

219-
mockCheckChatAccess.mockResolvedValue({ hasAccess: true, chat: mockChat })
220-
mockLimit.mockResolvedValueOnce([]) // No identifier conflict
221+
mockCheckChatAccess.mockResolvedValue({
222+
hasAccess: true,
223+
chat: mockChat,
224+
workspaceId: 'workspace-123',
225+
})
221226

222227
const req = new NextRequest('http://localhost:3000/api/chat/manage/chat-123', {
223228
method: 'PATCH',
@@ -311,8 +316,11 @@ describe('Chat Edit API Route', () => {
311316
workflowId: 'workflow-123',
312317
}
313318

314-
mockCheckChatAccess.mockResolvedValue({ hasAccess: true, chat: mockChat })
315-
mockLimit.mockResolvedValueOnce([])
319+
mockCheckChatAccess.mockResolvedValue({
320+
hasAccess: true,
321+
chat: mockChat,
322+
workspaceId: 'workspace-123',
323+
})
316324

317325
const req = new NextRequest('http://localhost:3000/api/chat/manage/chat-123', {
318326
method: 'PATCH',
@@ -371,8 +379,11 @@ describe('Chat Edit API Route', () => {
371379
}),
372380
}))
373381

374-
mockCheckChatAccess.mockResolvedValue({ hasAccess: true })
375-
mockWhere.mockResolvedValue(undefined)
382+
mockCheckChatAccess.mockResolvedValue({
383+
hasAccess: true,
384+
chat: { title: 'Test Chat', workflowId: 'workflow-123' },
385+
workspaceId: 'workspace-123',
386+
})
376387

377388
const req = new NextRequest('http://localhost:3000/api/chat/manage/chat-123', {
378389
method: 'DELETE',
@@ -393,8 +404,11 @@ describe('Chat Edit API Route', () => {
393404
}),
394405
}))
395406

396-
mockCheckChatAccess.mockResolvedValue({ hasAccess: true })
397-
mockWhere.mockResolvedValue(undefined)
407+
mockCheckChatAccess.mockResolvedValue({
408+
hasAccess: true,
409+
chat: { title: 'Test Chat', workflowId: 'workflow-123' },
410+
workspaceId: 'workspace-123',
411+
})
398412

399413
const req = new NextRequest('http://localhost:3000/api/chat/manage/chat-123', {
400414
method: 'DELETE',

apps/sim/app/api/chat/manage/[id]/route.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
44
import { eq } from 'drizzle-orm'
55
import type { NextRequest } from 'next/server'
66
import { z } from 'zod'
7+
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
78
import { getSession } from '@/lib/auth'
89
import { isDev } from '@/lib/core/config/feature-flags'
910
import { encryptSecret } from '@/lib/core/security/encryption'
@@ -103,7 +104,11 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
103104
try {
104105
const validatedData = chatUpdateSchema.parse(body)
105106

106-
const { hasAccess, chat: existingChatRecord } = await checkChatAccess(chatId, session.user.id)
107+
const {
108+
hasAccess,
109+
chat: existingChatRecord,
110+
workspaceId: chatWorkspaceId,
111+
} = await checkChatAccess(chatId, session.user.id)
107112

108113
if (!hasAccess || !existingChatRecord) {
109114
return createErrorResponse('Chat not found or access denied', 404)
@@ -217,6 +222,19 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
217222

218223
logger.info(`Chat "${chatId}" updated successfully`)
219224

225+
recordAudit({
226+
workspaceId: chatWorkspaceId || null,
227+
actorId: session.user.id,
228+
actorName: session.user.name,
229+
actorEmail: session.user.email,
230+
action: AuditAction.CHAT_UPDATED,
231+
resourceType: AuditResourceType.CHAT,
232+
resourceId: chatId,
233+
resourceName: title || existingChatRecord.title,
234+
description: `Updated chat deployment "${title || existingChatRecord.title}"`,
235+
request,
236+
})
237+
220238
return createSuccessResponse({
221239
id: chatId,
222240
chatUrl,
@@ -252,7 +270,11 @@ export async function DELETE(
252270
return createErrorResponse('Unauthorized', 401)
253271
}
254272

255-
const { hasAccess } = await checkChatAccess(chatId, session.user.id)
273+
const {
274+
hasAccess,
275+
chat: chatRecord,
276+
workspaceId: chatWorkspaceId,
277+
} = await checkChatAccess(chatId, session.user.id)
256278

257279
if (!hasAccess) {
258280
return createErrorResponse('Chat not found or access denied', 404)
@@ -262,6 +284,19 @@ export async function DELETE(
262284

263285
logger.info(`Chat "${chatId}" deleted successfully`)
264286

287+
recordAudit({
288+
workspaceId: chatWorkspaceId || null,
289+
actorId: session.user.id,
290+
actorName: session.user.name,
291+
actorEmail: session.user.email,
292+
action: AuditAction.CHAT_DELETED,
293+
resourceType: AuditResourceType.CHAT,
294+
resourceId: chatId,
295+
resourceName: chatRecord?.title || chatId,
296+
description: `Deleted chat deployment "${chatRecord?.title || chatId}"`,
297+
request: _request,
298+
})
299+
265300
return createSuccessResponse({
266301
message: 'Chat deployment deleted successfully',
267302
})

apps/sim/app/api/chat/route.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { NextRequest } from 'next/server'
21
/**
32
* Tests for chat API route
43
*
54
* @vitest-environment node
65
*/
6+
import { auditMock } from '@sim/testing'
7+
import { NextRequest } from 'next/server'
78
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
89

910
describe('Chat API Route', () => {
@@ -30,6 +31,8 @@ describe('Chat API Route', () => {
3031
mockInsert.mockReturnValue({ values: mockValues })
3132
mockValues.mockReturnValue({ returning: mockReturning })
3233

34+
vi.doMock('@/lib/audit/log', () => auditMock)
35+
3336
vi.doMock('@sim/db', () => ({
3437
db: {
3538
select: mockSelect,

apps/sim/app/api/chat/route.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { eq } from 'drizzle-orm'
55
import type { NextRequest } from 'next/server'
66
import { v4 as uuidv4 } from 'uuid'
77
import { z } from 'zod'
8+
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
89
import { getSession } from '@/lib/auth'
910
import { isDev } from '@/lib/core/config/feature-flags'
1011
import { encryptSecret } from '@/lib/core/security/encryption'
@@ -42,7 +43,7 @@ const chatSchema = z.object({
4243
.default([]),
4344
})
4445

45-
export async function GET(request: NextRequest) {
46+
export async function GET(_request: NextRequest) {
4647
try {
4748
const session = await getSession()
4849

@@ -174,7 +175,7 @@ export async function POST(request: NextRequest) {
174175
userId: session.user.id,
175176
identifier,
176177
title,
177-
description: description || '',
178+
description: description || null,
178179
customizations: mergedCustomizations,
179180
isActive: true,
180181
authType,
@@ -224,6 +225,20 @@ export async function POST(request: NextRequest) {
224225
// Silently fail
225226
}
226227

228+
recordAudit({
229+
workspaceId: workflowRecord.workspaceId || null,
230+
actorId: session.user.id,
231+
actorName: session.user.name,
232+
actorEmail: session.user.email,
233+
action: AuditAction.CHAT_DEPLOYED,
234+
resourceType: AuditResourceType.CHAT,
235+
resourceId: id,
236+
resourceName: title,
237+
description: `Deployed chat "${title}"`,
238+
metadata: { workflowId, identifier, authType },
239+
request,
240+
})
241+
227242
return createSuccessResponse({
228243
id,
229244
chatUrl,

apps/sim/app/api/chat/utils.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export async function checkWorkflowAccessForChatCreation(
5252
export async function checkChatAccess(
5353
chatId: string,
5454
userId: string
55-
): Promise<{ hasAccess: boolean; chat?: any }> {
55+
): Promise<{ hasAccess: boolean; chat?: any; workspaceId?: string }> {
5656
const chatData = await db
5757
.select({
5858
chat: chat,
@@ -78,7 +78,9 @@ export async function checkChatAccess(
7878
action: 'admin',
7979
})
8080

81-
return authorization.allowed ? { hasAccess: true, chat: chatRecord } : { hasAccess: false }
81+
return authorization.allowed
82+
? { hasAccess: true, chat: chatRecord, workspaceId: workflowWorkspaceId }
83+
: { hasAccess: false }
8284
}
8385

8486
export async function validateChatAuth(

apps/sim/app/api/credential-sets/[id]/invite/[invitationId]/route.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
44
import { and, eq } from 'drizzle-orm'
55
import { type NextRequest, NextResponse } from 'next/server'
66
import { getEmailSubject, renderPollingGroupInvitationEmail } from '@/components/emails'
7+
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
78
import { getSession } from '@/lib/auth'
89
import { hasCredentialSetsAccess } from '@/lib/billing'
910
import { getBaseUrl } from '@/lib/core/utils/urls'
@@ -148,6 +149,19 @@ export async function POST(
148149
userId: session.user.id,
149150
})
150151

152+
recordAudit({
153+
actorId: session.user.id,
154+
actorName: session.user.name,
155+
actorEmail: session.user.email,
156+
action: AuditAction.CREDENTIAL_SET_INVITATION_RESENT,
157+
resourceType: AuditResourceType.CREDENTIAL_SET,
158+
resourceId: id,
159+
resourceName: result.set.name,
160+
description: `Resent credential set invitation to ${invitation.email}`,
161+
metadata: { invitationId, email: invitation.email },
162+
request: req,
163+
})
164+
151165
return NextResponse.json({ success: true })
152166
} catch (error) {
153167
logger.error('Error resending invitation', error)

apps/sim/app/api/credential-sets/[id]/invite/route.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { and, eq } from 'drizzle-orm'
55
import { type NextRequest, NextResponse } from 'next/server'
66
import { z } from 'zod'
77
import { getEmailSubject, renderPollingGroupInvitationEmail } from '@/components/emails'
8+
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
89
import { getSession } from '@/lib/auth'
910
import { hasCredentialSetsAccess } from '@/lib/billing'
1011
import { getBaseUrl } from '@/lib/core/utils/urls'
@@ -175,6 +176,19 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
175176
emailSent: !!email,
176177
})
177178

179+
recordAudit({
180+
workspaceId: null,
181+
actorId: session.user.id,
182+
action: AuditAction.CREDENTIAL_SET_INVITATION_CREATED,
183+
resourceType: AuditResourceType.CREDENTIAL_SET,
184+
resourceId: id,
185+
actorName: session.user.name ?? undefined,
186+
actorEmail: session.user.email ?? undefined,
187+
resourceName: result.set.name,
188+
description: `Created invitation for credential set "${result.set.name}"${email ? ` to ${email}` : ''}`,
189+
request: req,
190+
})
191+
178192
return NextResponse.json({
179193
invitation: {
180194
...invitation,
@@ -235,6 +249,19 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i
235249
)
236250
)
237251

252+
recordAudit({
253+
workspaceId: null,
254+
actorId: session.user.id,
255+
action: AuditAction.CREDENTIAL_SET_INVITATION_REVOKED,
256+
resourceType: AuditResourceType.CREDENTIAL_SET,
257+
resourceId: id,
258+
actorName: session.user.name ?? undefined,
259+
actorEmail: session.user.email ?? undefined,
260+
resourceName: result.set.name,
261+
description: `Revoked invitation "${invitationId}" for credential set "${result.set.name}"`,
262+
request: req,
263+
})
264+
238265
return NextResponse.json({ success: true })
239266
} catch (error) {
240267
logger.error('Error cancelling invitation', error)

0 commit comments

Comments
 (0)