import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; import { DynamoDBDocumentClient, PutCommand, QueryCommand } from '@aws-sdk/lib-dynamodb'; import { ulid } from 'ulid'; type Role = 'user' | 'assistant' | 'system'; const region = process.env.AWS_REGION!; const tableName = process.env.CHAT_TABLE_NAME!; const ddb = new DynamoDBClient({ region }); const doc = DynamoDBDocumentClient.from(ddb); export type SessionItem = { pk: string; // SESSION#{sessionId} sk: 'SESSION'; userId: string; companyId?: string | null; model?: string; promptId?: string; promptVersion?: string; createdAt: string; updatedAt: string; }; export type MessageItem = { pk: string; // SESSION#{sessionId} sk: string; // MSG#{isoOrUlid} role: Role; content: string; createdAt: string; // eslint-disable-next-line @typescript-eslint/no-explicit-any redactions?: any; // eslint-disable-next-line @typescript-eslint/no-explicit-any toolCalls?: any; // eslint-disable-next-line @typescript-eslint/no-explicit-any evidenceJson?: any; }; export async function createSession(params: { userId: string; companyId?: string | null; model?: string; promptId?: string; promptVersion?: string; }): Promise { const sessionId = ulid(); const now = new Date().toISOString(); let item: SessionItem = { pk: `SESSION#${sessionId}`, sk: 'SESSION', userId: params.userId, createdAt: now, updatedAt: now, } if (params.companyId) { item.companyId = params.companyId; } if (params.model) { item.model = params.model; } if (params.promptId) { item.promptId = params.promptId; } if (params.promptVersion) { item.promptVersion = params.promptVersion; } await doc.send(new PutCommand({ TableName: tableName, Item: item })); return sessionId; } // eslint-disable-next-line @typescript-eslint/no-explicit-any export async function putMessage(params: { sessionId: string; role: Role; content: string; evidenceJson?: any }): Promise { const now = new Date().toISOString(); const item: MessageItem = { pk: `SESSION#${params.sessionId}`, sk: `MSG#${ulid()}`, role: params.role, content: sanitizeContent(params.content), createdAt: now, evidenceJson: params.evidenceJson, } as MessageItem; await doc.send(new PutCommand({ TableName: tableName, Item: item })); } export async function getHistory(sessionId: string, limit: number = 20): Promise> { const resp = await doc.send(new QueryCommand({ TableName: tableName, KeyConditionExpression: 'pk = :pk and begins_with(sk, :msg) ', ExpressionAttributeValues: { ':pk': `SESSION#${sessionId}`, ':msg': 'MSG#' }, ScanIndexForward: true, Limit: limit, })); const items = (resp.Items || []) as MessageItem[]; return items.map(it => ({ role: it.role, content: it.content })).slice(-limit); } function sanitizeContent(text: string): string { // Basic server-side redaction: do not persist raw secrets-looking strings try { return String(text).replace(/(api|secret|token|password)[=:]\s*[^\s"']+/gi, '$1=[REDACTED]'); } catch { return text; } }