@retrivora-ai/rag-engine 1.9.6 → 1.9.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +130 -113
- package/dist/{ILLMProvider-DNhyOYoK.d.mts → ILLMProvider-B8ROITYK.d.mts} +59 -8
- package/dist/{ILLMProvider-DNhyOYoK.d.ts → ILLMProvider-B8ROITYK.d.ts} +59 -8
- package/dist/handlers/index.d.mts +2 -2
- package/dist/handlers/index.d.ts +2 -2
- package/dist/handlers/index.js +2376 -489
- package/dist/handlers/index.mjs +2372 -489
- package/dist/{index-CjQdL0cX.d.ts → index-B8PbEFSY.d.mts} +17 -3
- package/dist/{index-CHL1jdYm.d.mts → index-BCPKUAVL.d.ts} +33 -41
- package/dist/{index-Hgbwl9X4.d.ts → index-CrxCy36A.d.mts} +33 -41
- package/dist/{index-C9v7-tWd.d.mts → index-DNvoi-sV.d.ts} +17 -3
- package/dist/index.css +695 -203
- package/dist/index.d.mts +37 -7
- package/dist/index.d.ts +37 -7
- package/dist/index.js +1197 -286
- package/dist/index.mjs +1221 -297
- package/dist/server.d.mts +62 -6
- package/dist/server.d.ts +62 -6
- package/dist/server.js +2526 -574
- package/dist/server.mjs +2517 -573
- package/package.json +12 -10
- package/src/app/constants.tsx +207 -212
- package/src/app/layout.tsx +4 -28
- package/src/app/types.ts +17 -17
- package/src/components/AmbientBackground.tsx +10 -10
- package/src/components/ArchitectureCard.tsx +43 -7
- package/src/components/ArchitectureCardsSection.tsx +37 -4
- package/src/components/ChatWidget.tsx +4 -1
- package/src/components/ChatWindow.tsx +9 -2
- package/src/components/CodeViewer.tsx +19 -14
- package/src/components/DocViewer.tsx +75 -15
- package/src/components/Documentation.tsx +111 -28
- package/src/components/Hero.tsx +103 -20
- package/src/components/Lifecycle.tsx +65 -25
- package/src/components/MarkdownComponents.tsx +44 -1
- package/src/components/MessageBubble.tsx +162 -50
- package/src/components/constants.tsx +279 -0
- package/src/config/RagConfig.ts +56 -10
- package/src/config/constants.ts +5 -0
- package/src/config/serverConfig.ts +15 -0
- package/src/core/ConfigResolver.ts +30 -25
- package/src/core/DatabaseStorage.ts +469 -0
- package/src/core/LicenseVerifier.ts +154 -0
- package/src/core/MultiAgentCoordinator.ts +239 -0
- package/src/core/Pipeline.ts +148 -16
- package/src/core/ProviderRegistry.ts +5 -5
- package/src/core/Retrivora.ts +52 -6
- package/src/core/VectorPlugin.ts +74 -11
- package/src/core/mcp.ts +261 -0
- package/src/exceptions/index.ts +52 -0
- package/src/handlers/index.ts +504 -47
- package/src/hooks/useRagChat.ts +100 -43
- package/src/hooks/useStoredMessages.ts +15 -4
- package/src/index.ts +7 -0
- package/src/llm/LLMFactory.ts +15 -6
- package/src/llm/providers/GroqProvider.ts +176 -0
- package/src/llm/providers/QwenProvider.ts +191 -0
- package/src/server.ts +23 -13
- package/src/types/chat.ts +16 -0
- package/src/types/props.ts +50 -1
- package/.env.example +0 -80
- package/LICENSE.txt +0 -21
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { RagConfig } from '../config/RagConfig';
|
|
4
|
+
import { LicenseVerifier } from './LicenseVerifier';
|
|
5
|
+
|
|
6
|
+
export interface HistoryMessage {
|
|
7
|
+
id: string;
|
|
8
|
+
role: string;
|
|
9
|
+
content: string;
|
|
10
|
+
sources?: any;
|
|
11
|
+
uiTransformation?: any;
|
|
12
|
+
trace?: any;
|
|
13
|
+
createdAt?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface FeedbackItem {
|
|
17
|
+
id: string;
|
|
18
|
+
messageId: string;
|
|
19
|
+
sessionId: string;
|
|
20
|
+
rating: string;
|
|
21
|
+
comment?: string;
|
|
22
|
+
createdAt?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class DatabaseStorage {
|
|
26
|
+
private config: RagConfig;
|
|
27
|
+
private provider: string;
|
|
28
|
+
// Dynamic PG Pool
|
|
29
|
+
private pgPool: any = null;
|
|
30
|
+
// Dynamic MongoDB Client
|
|
31
|
+
private mongoClient: any = null;
|
|
32
|
+
private mongoDbName = '';
|
|
33
|
+
|
|
34
|
+
private historyTableName: string;
|
|
35
|
+
private feedbackTableName: string;
|
|
36
|
+
|
|
37
|
+
// Filesystem fallback configuration
|
|
38
|
+
private fallbackDir = path.join(process.cwd(), '.retrivora');
|
|
39
|
+
private historyFile = path.join(this.fallbackDir, 'history.json');
|
|
40
|
+
private feedbackFile = path.join(this.fallbackDir, 'feedback.json');
|
|
41
|
+
|
|
42
|
+
constructor(config: RagConfig) {
|
|
43
|
+
this.config = config;
|
|
44
|
+
this.provider = config.vectorDb?.provider || 'fs';
|
|
45
|
+
|
|
46
|
+
// Retrieve and sanitize table/collection names (alphanumeric + underscores only)
|
|
47
|
+
const rawHistoryTable = config.rag?.history?.tableName || 'retrivora_history';
|
|
48
|
+
const rawFeedbackTable = config.rag?.feedback?.tableName || 'retrivora_feedback';
|
|
49
|
+
this.historyTableName = rawHistoryTable.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
50
|
+
this.feedbackTableName = rawFeedbackTable.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Asserts the user is running a valid Premium/Enterprise license in production.
|
|
55
|
+
* In non-production (development / test) environments this is a complete no-op —
|
|
56
|
+
* all History and Feedback features are freely accessible for local development.
|
|
57
|
+
*/
|
|
58
|
+
private checkPremiumSubscription() {
|
|
59
|
+
// ── Development / test: fully open, no checks ────────────────────────────
|
|
60
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── Production: license key required ────────────────────────────────────
|
|
65
|
+
if (!this.config.licenseKey) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
'[Retrivora SDK] Chat History and Feedback features require a Premium or Enterprise license key in production.'
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const payload = LicenseVerifier.verify(this.config.licenseKey, this.config.projectId);
|
|
73
|
+
const tier = (payload.tier || '').toLowerCase();
|
|
74
|
+
if (
|
|
75
|
+
tier !== 'premium' &&
|
|
76
|
+
tier !== 'enterprise' &&
|
|
77
|
+
tier !== 'growth' &&
|
|
78
|
+
tier !== 'pro' &&
|
|
79
|
+
tier !== 'developer_pro'
|
|
80
|
+
) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`[Retrivora SDK] Chat History and Feedback features are not available on the "${tier}" tier. ` +
|
|
83
|
+
`Please upgrade to a Developer Pro or Enterprise plan.`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
} catch (err) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`[Retrivora SDK] Subscription check failed: ${err instanceof Error ? err.message : String(err)}`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private checkHistoryEnabled() {
|
|
94
|
+
this.checkPremiumSubscription();
|
|
95
|
+
if (this.config.rag?.history?.enabled === false) {
|
|
96
|
+
throw new Error('[Retrivora SDK] Chat History is disabled in RagConfig.');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private checkFeedbackEnabled() {
|
|
101
|
+
this.checkPremiumSubscription();
|
|
102
|
+
if (this.config.rag?.feedback?.enabled === false) {
|
|
103
|
+
throw new Error('[Retrivora SDK] User Feedback is disabled in RagConfig.');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private async getPGPool() {
|
|
108
|
+
const globalKey = `__retrivora_pg_pool_${this.config.projectId}`;
|
|
109
|
+
const _g = global as any;
|
|
110
|
+
if (_g[globalKey]) {
|
|
111
|
+
this.pgPool = _g[globalKey];
|
|
112
|
+
return this.pgPool;
|
|
113
|
+
}
|
|
114
|
+
const opts = this.config.vectorDb.options as Record<string, any>;
|
|
115
|
+
if (!opts.connectionString) {
|
|
116
|
+
throw new Error('[DatabaseStorage] pg connectionString option is missing.');
|
|
117
|
+
}
|
|
118
|
+
const { Pool } = await import('pg');
|
|
119
|
+
this.pgPool = new Pool({ connectionString: opts.connectionString });
|
|
120
|
+
_g[globalKey] = this.pgPool;
|
|
121
|
+
|
|
122
|
+
// Initialize tables using dynamic sanitized table names
|
|
123
|
+
const client = await this.pgPool.connect();
|
|
124
|
+
try {
|
|
125
|
+
await client.query(`
|
|
126
|
+
CREATE TABLE IF NOT EXISTS ${this.historyTableName} (
|
|
127
|
+
id TEXT PRIMARY KEY,
|
|
128
|
+
session_id TEXT NOT NULL,
|
|
129
|
+
role TEXT NOT NULL,
|
|
130
|
+
content TEXT NOT NULL,
|
|
131
|
+
sources JSONB,
|
|
132
|
+
ui_transformation JSONB,
|
|
133
|
+
trace JSONB,
|
|
134
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
135
|
+
)
|
|
136
|
+
`);
|
|
137
|
+
await client.query(`
|
|
138
|
+
CREATE TABLE IF NOT EXISTS ${this.feedbackTableName} (
|
|
139
|
+
id TEXT PRIMARY KEY,
|
|
140
|
+
message_id TEXT NOT NULL UNIQUE,
|
|
141
|
+
session_id TEXT NOT NULL,
|
|
142
|
+
rating TEXT NOT NULL,
|
|
143
|
+
comment TEXT,
|
|
144
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
145
|
+
)
|
|
146
|
+
`);
|
|
147
|
+
} finally {
|
|
148
|
+
client.release();
|
|
149
|
+
}
|
|
150
|
+
return this.pgPool;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private async getMongoClient() {
|
|
154
|
+
const globalKey = `__retrivora_mongo_client_${this.config.projectId}`;
|
|
155
|
+
const _g = global as any;
|
|
156
|
+
if (_g[globalKey]) {
|
|
157
|
+
this.mongoClient = _g[globalKey];
|
|
158
|
+
this.mongoDbName = (this.config.vectorDb.options as any).database;
|
|
159
|
+
return this.mongoClient;
|
|
160
|
+
}
|
|
161
|
+
const opts = this.config.vectorDb.options as Record<string, any>;
|
|
162
|
+
if (!opts.uri || !opts.database) {
|
|
163
|
+
throw new Error('[DatabaseStorage] mongo uri and database options are missing.');
|
|
164
|
+
}
|
|
165
|
+
const { MongoClient } = await import('mongodb');
|
|
166
|
+
this.mongoClient = new MongoClient(opts.uri);
|
|
167
|
+
await this.mongoClient.connect();
|
|
168
|
+
_g[globalKey] = this.mongoClient;
|
|
169
|
+
this.mongoDbName = opts.database;
|
|
170
|
+
return this.mongoClient;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private getMongoDb() {
|
|
174
|
+
return this.mongoClient.db(this.mongoDbName);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Helper for FS fallback reads
|
|
178
|
+
private readLocalFile(filePath: string): any[] {
|
|
179
|
+
if (!fs.existsSync(filePath)) return [];
|
|
180
|
+
try {
|
|
181
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
182
|
+
return JSON.parse(raw) || [];
|
|
183
|
+
} catch {
|
|
184
|
+
return [];
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Helper for FS fallback writes
|
|
189
|
+
private writeLocalFile(filePath: string, data: any[]): void {
|
|
190
|
+
if (!fs.existsSync(this.fallbackDir)) {
|
|
191
|
+
fs.mkdirSync(this.fallbackDir, { recursive: true });
|
|
192
|
+
}
|
|
193
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ─── Message History Operations ──────────────────────────────────────────
|
|
197
|
+
|
|
198
|
+
async saveMessage(
|
|
199
|
+
sessionId: string,
|
|
200
|
+
message: Omit<HistoryMessage, 'createdAt'>
|
|
201
|
+
): Promise<void> {
|
|
202
|
+
this.checkHistoryEnabled();
|
|
203
|
+
const createdAt = new Date().toISOString();
|
|
204
|
+
const msgId = message.id || `msg_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
|
205
|
+
|
|
206
|
+
if (this.provider === 'postgresql' || this.provider === 'pgvector') {
|
|
207
|
+
const pool = await this.getPGPool();
|
|
208
|
+
await pool.query(
|
|
209
|
+
`INSERT INTO ${this.historyTableName} (id, session_id, role, content, sources, ui_transformation, trace, created_at)
|
|
210
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
211
|
+
ON CONFLICT (id) DO UPDATE
|
|
212
|
+
SET content = EXCLUDED.content, sources = EXCLUDED.sources, ui_transformation = EXCLUDED.ui_transformation, trace = EXCLUDED.trace`,
|
|
213
|
+
[
|
|
214
|
+
msgId,
|
|
215
|
+
sessionId,
|
|
216
|
+
message.role,
|
|
217
|
+
message.content,
|
|
218
|
+
message.sources ? JSON.stringify(message.sources) : null,
|
|
219
|
+
message.uiTransformation ? JSON.stringify(message.uiTransformation) : null,
|
|
220
|
+
message.trace ? JSON.stringify(message.trace) : null,
|
|
221
|
+
createdAt,
|
|
222
|
+
]
|
|
223
|
+
);
|
|
224
|
+
} else if (this.provider === 'mongodb') {
|
|
225
|
+
await this.getMongoClient();
|
|
226
|
+
const db = this.getMongoDb();
|
|
227
|
+
const col = db.collection(this.historyTableName);
|
|
228
|
+
await col.updateOne(
|
|
229
|
+
{ id: msgId },
|
|
230
|
+
{
|
|
231
|
+
$set: {
|
|
232
|
+
id: msgId,
|
|
233
|
+
session_id: sessionId,
|
|
234
|
+
role: message.role,
|
|
235
|
+
content: message.content,
|
|
236
|
+
sources: message.sources || null,
|
|
237
|
+
ui_transformation: message.uiTransformation || null,
|
|
238
|
+
trace: message.trace || null,
|
|
239
|
+
created_at: createdAt,
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
{ upsert: true }
|
|
243
|
+
);
|
|
244
|
+
} else {
|
|
245
|
+
// Local FS fallback
|
|
246
|
+
const history = this.readLocalFile(this.historyFile);
|
|
247
|
+
const index = history.findIndex((h) => h.id === msgId);
|
|
248
|
+
const item = {
|
|
249
|
+
id: msgId,
|
|
250
|
+
session_id: sessionId,
|
|
251
|
+
role: message.role,
|
|
252
|
+
content: message.content,
|
|
253
|
+
sources: message.sources || null,
|
|
254
|
+
ui_transformation: message.uiTransformation || null,
|
|
255
|
+
trace: message.trace || null,
|
|
256
|
+
created_at: createdAt,
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
if (index !== -1) {
|
|
260
|
+
history[index] = item;
|
|
261
|
+
} else {
|
|
262
|
+
history.push(item);
|
|
263
|
+
}
|
|
264
|
+
this.writeLocalFile(this.historyFile, history);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async getHistory(sessionId: string): Promise<HistoryMessage[]> {
|
|
269
|
+
this.checkHistoryEnabled();
|
|
270
|
+
if (this.provider === 'postgresql' || this.provider === 'pgvector') {
|
|
271
|
+
const pool = await this.getPGPool();
|
|
272
|
+
const res = await pool.query(
|
|
273
|
+
`SELECT id, role, content, sources, ui_transformation as "uiTransformation", trace, created_at as "createdAt"
|
|
274
|
+
FROM ${this.historyTableName}
|
|
275
|
+
WHERE session_id = $1
|
|
276
|
+
ORDER BY created_at ASC`,
|
|
277
|
+
[sessionId]
|
|
278
|
+
);
|
|
279
|
+
return res.rows;
|
|
280
|
+
} else if (this.provider === 'mongodb') {
|
|
281
|
+
await this.getMongoClient();
|
|
282
|
+
const db = this.getMongoDb();
|
|
283
|
+
const col = db.collection(this.historyTableName);
|
|
284
|
+
const items = await col
|
|
285
|
+
.find({ session_id: sessionId })
|
|
286
|
+
.sort({ created_at: 1 })
|
|
287
|
+
.toArray();
|
|
288
|
+
return items.map((item: any) => ({
|
|
289
|
+
id: item.id,
|
|
290
|
+
role: item.role,
|
|
291
|
+
content: item.content,
|
|
292
|
+
sources: item.sources,
|
|
293
|
+
uiTransformation: item.ui_transformation,
|
|
294
|
+
trace: item.trace,
|
|
295
|
+
createdAt: item.created_at,
|
|
296
|
+
}));
|
|
297
|
+
} else {
|
|
298
|
+
const history = this.readLocalFile(this.historyFile);
|
|
299
|
+
return history
|
|
300
|
+
.filter((h) => h.session_id === sessionId)
|
|
301
|
+
.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async clearHistory(sessionId: string): Promise<void> {
|
|
306
|
+
this.checkHistoryEnabled();
|
|
307
|
+
if (this.provider === 'postgresql' || this.provider === 'pgvector') {
|
|
308
|
+
const pool = await this.getPGPool();
|
|
309
|
+
await pool.query(`DELETE FROM ${this.historyTableName} WHERE session_id = $1`, [sessionId]);
|
|
310
|
+
await pool.query(`DELETE FROM ${this.feedbackTableName} WHERE session_id = $1`, [sessionId]);
|
|
311
|
+
} else if (this.provider === 'mongodb') {
|
|
312
|
+
await this.getMongoClient();
|
|
313
|
+
const db = this.getMongoDb();
|
|
314
|
+
await db.collection(this.historyTableName).deleteMany({ session_id: sessionId });
|
|
315
|
+
await db.collection(this.feedbackTableName).deleteMany({ session_id: sessionId });
|
|
316
|
+
} else {
|
|
317
|
+
const history = this.readLocalFile(this.historyFile);
|
|
318
|
+
const filteredHistory = history.filter((h) => h.session_id !== sessionId);
|
|
319
|
+
this.writeLocalFile(this.historyFile, filteredHistory);
|
|
320
|
+
|
|
321
|
+
const feedback = this.readLocalFile(this.feedbackFile);
|
|
322
|
+
const filteredFeedback = feedback.filter((f) => f.session_id !== sessionId);
|
|
323
|
+
this.writeLocalFile(this.feedbackFile, filteredFeedback);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async listSessions(): Promise<string[]> {
|
|
328
|
+
this.checkHistoryEnabled();
|
|
329
|
+
if (this.provider === 'postgresql' || this.provider === 'pgvector') {
|
|
330
|
+
const pool = await this.getPGPool();
|
|
331
|
+
const res = await pool.query(`SELECT DISTINCT session_id FROM ${this.historyTableName}`);
|
|
332
|
+
return res.rows.map((r: any) => r.session_id);
|
|
333
|
+
} else if (this.provider === 'mongodb') {
|
|
334
|
+
await this.getMongoClient();
|
|
335
|
+
const db = this.getMongoDb();
|
|
336
|
+
return db.collection(this.historyTableName).distinct('session_id');
|
|
337
|
+
} else {
|
|
338
|
+
const history = this.readLocalFile(this.historyFile);
|
|
339
|
+
const sessions = new Set<string>(history.map((h) => h.session_id));
|
|
340
|
+
return Array.from(sessions);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ─── Feedback Operations ──────────────────────────────────────────────────
|
|
345
|
+
|
|
346
|
+
async saveFeedback(feedback: Omit<FeedbackItem, 'id' | 'createdAt'>): Promise<void> {
|
|
347
|
+
this.checkFeedbackEnabled();
|
|
348
|
+
const id = `fb_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
|
349
|
+
const createdAt = new Date().toISOString();
|
|
350
|
+
|
|
351
|
+
if (this.provider === 'postgresql' || this.provider === 'pgvector') {
|
|
352
|
+
const pool = await this.getPGPool();
|
|
353
|
+
await pool.query(
|
|
354
|
+
`INSERT INTO ${this.feedbackTableName} (id, message_id, session_id, rating, comment, created_at)
|
|
355
|
+
VALUES ($1, $2, $3, $4, $5, $6)
|
|
356
|
+
ON CONFLICT (message_id) DO UPDATE
|
|
357
|
+
SET rating = EXCLUDED.rating, comment = EXCLUDED.comment`,
|
|
358
|
+
[id, feedback.messageId, feedback.sessionId, feedback.rating, feedback.comment || null, createdAt]
|
|
359
|
+
);
|
|
360
|
+
} else if (this.provider === 'mongodb') {
|
|
361
|
+
await this.getMongoClient();
|
|
362
|
+
const db = this.getMongoDb();
|
|
363
|
+
const col = db.collection(this.feedbackTableName);
|
|
364
|
+
await col.updateOne(
|
|
365
|
+
{ message_id: feedback.messageId },
|
|
366
|
+
{
|
|
367
|
+
$set: {
|
|
368
|
+
id,
|
|
369
|
+
message_id: feedback.messageId,
|
|
370
|
+
session_id: feedback.sessionId,
|
|
371
|
+
rating: feedback.rating,
|
|
372
|
+
comment: feedback.comment || null,
|
|
373
|
+
created_at: createdAt,
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
{ upsert: true }
|
|
377
|
+
);
|
|
378
|
+
} else {
|
|
379
|
+
const list = this.readLocalFile(this.feedbackFile);
|
|
380
|
+
const index = list.findIndex((f) => f.message_id === feedback.messageId);
|
|
381
|
+
const item = {
|
|
382
|
+
id,
|
|
383
|
+
message_id: feedback.messageId,
|
|
384
|
+
session_id: feedback.sessionId,
|
|
385
|
+
rating: feedback.rating,
|
|
386
|
+
comment: feedback.comment || null,
|
|
387
|
+
created_at: createdAt,
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
if (index !== -1) {
|
|
391
|
+
list[index] = item;
|
|
392
|
+
} else {
|
|
393
|
+
list.push(item);
|
|
394
|
+
}
|
|
395
|
+
this.writeLocalFile(this.feedbackFile, list);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async getFeedback(messageId: string): Promise<FeedbackItem | null> {
|
|
400
|
+
this.checkFeedbackEnabled();
|
|
401
|
+
if (this.provider === 'postgresql' || this.provider === 'pgvector') {
|
|
402
|
+
const pool = await this.getPGPool();
|
|
403
|
+
const res = await pool.query(
|
|
404
|
+
`SELECT id, message_id as "messageId", session_id as "sessionId", rating, comment, created_at as "createdAt"
|
|
405
|
+
FROM ${this.feedbackTableName}
|
|
406
|
+
WHERE message_id = $1`,
|
|
407
|
+
[messageId]
|
|
408
|
+
);
|
|
409
|
+
return res.rows[0] || null;
|
|
410
|
+
} else if (this.provider === 'mongodb') {
|
|
411
|
+
await this.getMongoClient();
|
|
412
|
+
const db = this.getMongoDb();
|
|
413
|
+
const col = db.collection(this.feedbackTableName);
|
|
414
|
+
const item = await col.findOne({ message_id: messageId });
|
|
415
|
+
if (!item) return null;
|
|
416
|
+
return {
|
|
417
|
+
id: item.id,
|
|
418
|
+
messageId: item.message_id,
|
|
419
|
+
sessionId: item.session_id,
|
|
420
|
+
rating: item.rating,
|
|
421
|
+
comment: item.comment,
|
|
422
|
+
createdAt: item.created_at,
|
|
423
|
+
};
|
|
424
|
+
} else {
|
|
425
|
+
const list = this.readLocalFile(this.feedbackFile);
|
|
426
|
+
return list.find((f) => f.message_id === messageId) || null;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async listFeedback(): Promise<FeedbackItem[]> {
|
|
431
|
+
this.checkFeedbackEnabled();
|
|
432
|
+
if (this.provider === 'postgresql' || this.provider === 'pgvector') {
|
|
433
|
+
const pool = await this.getPGPool();
|
|
434
|
+
const res = await pool.query(
|
|
435
|
+
`SELECT id, message_id as "messageId", session_id as "sessionId", rating, comment, created_at as "createdAt"
|
|
436
|
+
FROM ${this.feedbackTableName}
|
|
437
|
+
ORDER BY created_at DESC`
|
|
438
|
+
);
|
|
439
|
+
return res.rows;
|
|
440
|
+
} else if (this.provider === 'mongodb') {
|
|
441
|
+
await this.getMongoClient();
|
|
442
|
+
const db = this.getMongoDb();
|
|
443
|
+
const col = db.collection(this.feedbackTableName);
|
|
444
|
+
const items = await col.find({}).sort({ created_at: -1 }).toArray();
|
|
445
|
+
return items.map((item: any) => ({
|
|
446
|
+
id: item.id,
|
|
447
|
+
messageId: item.message_id,
|
|
448
|
+
sessionId: item.session_id,
|
|
449
|
+
rating: item.rating,
|
|
450
|
+
comment: item.comment,
|
|
451
|
+
createdAt: item.created_at,
|
|
452
|
+
}));
|
|
453
|
+
} else {
|
|
454
|
+
const list = this.readLocalFile(this.feedbackFile);
|
|
455
|
+
return [...list].sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
async disconnect() {
|
|
460
|
+
if (this.pgPool) {
|
|
461
|
+
await this.pgPool.end();
|
|
462
|
+
this.pgPool = null;
|
|
463
|
+
}
|
|
464
|
+
if (this.mongoClient) {
|
|
465
|
+
await this.mongoClient.close();
|
|
466
|
+
this.mongoClient = null;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import * as crypto from 'crypto';
|
|
2
|
+
import { ConfigurationException } from '../exceptions';
|
|
3
|
+
|
|
4
|
+
export interface LicensePayload {
|
|
5
|
+
projectId: string;
|
|
6
|
+
expiresAt: number; // UNIX timestamp in seconds
|
|
7
|
+
tier: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* LicenseVerifier — handles cryptographic validation of signed JWT license keys.
|
|
12
|
+
* Enables zero-latency local license validation without external network calls.
|
|
13
|
+
*/
|
|
14
|
+
export class LicenseVerifier {
|
|
15
|
+
// Retrivora's Public Key used to verify the license key signature.
|
|
16
|
+
// In production, this matches the private key held by Retrivora SaaS.
|
|
17
|
+
private static readonly PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
|
|
18
|
+
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArMieCkCBtTfByRNOserm
|
|
19
|
+
XM4T0Am29JyNzB4XQPe+WJJ56CN73H1HLXx9n1ByFcxkEJ9po+SURj5DnvUXwEy+
|
|
20
|
+
xstx33wDPpVmcMQe33j5CRYS0S4gICiNqIRN7XkTlJtrcXqrmh1/hdOAwfRbjO1T
|
|
21
|
+
NVtORHwUwWfI/lBLyxUPGtKPed+0fQ7NtcW4o00JXxs3EjCB5BV9CbnXoTjQb/4h
|
|
22
|
+
iwZof4l8rb7oaNzOsVg0RSyxsETX7Ex5sI0CjBz1p2mYp4oVhw/A/h8Ls7H0XzjC
|
|
23
|
+
+Eb6BLtsytt5JHoSp0jExLlCpDmpys2L+kETcBVx+IqiXtdN1n6KbkksvEDUVzLI
|
|
24
|
+
MwIDAQAB
|
|
25
|
+
-----END PUBLIC KEY-----`;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Decodes, verifies signature, and checks metadata of a license key.
|
|
29
|
+
*
|
|
30
|
+
* @param licenseKey - Base64url signed JWT license key.
|
|
31
|
+
* @param currentProjectId - Project namespace ID.
|
|
32
|
+
* @param publicKeyOverride - Optional override for unit/integration tests.
|
|
33
|
+
*/
|
|
34
|
+
static verify(
|
|
35
|
+
licenseKey: string | undefined,
|
|
36
|
+
currentProjectId: string,
|
|
37
|
+
provider?: string,
|
|
38
|
+
publicKeyOverride?: string
|
|
39
|
+
): LicensePayload {
|
|
40
|
+
const isProduction = process.env.NODE_ENV === 'production';
|
|
41
|
+
|
|
42
|
+
// 1. Development Fallback (Fail-open locally but log warning)
|
|
43
|
+
if (!licenseKey) {
|
|
44
|
+
if (isProduction) {
|
|
45
|
+
throw new ConfigurationException(
|
|
46
|
+
'[Retrivora SDK] License key (licenseKey) is required in production environments.'
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
console.warn(
|
|
50
|
+
`\x1b[33m[Retrivora SDK] WARNING: Running without a license key. This namespace (${currentProjectId}) is permitted for local development only.\x1b[0m`
|
|
51
|
+
);
|
|
52
|
+
return {
|
|
53
|
+
projectId: currentProjectId,
|
|
54
|
+
expiresAt: Math.floor(Date.now() / 1000) + 86400, // Valid for 24h
|
|
55
|
+
tier: 'hobby',
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const parts = licenseKey.split('.');
|
|
61
|
+
if (parts.length !== 3) {
|
|
62
|
+
throw new Error('Malformed token structure (expected 3 parts).');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const [headerB64, payloadB64, signatureB64] = parts;
|
|
66
|
+
const dataToVerify = `${headerB64}.${payloadB64}`;
|
|
67
|
+
|
|
68
|
+
const publicKey = publicKeyOverride ?? this.PUBLIC_KEY;
|
|
69
|
+
|
|
70
|
+
// 2. Cryptographic signature check
|
|
71
|
+
const signature = Buffer.from(signatureB64, 'base64url');
|
|
72
|
+
const data = Buffer.from(dataToVerify);
|
|
73
|
+
|
|
74
|
+
const isValid = crypto.verify(
|
|
75
|
+
'sha256',
|
|
76
|
+
data,
|
|
77
|
+
publicKey,
|
|
78
|
+
signature
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
if (!isValid) {
|
|
82
|
+
throw new Error('Signature verification failed.');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 3. Decode & validate payload checks
|
|
86
|
+
const payload: LicensePayload = JSON.parse(
|
|
87
|
+
Buffer.from(payloadB64, 'base64url').toString('utf8')
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
if (!payload.projectId) {
|
|
91
|
+
throw new Error('License payload is missing "projectId".');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (payload.projectId !== currentProjectId) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`Project ID mismatch. License is bound to project namespace "${payload.projectId}" ` +
|
|
97
|
+
`but configuration has "${currentProjectId}".`
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!payload.expiresAt) {
|
|
102
|
+
throw new Error('License payload is missing expiration ("expiresAt").');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const currentTimestampSec = Math.floor(Date.now() / 1000);
|
|
106
|
+
if (currentTimestampSec > payload.expiresAt) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`License key has expired. Expiration: ${new Date(
|
|
109
|
+
payload.expiresAt * 1000
|
|
110
|
+
).toDateString()}`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 4. Validate Vector DB Provider against Tier (Production only)
|
|
115
|
+
if (isProduction && provider) {
|
|
116
|
+
const tier = (payload.tier || '').toLowerCase();
|
|
117
|
+
const normalizedProvider = provider.toLowerCase();
|
|
118
|
+
|
|
119
|
+
if (tier === 'hobby') {
|
|
120
|
+
// Hobby tier only supports PostgreSQL/pgvector/Supabase
|
|
121
|
+
const allowedHobby = ['postgresql', 'pgvector', 'supabase'];
|
|
122
|
+
if (!allowedHobby.includes(normalizedProvider)) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`The database provider "${provider}" is not allowed on the Hobby tier. ` +
|
|
125
|
+
`Hobby tier is restricted to PostgreSQL and Supabase. ` +
|
|
126
|
+
`Please upgrade to a Developer Pro or Enterprise plan.`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
} else if (tier === 'pro' || tier === 'developer_pro' || tier === 'premium' || tier === 'growth') {
|
|
130
|
+
// Developer Pro supports: PostgreSQL, Pinecone, Qdrant, MongoDB, Milvus, ChromaDB
|
|
131
|
+
const allowedPro = [
|
|
132
|
+
'postgresql', 'pgvector', 'supabase',
|
|
133
|
+
'pinecone', 'qdrant', 'mongodb', 'milvus',
|
|
134
|
+
'chroma', 'chromadb'
|
|
135
|
+
];
|
|
136
|
+
if (!allowedPro.includes(normalizedProvider)) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
`The database provider "${provider}" is not allowed on the Developer Pro tier. ` +
|
|
139
|
+
`Please upgrade to an Enterprise plan.`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// Enterprise has access to all databases (including redis, weaviate, custom ones)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return payload;
|
|
147
|
+
} catch (err) {
|
|
148
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
149
|
+
throw new ConfigurationException(
|
|
150
|
+
`[Retrivora SDK] License key validation failed: ${message}`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|