mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
* feat(audit-log): add persistent audit log system with comprehensive route instrumentation
* fix(audit-log): address PR review — nullable workspaceId, enum usage, remove redundant queries
- Make audit_log.workspace_id nullable with ON DELETE SET NULL (logs survive workspace/user deletion)
- Make audit_log.actor_id nullable with ON DELETE SET NULL
- Replace all 53 routes' string literal action/resourceType with AuditAction.X and AuditResourceType.X enums
- Fix empty workspaceId ('') → null for OAuth, form, and org routes to avoid FK violations
- Remove redundant DB queries in chat manage route (use checkChatAccess return data)
- Fix organization routes to pass workspaceId: null instead of organizationId
* fix(audit-log): replace remaining workspaceId '' fallbacks with null
* fix(audit-log): credential-set org IDs, workspace deletion FK, actorId fallback, string literal action
* reran migrations
* fix(mcp,audit): tighten env var domain bypass, add post-resolution check, form workspaceId
- Only bypass MCP domain check when env var is in hostname/authority, not path/query
- Add post-resolution validateMcpDomain call in test-connection endpoint
- Match client-side isDomainAllowed to same hostname-only bypass logic
- Return workspaceId from checkFormAccess, use in form audit logs
- Add 49 comprehensive domain-check tests covering all edge cases
* fix(mcp): stateful regex lastIndex bug, RFC 3986 authority parsing
- Remove /g flag from module-level ENV_VAR_PATTERN to avoid lastIndex state
- Create fresh regex instances per call in server-side hasEnvVarInHostname
- Fix authority extraction to terminate at /, ?, or # per RFC 3986
- Prevents bypass via https://evil.com?token={{SECRET}} (no path)
- Add test cases for query-only and fragment-only env var URLs (53 total)
* fix(audit-log): try/catch for never-throw contract, accept null actorName/Email, fix misleading action
- Wrap recordAudit body in try/catch so nanoid() or header extraction can't throw
- Accept string | null for actorName and actorEmail (session.user.name can be null)
- Normalize null -> undefined before insert to match DB column types
- Fix org members route: ORG_MEMBER_ADDED -> ORG_INVITATION_CREATED (sends invite, not adds member)
* improvement(audit-log): add resource names and specific invitation actions
* fix(audit-log): use validated chat record, add mock sync tests
148 lines
4.8 KiB
TypeScript
148 lines
4.8 KiB
TypeScript
import { createLogger } from '@sim/logger'
|
||
import { type NextRequest, NextResponse } from 'next/server'
|
||
import { z } from 'zod'
|
||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||
import { getSession } from '@/lib/auth'
|
||
import { PlatformEvents } from '@/lib/core/telemetry'
|
||
import { generateRequestId } from '@/lib/core/utils/request'
|
||
import { createKnowledgeBase, getKnowledgeBases } from '@/lib/knowledge/service'
|
||
|
||
const logger = createLogger('KnowledgeBaseAPI')
|
||
|
||
/**
|
||
* Schema for creating a knowledge base
|
||
*
|
||
* Chunking config units:
|
||
* - maxSize: tokens (1 token ≈ 4 characters)
|
||
* - minSize: characters
|
||
* - overlap: tokens (1 token ≈ 4 characters)
|
||
*/
|
||
const CreateKnowledgeBaseSchema = z.object({
|
||
name: z.string().min(1, 'Name is required'),
|
||
description: z.string().optional(),
|
||
workspaceId: z.string().min(1, 'Workspace ID is required'),
|
||
embeddingModel: z.literal('text-embedding-3-small').default('text-embedding-3-small'),
|
||
embeddingDimension: z.literal(1536).default(1536),
|
||
chunkingConfig: z
|
||
.object({
|
||
/** Maximum chunk size in tokens (1 token ≈ 4 characters) */
|
||
maxSize: z.number().min(100).max(4000).default(1024),
|
||
/** Minimum chunk size in characters */
|
||
minSize: z.number().min(1).max(2000).default(100),
|
||
/** Overlap between chunks in tokens (1 token ≈ 4 characters) */
|
||
overlap: z.number().min(0).max(500).default(200),
|
||
})
|
||
.default({
|
||
maxSize: 1024,
|
||
minSize: 100,
|
||
overlap: 200,
|
||
})
|
||
.refine(
|
||
(data) => {
|
||
// Convert maxSize from tokens to characters for comparison (1 token ≈ 4 chars)
|
||
const maxSizeInChars = data.maxSize * 4
|
||
return data.minSize < maxSizeInChars
|
||
},
|
||
{
|
||
message: 'Min chunk size (characters) must be less than max chunk size (tokens × 4)',
|
||
}
|
||
),
|
||
})
|
||
|
||
export async function GET(req: NextRequest) {
|
||
const requestId = generateRequestId()
|
||
|
||
try {
|
||
const session = await getSession()
|
||
if (!session?.user?.id) {
|
||
logger.warn(`[${requestId}] Unauthorized knowledge base access attempt`)
|
||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||
}
|
||
|
||
const { searchParams } = new URL(req.url)
|
||
const workspaceId = searchParams.get('workspaceId')
|
||
|
||
const knowledgeBasesWithCounts = await getKnowledgeBases(session.user.id, workspaceId)
|
||
|
||
return NextResponse.json({
|
||
success: true,
|
||
data: knowledgeBasesWithCounts,
|
||
})
|
||
} catch (error) {
|
||
logger.error(`[${requestId}] Error fetching knowledge bases`, error)
|
||
return NextResponse.json({ error: 'Failed to fetch knowledge bases' }, { status: 500 })
|
||
}
|
||
}
|
||
|
||
export async function POST(req: NextRequest) {
|
||
const requestId = generateRequestId()
|
||
|
||
try {
|
||
const session = await getSession()
|
||
if (!session?.user?.id) {
|
||
logger.warn(`[${requestId}] Unauthorized knowledge base creation attempt`)
|
||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||
}
|
||
|
||
const body = await req.json()
|
||
|
||
try {
|
||
const validatedData = CreateKnowledgeBaseSchema.parse(body)
|
||
|
||
const createData = {
|
||
...validatedData,
|
||
userId: session.user.id,
|
||
}
|
||
|
||
const newKnowledgeBase = await createKnowledgeBase(createData, requestId)
|
||
|
||
try {
|
||
PlatformEvents.knowledgeBaseCreated({
|
||
knowledgeBaseId: newKnowledgeBase.id,
|
||
name: validatedData.name,
|
||
workspaceId: validatedData.workspaceId,
|
||
})
|
||
} catch {
|
||
// Telemetry should not fail the operation
|
||
}
|
||
|
||
logger.info(
|
||
`[${requestId}] Knowledge base created: ${newKnowledgeBase.id} for user ${session.user.id}`
|
||
)
|
||
|
||
recordAudit({
|
||
workspaceId: validatedData.workspaceId,
|
||
actorId: session.user.id,
|
||
actorName: session.user.name,
|
||
actorEmail: session.user.email,
|
||
action: AuditAction.KNOWLEDGE_BASE_CREATED,
|
||
resourceType: AuditResourceType.KNOWLEDGE_BASE,
|
||
resourceId: newKnowledgeBase.id,
|
||
resourceName: validatedData.name,
|
||
description: `Created knowledge base "${validatedData.name}"`,
|
||
metadata: { name: validatedData.name },
|
||
request: req,
|
||
})
|
||
|
||
return NextResponse.json({
|
||
success: true,
|
||
data: newKnowledgeBase,
|
||
})
|
||
} catch (validationError) {
|
||
if (validationError instanceof z.ZodError) {
|
||
logger.warn(`[${requestId}] Invalid knowledge base data`, {
|
||
errors: validationError.errors,
|
||
})
|
||
return NextResponse.json(
|
||
{ error: 'Invalid request data', details: validationError.errors },
|
||
{ status: 400 }
|
||
)
|
||
}
|
||
throw validationError
|
||
}
|
||
} catch (error) {
|
||
logger.error(`[${requestId}] Error creating knowledge base`, error)
|
||
return NextResponse.json({ error: 'Failed to create knowledge base' }, { status: 500 })
|
||
}
|
||
}
|