@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.
Files changed (62) hide show
  1. package/README.md +130 -113
  2. package/dist/{ILLMProvider-DNhyOYoK.d.mts → ILLMProvider-B8ROITYK.d.mts} +59 -8
  3. package/dist/{ILLMProvider-DNhyOYoK.d.ts → ILLMProvider-B8ROITYK.d.ts} +59 -8
  4. package/dist/handlers/index.d.mts +2 -2
  5. package/dist/handlers/index.d.ts +2 -2
  6. package/dist/handlers/index.js +2376 -489
  7. package/dist/handlers/index.mjs +2372 -489
  8. package/dist/{index-CjQdL0cX.d.ts → index-B8PbEFSY.d.mts} +17 -3
  9. package/dist/{index-CHL1jdYm.d.mts → index-BCPKUAVL.d.ts} +33 -41
  10. package/dist/{index-Hgbwl9X4.d.ts → index-CrxCy36A.d.mts} +33 -41
  11. package/dist/{index-C9v7-tWd.d.mts → index-DNvoi-sV.d.ts} +17 -3
  12. package/dist/index.css +695 -203
  13. package/dist/index.d.mts +37 -7
  14. package/dist/index.d.ts +37 -7
  15. package/dist/index.js +1197 -286
  16. package/dist/index.mjs +1221 -297
  17. package/dist/server.d.mts +62 -6
  18. package/dist/server.d.ts +62 -6
  19. package/dist/server.js +2526 -574
  20. package/dist/server.mjs +2517 -573
  21. package/package.json +12 -10
  22. package/src/app/constants.tsx +207 -212
  23. package/src/app/layout.tsx +4 -28
  24. package/src/app/types.ts +17 -17
  25. package/src/components/AmbientBackground.tsx +10 -10
  26. package/src/components/ArchitectureCard.tsx +43 -7
  27. package/src/components/ArchitectureCardsSection.tsx +37 -4
  28. package/src/components/ChatWidget.tsx +4 -1
  29. package/src/components/ChatWindow.tsx +9 -2
  30. package/src/components/CodeViewer.tsx +19 -14
  31. package/src/components/DocViewer.tsx +75 -15
  32. package/src/components/Documentation.tsx +111 -28
  33. package/src/components/Hero.tsx +103 -20
  34. package/src/components/Lifecycle.tsx +65 -25
  35. package/src/components/MarkdownComponents.tsx +44 -1
  36. package/src/components/MessageBubble.tsx +162 -50
  37. package/src/components/constants.tsx +279 -0
  38. package/src/config/RagConfig.ts +56 -10
  39. package/src/config/constants.ts +5 -0
  40. package/src/config/serverConfig.ts +15 -0
  41. package/src/core/ConfigResolver.ts +30 -25
  42. package/src/core/DatabaseStorage.ts +469 -0
  43. package/src/core/LicenseVerifier.ts +154 -0
  44. package/src/core/MultiAgentCoordinator.ts +239 -0
  45. package/src/core/Pipeline.ts +148 -16
  46. package/src/core/ProviderRegistry.ts +5 -5
  47. package/src/core/Retrivora.ts +52 -6
  48. package/src/core/VectorPlugin.ts +74 -11
  49. package/src/core/mcp.ts +261 -0
  50. package/src/exceptions/index.ts +52 -0
  51. package/src/handlers/index.ts +504 -47
  52. package/src/hooks/useRagChat.ts +100 -43
  53. package/src/hooks/useStoredMessages.ts +15 -4
  54. package/src/index.ts +7 -0
  55. package/src/llm/LLMFactory.ts +15 -6
  56. package/src/llm/providers/GroqProvider.ts +176 -0
  57. package/src/llm/providers/QwenProvider.ts +191 -0
  58. package/src/server.ts +23 -13
  59. package/src/types/chat.ts +16 -0
  60. package/src/types/props.ts +50 -1
  61. package/.env.example +0 -80
  62. 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
+ }