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:
Jakob Wennberg
2026-03-02 11:32:43 +01:00
co-authored by Claude Opus 4.6
parent 1a1c6ba40f
commit 13725ffc16
33 changed files with 2852 additions and 271 deletions
+44 -14
View File
@@ -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' } }
+13 -2
View File
@@ -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