feat: production readiness — 3-extension deploy with security hardening and observability
- Strip extensions to enable-banking, ai-categorization, ai-chat only - Remove push-notifications cron from vercel.json - Add security headers (HSTS, CSP, X-Frame-Options, Permissions-Policy) - Add /api/health endpoint for uptime monitoring - Add env var validation in ensureInitialized() - Fix SIE4 #IB opening balance records from year-end closing entry - Replace in-memory ai-chat rate limiter with Supabase-backed distributed rate limiting - Add Sentry error tracking scaffolding (@sentry/nextjs, instrumentation hook) - Add AI token usage tracking (migration 047, usage-tracker, wired into both AI extensions) - Include pending enable-banking and dashboard improvements Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
1a1c6ba40f
commit
13725ffc16
@@ -4,26 +4,56 @@ import { generateChatResponse, streamChatResponse, streamRoutedResponse } from '
|
||||
import { CHATBOT_CONFIG } from '@/extensions/general/ai-chat/chatbot/config'
|
||||
import type { ChatMessage, ChatRequest, SourceReference, ArtifactSpec } from '@/types/chat'
|
||||
|
||||
// Simple in-memory rate limiting (per user)
|
||||
const rateLimitMap = new Map<string, { count: number; resetTime: number }>()
|
||||
// Distributed rate limiting backed by Supabase extension_data table
|
||||
import type { SupabaseClient } from '@supabase/supabase-js'
|
||||
|
||||
function checkRateLimit(userId: string): boolean {
|
||||
interface RateLimitState {
|
||||
count: number
|
||||
window_start: number
|
||||
}
|
||||
|
||||
async function checkRateLimitDB(supabase: SupabaseClient, userId: string): Promise<boolean> {
|
||||
const now = Date.now()
|
||||
const limit = rateLimitMap.get(userId)
|
||||
const WINDOW_MS = 60000
|
||||
|
||||
if (!limit || now > limit.resetTime) {
|
||||
rateLimitMap.set(userId, {
|
||||
count: 1,
|
||||
resetTime: now + 60000, // 1 minute window
|
||||
})
|
||||
const { data } = await supabase
|
||||
.from('extension_data')
|
||||
.select('value')
|
||||
.eq('user_id', userId)
|
||||
.eq('extension_id', 'ai-chat')
|
||||
.eq('key', 'rate_limit')
|
||||
.single()
|
||||
|
||||
const state = data?.value as RateLimitState | null
|
||||
|
||||
if (!state || now - state.window_start > WINDOW_MS) {
|
||||
// New window
|
||||
await supabase.from('extension_data').upsert(
|
||||
{
|
||||
user_id: userId,
|
||||
extension_id: 'ai-chat',
|
||||
key: 'rate_limit',
|
||||
value: { count: 1, window_start: now },
|
||||
},
|
||||
{ onConflict: 'user_id,extension_id,key' }
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
if (limit.count >= CHATBOT_CONFIG.rateLimitPerMinute) {
|
||||
if (state.count >= CHATBOT_CONFIG.rateLimitPerMinute) {
|
||||
return false
|
||||
}
|
||||
|
||||
limit.count++
|
||||
// Increment count
|
||||
await supabase.from('extension_data').upsert(
|
||||
{
|
||||
user_id: userId,
|
||||
extension_id: 'ai-chat',
|
||||
key: 'rate_limit',
|
||||
value: { count: state.count + 1, window_start: state.window_start },
|
||||
},
|
||||
{ onConflict: 'user_id,extension_id,key' }
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -40,7 +70,7 @@ async function handlePostChat(
|
||||
const supabase = await createClient()
|
||||
|
||||
// Rate limiting
|
||||
if (!checkRateLimit(userId)) {
|
||||
if (!await checkRateLimitDB(supabase, userId)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Rate limit exceeded. Please wait a moment.' },
|
||||
{ status: 429 }
|
||||
@@ -129,7 +159,7 @@ async function handlePostChat(
|
||||
const conversationHistory = (history || []) as ChatMessage[]
|
||||
|
||||
// Generate AI response
|
||||
const result = await generateChatResponse(message.trim(), conversationHistory)
|
||||
const result = await generateChatResponse(message.trim(), conversationHistory, { supabase, userId })
|
||||
|
||||
// Save assistant message
|
||||
const { data: assistantMessage, error: assistantMsgError } = await supabase
|
||||
@@ -177,7 +207,7 @@ async function handlePostStream(
|
||||
const { createClient } = await import('@/lib/supabase/server')
|
||||
const supabase = await createClient()
|
||||
|
||||
if (!checkRateLimit(userId)) {
|
||||
if (!await checkRateLimitDB(supabase, userId)) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Rate limit exceeded' }),
|
||||
{ status: 429, headers: { 'Content-Type': 'application/json' } }
|
||||
|
||||
@@ -17,6 +17,7 @@ import { streamAgentResponse, type ToolResultEntry } from './agent'
|
||||
import { generateArtifact, type ArtifactSpec } from './artifacts'
|
||||
import type { ChatMessage, SourceReference } from '@/types/chat'
|
||||
import type { SupabaseClient } from '@supabase/supabase-js'
|
||||
import { trackTokenUsage } from '@/lib/ai/usage-tracker'
|
||||
|
||||
// Initialize the LLM
|
||||
function getChatModel() {
|
||||
@@ -35,7 +36,8 @@ export interface ChatResult {
|
||||
|
||||
export async function generateChatResponse(
|
||||
userMessage: string,
|
||||
conversationHistory: ChatMessage[]
|
||||
conversationHistory: ChatMessage[],
|
||||
tracking?: { supabase: SupabaseClient; userId: string }
|
||||
): Promise<ChatResult> {
|
||||
// 1. Retrieve relevant documents
|
||||
const relevantDocs = await retrieveRelevantDocuments(userMessage)
|
||||
@@ -74,7 +76,16 @@ export async function generateChatResponse(
|
||||
new HumanMessage(userMessage),
|
||||
])
|
||||
|
||||
// 6. Extract content and sources
|
||||
// 6. Track token usage
|
||||
if (tracking && response.usage_metadata) {
|
||||
trackTokenUsage(tracking.supabase, tracking.userId, 'ai-chat', {
|
||||
inputTokens: response.usage_metadata.input_tokens ?? 0,
|
||||
outputTokens: response.usage_metadata.output_tokens ?? 0,
|
||||
model: CHATBOT_CONFIG.model,
|
||||
})
|
||||
}
|
||||
|
||||
// 7. Extract content and sources
|
||||
const content =
|
||||
typeof response.content === 'string'
|
||||
? response.content
|
||||
|
||||
Reference in New Issue
Block a user