/** * Tenant-Scoped Event Store Wrapper (Story 1.3) * * Wraps a SqliteEventStore and injects `tenantId` into every operation. * Created per-request in route handlers to enforce tenant isolation. * * All queries and writes go through this wrapper, ensuring that * a tenant can never access another tenant's data. */ import type { AgentLensEvent, EventQuery, EventQueryResult, Session, SessionQuery, Agent, AlertRule, AlertHistory, } from '@agentkitai/agentlens-core'; import type { IEventStore, AnalyticsResult, StorageStats } from '@agentkitai/agentlens-core'; import type { SqliteEventStore } from './sqlite-store.js'; import type { PostgresEventStore } from './postgres-store.js'; export class TenantScopedStore implements IEventStore { constructor( private readonly inner: SqliteEventStore | PostgresEventStore, public readonly tenantId: string, ) {} // ─── Events ────────────────────────────────────────────── async insertEvents(eventList: AgentLensEvent[]): Promise { // Stamp every event with this tenant's ID const stamped = eventList.map((e) => ({ ...e, tenantId: this.tenantId })); return this.inner.insertEvents(stamped); } async queryEvents(query: EventQuery): Promise { return this.inner.queryEvents({ ...query, tenantId: this.tenantId }); } async getEvent(id: string): Promise { return this.inner.getEvent(id, this.tenantId); } async getSessionTimeline(sessionId: string): Promise { return this.inner.getSessionTimeline(sessionId, this.tenantId); } async getLastEventHash(sessionId: string): Promise { return this.inner.getLastEventHash(sessionId, this.tenantId); } async countEvents(query: Omit): Promise { return this.inner.countEvents({ ...query, tenantId: this.tenantId }); } async countEventsBatch(query: { agentId: string; from: string; to: string; tenantId?: string }): Promise<{ total: number; error: number; critical: number; toolError: number }> { return this.inner.countEventsBatch({ ...query, tenantId: this.tenantId }); } async sumSessionCost(query: { agentId: string; from: string; tenantId?: string }): Promise { return this.inner.sumSessionCost({ ...query, tenantId: this.tenantId }); } // ─── Sessions ──────────────────────────────────────────── async upsertSession(session: Partial & { id: string }): Promise { return this.inner.upsertSession({ ...session, tenantId: this.tenantId }); } async querySessions(query: SessionQuery): Promise<{ sessions: Session[]; total: number }> { return this.inner.querySessions({ ...query, tenantId: this.tenantId }); } async getSession(id: string): Promise { return this.inner.getSession(id, this.tenantId); } // ─── Agents ────────────────────────────────────────────── async upsertAgent(agent: Partial & { id: string }): Promise { return this.inner.upsertAgent({ ...agent, tenantId: this.tenantId }); } async listAgents(): Promise { return this.inner.listAgents(this.tenantId); } async getAgent(id: string): Promise { return this.inner.getAgent(id, this.tenantId); } async unpauseAgent(agentId: string, clearModelOverride: boolean): Promise { return this.inner.unpauseAgent(this.tenantId, agentId, clearModelOverride); } // ─── Analytics ─────────────────────────────────────────── async getAnalytics(params: { from: string; to: string; agentId?: string; granularity: 'hour' | 'day' | 'week'; }): Promise { return this.inner.getAnalytics({ ...params, tenantId: this.tenantId }); } // ─── Alert Rules ───────────────────────────────────────── async createAlertRule(rule: AlertRule): Promise { return this.inner.createAlertRule({ ...rule, tenantId: this.tenantId }); } async updateAlertRule(id: string, updates: Partial): Promise { return this.inner.updateAlertRule(id, updates, this.tenantId); } async deleteAlertRule(id: string): Promise { return this.inner.deleteAlertRule(id, this.tenantId); } async listAlertRules(): Promise { return this.inner.listAlertRules(this.tenantId); } async getAlertRule(id: string): Promise { return this.inner.getAlertRule(id, this.tenantId); } // ─── Alert History ─────────────────────────────────────── async insertAlertHistory(entry: AlertHistory): Promise { return this.inner.insertAlertHistory({ ...entry, tenantId: this.tenantId }); } async listAlertHistory(opts?: { ruleId?: string; limit?: number; offset?: number; }): Promise<{ entries: AlertHistory[]; total: number }> { return this.inner.listAlertHistory({ ...opts, tenantId: this.tenantId }); } // ─── Maintenance ───────────────────────────────────────── async applyRetention(olderThan: string): Promise<{ deletedCount: number }> { // Retention is scoped to this tenant — only deletes this tenant's old data return this.inner.applyRetention(olderThan, this.tenantId); } async getStats(): Promise { return this.inner.getStats(this.tenantId); } }