@retrivora-ai/rag-engine 2.3.0 → 2.3.2
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/dist/{LicenseValidator-CENvo9o2.d.mts → BatchProcessor-7yV-UCHW.d.mts} +142 -3
- package/dist/{LicenseValidator-CsjJp2PP.d.ts → BatchProcessor-BfzuU4cK.d.ts} +142 -3
- package/dist/handlers/index.d.mts +1 -1
- package/dist/handlers/index.d.ts +1 -1
- package/dist/handlers/index.js +881 -331
- package/dist/handlers/index.mjs +881 -331
- package/dist/index-DR_O_B-W.d.ts +394 -0
- package/dist/index-fnpaCuma.d.mts +394 -0
- package/dist/index.css +58 -0
- package/dist/index.d.mts +63 -3
- package/dist/index.d.ts +63 -3
- package/dist/index.js +454 -36
- package/dist/index.mjs +453 -38
- package/dist/server.d.mts +35 -73
- package/dist/server.d.ts +35 -73
- package/dist/server.js +911 -345
- package/dist/server.mjs +911 -345
- package/package.json +3 -2
- package/src/components/ChatWidget.tsx +149 -48
- package/src/components/ChatWindow.tsx +52 -9
- package/src/components/CodeViewer.tsx +1 -1
- package/src/core/BatchProcessor.ts +42 -4
- package/src/core/CircuitBreaker.ts +118 -0
- package/src/core/DatabaseStorage.ts +55 -24
- package/src/core/FreeTierLimitsGuard.ts +281 -0
- package/src/core/LangChainAgent.ts +0 -2
- package/src/core/LicenseValidator.ts +66 -2
- package/src/core/LicenseVerifier.ts +31 -3
- package/src/core/VectorPlugin.ts +30 -0
- package/src/handlers/index.ts +491 -36
- package/src/index.css +58 -0
- package/src/index.ts +4 -0
- package/src/providers/vectordb/MilvusProvider.ts +0 -1
- package/src/providers/vectordb/MongoDBProvider.ts +0 -1
- package/src/providers/vectordb/MultiTablePostgresProvider.ts +2 -2
- package/src/providers/vectordb/RedisProvider.ts +0 -1
- package/src/providers/vectordb/WeaviateProvider.ts +0 -1
- package/src/rag/LlamaIndexIngestor.ts +0 -1
- package/src/server.ts +1 -0
- package/dist/index-BPJ3KDYI.d.ts +0 -195
- package/dist/index-Dmq5lH0j.d.mts +0 -195
|
@@ -291,8 +291,11 @@ export class DatabaseStorage {
|
|
|
291
291
|
}
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
-
async getHistory(sessionId: string): Promise<HistoryMessage[]> {
|
|
294
|
+
async getHistory(sessionId: string, projectIds?: string[]): Promise<HistoryMessage[]> {
|
|
295
295
|
this.checkHistoryEnabled();
|
|
296
|
+
if (projectIds && projectIds.length > 0 && !this.sessionInScope(sessionId, projectIds)) {
|
|
297
|
+
return [];
|
|
298
|
+
}
|
|
296
299
|
if (this.provider === 'postgresql' || this.provider === 'pgvector') {
|
|
297
300
|
const pool = await this.getPGPool();
|
|
298
301
|
const res = await pool.query(
|
|
@@ -328,8 +331,11 @@ export class DatabaseStorage {
|
|
|
328
331
|
}
|
|
329
332
|
}
|
|
330
333
|
|
|
331
|
-
async clearHistory(sessionId: string): Promise<void> {
|
|
334
|
+
async clearHistory(sessionId: string, projectIds?: string[]): Promise<void> {
|
|
332
335
|
this.checkHistoryEnabled();
|
|
336
|
+
if (projectIds && projectIds.length > 0 && !this.sessionInScope(sessionId, projectIds)) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
333
339
|
if (this.provider === 'postgresql' || this.provider === 'pgvector') {
|
|
334
340
|
const pool = await this.getPGPool();
|
|
335
341
|
await pool.query(`DELETE FROM ${this.historyTableName} WHERE session_id = $1`, [sessionId]);
|
|
@@ -350,21 +356,37 @@ export class DatabaseStorage {
|
|
|
350
356
|
}
|
|
351
357
|
}
|
|
352
358
|
|
|
353
|
-
|
|
359
|
+
/**
|
|
360
|
+
* Return true when `sessionId` falls into the allowed `projectIds` scope.
|
|
361
|
+
* When `projectIds` is empty / undefined we allow the call (legacy behavior
|
|
362
|
+
* for non-multi-tenant consumers). Session ids typically begin with the
|
|
363
|
+
* project id they were created for, so we prefix-match as well as contains-match
|
|
364
|
+
* to catch sharded / suffix-style ids used by some integrations.
|
|
365
|
+
*/
|
|
366
|
+
private sessionInScope(sessionId: string, projectIds?: string[]): boolean {
|
|
367
|
+
if (!projectIds || projectIds.length === 0) return true;
|
|
368
|
+
return projectIds.some(
|
|
369
|
+
(pid) => pid && (sessionId === pid || sessionId.startsWith(pid) || sessionId.includes(pid))
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async listSessions(projectIds?: string[]): Promise<string[]> {
|
|
354
374
|
this.checkHistoryEnabled();
|
|
375
|
+
let raw: string[] = [];
|
|
355
376
|
if (this.provider === 'postgresql' || this.provider === 'pgvector') {
|
|
356
377
|
const pool = await this.getPGPool();
|
|
357
378
|
const res = await pool.query(`SELECT DISTINCT session_id FROM ${this.historyTableName}`);
|
|
358
|
-
|
|
379
|
+
raw = res.rows.map((r: any) => r.session_id);
|
|
359
380
|
} else if (this.provider === 'mongodb') {
|
|
360
381
|
await this.getMongoClient();
|
|
361
382
|
const db = this.getMongoDb();
|
|
362
|
-
|
|
383
|
+
raw = await db.collection(this.historyTableName).distinct('session_id');
|
|
363
384
|
} else {
|
|
364
385
|
const history = this.readLocalFile(this.historyFile);
|
|
365
|
-
|
|
366
|
-
return Array.from(sessions);
|
|
386
|
+
raw = Array.from(new Set<string>(history.map((h) => h.session_id)));
|
|
367
387
|
}
|
|
388
|
+
if (!projectIds || projectIds.length === 0) return raw;
|
|
389
|
+
return raw.filter((sid) => this.sessionInScope(sid, projectIds));
|
|
368
390
|
}
|
|
369
391
|
|
|
370
392
|
// ─── Feedback Operations ──────────────────────────────────────────────────
|
|
@@ -422,8 +444,9 @@ export class DatabaseStorage {
|
|
|
422
444
|
}
|
|
423
445
|
}
|
|
424
446
|
|
|
425
|
-
async getFeedback(messageId: string): Promise<FeedbackItem | null> {
|
|
447
|
+
async getFeedback(messageId: string, projectIds?: string[]): Promise<FeedbackItem | null> {
|
|
426
448
|
this.checkFeedbackEnabled();
|
|
449
|
+
let item: FeedbackItem | null = null;
|
|
427
450
|
if (this.provider === 'postgresql' || this.provider === 'pgvector') {
|
|
428
451
|
const pool = await this.getPGPool();
|
|
429
452
|
const res = await pool.query(
|
|
@@ -432,29 +455,35 @@ export class DatabaseStorage {
|
|
|
432
455
|
WHERE message_id = $1`,
|
|
433
456
|
[messageId]
|
|
434
457
|
);
|
|
435
|
-
|
|
458
|
+
item = res.rows[0] || null;
|
|
436
459
|
} else if (this.provider === 'mongodb') {
|
|
437
460
|
await this.getMongoClient();
|
|
438
461
|
const db = this.getMongoDb();
|
|
439
462
|
const col = db.collection(this.feedbackTableName);
|
|
440
|
-
const
|
|
441
|
-
if (
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
463
|
+
const raw = await col.findOne({ message_id: messageId });
|
|
464
|
+
if (raw) {
|
|
465
|
+
item = {
|
|
466
|
+
id: raw.id,
|
|
467
|
+
messageId: raw.message_id,
|
|
468
|
+
sessionId: raw.session_id,
|
|
469
|
+
rating: raw.rating,
|
|
470
|
+
comment: raw.comment,
|
|
471
|
+
createdAt: raw.created_at,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
450
474
|
} else {
|
|
451
475
|
const list = this.readLocalFile(this.feedbackFile);
|
|
452
|
-
|
|
476
|
+
item = list.find((f) => f.message_id === messageId) || null;
|
|
477
|
+
}
|
|
478
|
+
if (item && projectIds && projectIds.length > 0) {
|
|
479
|
+
return this.sessionInScope(item.sessionId, projectIds) ? item : null;
|
|
453
480
|
}
|
|
481
|
+
return item;
|
|
454
482
|
}
|
|
455
483
|
|
|
456
|
-
async listFeedback(): Promise<FeedbackItem[]> {
|
|
484
|
+
async listFeedback(projectIds?: string[]): Promise<FeedbackItem[]> {
|
|
457
485
|
this.checkFeedbackEnabled();
|
|
486
|
+
let raw: FeedbackItem[] = [];
|
|
458
487
|
if (this.provider === 'postgresql' || this.provider === 'pgvector') {
|
|
459
488
|
const pool = await this.getPGPool();
|
|
460
489
|
const res = await pool.query(
|
|
@@ -462,13 +491,13 @@ export class DatabaseStorage {
|
|
|
462
491
|
FROM ${this.feedbackTableName}
|
|
463
492
|
ORDER BY created_at DESC`
|
|
464
493
|
);
|
|
465
|
-
|
|
494
|
+
raw = res.rows;
|
|
466
495
|
} else if (this.provider === 'mongodb') {
|
|
467
496
|
await this.getMongoClient();
|
|
468
497
|
const db = this.getMongoDb();
|
|
469
498
|
const col = db.collection(this.feedbackTableName);
|
|
470
499
|
const items = await col.find({}).sort({ created_at: -1 }).toArray();
|
|
471
|
-
|
|
500
|
+
raw = items.map((item: any) => ({
|
|
472
501
|
id: item.id,
|
|
473
502
|
messageId: item.message_id,
|
|
474
503
|
sessionId: item.session_id,
|
|
@@ -478,8 +507,10 @@ export class DatabaseStorage {
|
|
|
478
507
|
}));
|
|
479
508
|
} else {
|
|
480
509
|
const list = this.readLocalFile(this.feedbackFile);
|
|
481
|
-
|
|
510
|
+
raw = [...list].sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
|
482
511
|
}
|
|
512
|
+
if (!projectIds || projectIds.length === 0) return raw;
|
|
513
|
+
return raw.filter((f) => this.sessionInScope(f.sessionId, projectIds));
|
|
483
514
|
}
|
|
484
515
|
|
|
485
516
|
async disconnect() {
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FreeTierLimitsGuard — Enforces strict operational quotas for Free Tier workspaces.
|
|
3
|
+
* Retrivora Free Tier (Hobby) policy enforcement & Request Units accounting.
|
|
4
|
+
*
|
|
5
|
+
* Inline copy for the published SDK package (@retrivora-ai/rag-engine). The
|
|
6
|
+
* canonical source lives in `services/billing/FreeTierLimitsGuard.ts` and is
|
|
7
|
+
* kept in sync for the monorepo via copy on changes; bundling it here ensures
|
|
8
|
+
* published dist builds don't depend on `@services/*` path aliases that only
|
|
9
|
+
* resolve inside the Turborepo workspace.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export interface WorkspaceUsageStats {
|
|
13
|
+
documentCount: number;
|
|
14
|
+
totalStorageBytes: number;
|
|
15
|
+
totalEmbeddingsCount: number;
|
|
16
|
+
dailyQueriesCount: number;
|
|
17
|
+
totalRequestUnitsUsed?: number;
|
|
18
|
+
dailyRequestUnitsUsed?: number;
|
|
19
|
+
trialStartedAt?: string;
|
|
20
|
+
concurrentRequests?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const FREE_TIER_QUOTAS = {
|
|
24
|
+
MAX_DOCUMENTS: 25,
|
|
25
|
+
MAX_FILE_SIZE_BYTES: 20 * 1024 * 1024,
|
|
26
|
+
MAX_STORAGE_BYTES: 250 * 1024 * 1024,
|
|
27
|
+
MAX_TRIAL_REQUEST_UNITS: 500,
|
|
28
|
+
MAX_DAILY_REQUEST_UNITS: 50,
|
|
29
|
+
TRIAL_DURATION_DAYS: 14,
|
|
30
|
+
GRACE_PERIOD_DAYS: 2,
|
|
31
|
+
MAX_RPM: 10,
|
|
32
|
+
MAX_RPH: 100,
|
|
33
|
+
MAX_RPD: 500,
|
|
34
|
+
MAX_CONCURRENT_REQUESTS: 2,
|
|
35
|
+
MAX_INPUT_TOKENS: 20000,
|
|
36
|
+
MAX_OUTPUT_TOKENS: 4000,
|
|
37
|
+
UPGRADE_REMINDER_DAYS: [7, 12, 14] as const,
|
|
38
|
+
MAX_USERS: 1,
|
|
39
|
+
MAX_NAMESPACES: 1,
|
|
40
|
+
MAX_PROJECTS: 1,
|
|
41
|
+
} as const;
|
|
42
|
+
|
|
43
|
+
export interface TrialTimestamps {
|
|
44
|
+
trial_started_at: string;
|
|
45
|
+
trial_expires_at: string;
|
|
46
|
+
grace_period_expires_at: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface TrialStatusResult {
|
|
50
|
+
status: 'active' | 'grace_period' | 'expired';
|
|
51
|
+
daysElapsed: number;
|
|
52
|
+
daysRemaining: number;
|
|
53
|
+
isGracePeriod: boolean;
|
|
54
|
+
isExpired: boolean;
|
|
55
|
+
upgradeReminderDue?: number;
|
|
56
|
+
usageReminderDue?: '80%' | '90%' | '100%';
|
|
57
|
+
reason?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface RequestCheckParams {
|
|
61
|
+
operationType?: string;
|
|
62
|
+
inputTokens?: number;
|
|
63
|
+
outputTokens?: number;
|
|
64
|
+
dailyUnitsUsed?: number;
|
|
65
|
+
totalUnitsUsed?: number;
|
|
66
|
+
trialStartedAt?: string | Date;
|
|
67
|
+
concurrentRequests?: number;
|
|
68
|
+
chunkCount?: number;
|
|
69
|
+
nowServerTime?: Date;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export class FreeTierLimitsGuard {
|
|
73
|
+
static calculateTrialTimestamps(startedAt?: Date | string): TrialTimestamps {
|
|
74
|
+
const startDate = startedAt ? new Date(startedAt) : new Date();
|
|
75
|
+
const trialStartedAtMs = startDate.getTime();
|
|
76
|
+
|
|
77
|
+
const trialExpiresAtMs = trialStartedAtMs + FREE_TIER_QUOTAS.TRIAL_DURATION_DAYS * 24 * 60 * 60 * 1000;
|
|
78
|
+
const gracePeriodExpiresAtMs =
|
|
79
|
+
trialStartedAtMs + (FREE_TIER_QUOTAS.TRIAL_DURATION_DAYS + FREE_TIER_QUOTAS.GRACE_PERIOD_DAYS) * 24 * 60 * 60 * 1000;
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
trial_started_at: new Date(trialStartedAtMs).toISOString(),
|
|
83
|
+
trial_expires_at: new Date(trialExpiresAtMs).toISOString(),
|
|
84
|
+
grace_period_expires_at: new Date(gracePeriodExpiresAtMs).toISOString(),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
static calculateRequestUnits(operationType: string, options?: { chunkCount?: number }): number {
|
|
89
|
+
const op = operationType.toLowerCase().trim();
|
|
90
|
+
|
|
91
|
+
if (
|
|
92
|
+
op.includes('suggestion') ||
|
|
93
|
+
op.includes('template') ||
|
|
94
|
+
op.includes('history') ||
|
|
95
|
+
op.includes('model') ||
|
|
96
|
+
op.includes('health') ||
|
|
97
|
+
op.includes('auth') ||
|
|
98
|
+
op.includes('license') ||
|
|
99
|
+
op.includes('usage') ||
|
|
100
|
+
op.includes('feedback') ||
|
|
101
|
+
op.includes('analytic')
|
|
102
|
+
) {
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (op.includes('image')) {
|
|
107
|
+
return 10;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (op.includes('embedding')) {
|
|
111
|
+
const chunks = options?.chunkCount ?? 1;
|
|
112
|
+
return Math.max(1, Math.ceil(chunks / 1000));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return 1;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
static checkTrialStatus(
|
|
119
|
+
startedAt?: Date | string,
|
|
120
|
+
totalUnitsUsed = 0,
|
|
121
|
+
nowServerTime = new Date()
|
|
122
|
+
): TrialStatusResult {
|
|
123
|
+
const timestamps = this.calculateTrialTimestamps(startedAt);
|
|
124
|
+
const startMs = new Date(timestamps.trial_started_at).getTime();
|
|
125
|
+
const expireMs = new Date(timestamps.trial_expires_at).getTime();
|
|
126
|
+
const graceExpireMs = new Date(timestamps.grace_period_expires_at).getTime();
|
|
127
|
+
const nowMs = nowServerTime.getTime();
|
|
128
|
+
|
|
129
|
+
const daysElapsed = Math.floor(Math.max(0, nowMs - startMs) / (1000 * 60 * 60 * 24));
|
|
130
|
+
const daysRemaining = Math.max(0, Math.ceil((expireMs - nowMs) / (1000 * 60 * 60 * 24)));
|
|
131
|
+
|
|
132
|
+
const isConsumedUnits = totalUnitsUsed >= FREE_TIER_QUOTAS.MAX_TRIAL_REQUEST_UNITS;
|
|
133
|
+
const isExpiredTime = nowMs > expireMs;
|
|
134
|
+
const isPastGraceTime = nowMs > graceExpireMs;
|
|
135
|
+
|
|
136
|
+
let status: 'active' | 'grace_period' | 'expired' = 'active';
|
|
137
|
+
let reason: string | undefined;
|
|
138
|
+
|
|
139
|
+
if (isPastGraceTime || (isConsumedUnits && isExpiredTime)) {
|
|
140
|
+
status = 'expired';
|
|
141
|
+
reason = 'Trial period and grace period have expired. Upgrade your plan.';
|
|
142
|
+
} else if (isExpiredTime || isConsumedUnits) {
|
|
143
|
+
status = 'grace_period';
|
|
144
|
+
reason = isConsumedUnits
|
|
145
|
+
? 'Total trial request quota (500 units) consumed. Grace period active (dashboard access only).'
|
|
146
|
+
: '14-day trial period expired. 2-day grace period active (dashboard access only).';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let upgradeReminderDue: number | undefined;
|
|
150
|
+
if (FREE_TIER_QUOTAS.UPGRADE_REMINDER_DAYS.includes(daysElapsed as any)) {
|
|
151
|
+
upgradeReminderDue = daysElapsed;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let usageReminderDue: '80%' | '90%' | '100%' | undefined;
|
|
155
|
+
const usagePercent = (totalUnitsUsed / FREE_TIER_QUOTAS.MAX_TRIAL_REQUEST_UNITS) * 100;
|
|
156
|
+
if (usagePercent >= 100) {
|
|
157
|
+
usageReminderDue = '100%';
|
|
158
|
+
} else if (usagePercent >= 90) {
|
|
159
|
+
usageReminderDue = '90%';
|
|
160
|
+
} else if (usagePercent >= 80) {
|
|
161
|
+
usageReminderDue = '80%';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
status,
|
|
166
|
+
daysElapsed,
|
|
167
|
+
daysRemaining,
|
|
168
|
+
isGracePeriod: status === 'grace_period',
|
|
169
|
+
isExpired: status === 'expired',
|
|
170
|
+
upgradeReminderDue,
|
|
171
|
+
usageReminderDue,
|
|
172
|
+
reason,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
static checkIngestionAllowed(
|
|
177
|
+
stats: Partial<WorkspaceUsageStats>,
|
|
178
|
+
incomingSizeBytes = 0
|
|
179
|
+
): { allowed: boolean; reason?: string } {
|
|
180
|
+
const docs = stats.documentCount ?? 0;
|
|
181
|
+
const storage = stats.totalStorageBytes ?? 0;
|
|
182
|
+
|
|
183
|
+
if (incomingSizeBytes > FREE_TIER_QUOTAS.MAX_FILE_SIZE_BYTES) {
|
|
184
|
+
const mbSize = (incomingSizeBytes / (1024 * 1024)).toFixed(1);
|
|
185
|
+
return {
|
|
186
|
+
allowed: false,
|
|
187
|
+
reason: `[FreeTierIngestor] File size (${mbSize}MB) exceeds maximum allowed size of 20MB under Free Tier rules.`,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (docs >= FREE_TIER_QUOTAS.MAX_DOCUMENTS) {
|
|
192
|
+
return {
|
|
193
|
+
allowed: false,
|
|
194
|
+
reason: `Document limit reached (${docs}/${FREE_TIER_QUOTAS.MAX_DOCUMENTS}). Upgrade to Pro to ingest more documents.`,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (storage + incomingSizeBytes > FREE_TIER_QUOTAS.MAX_STORAGE_BYTES) {
|
|
199
|
+
const mbUsed = (storage / (1024 * 1024)).toFixed(1);
|
|
200
|
+
return {
|
|
201
|
+
allowed: false,
|
|
202
|
+
reason: `Storage quota exceeded (${mbUsed}MB / 250MB). Upgrade for unlimited storage.`,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return { allowed: true };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
static checkRequestAllowed(params: RequestCheckParams): { allowed: boolean; reason?: string; costUnits?: number } {
|
|
210
|
+
const op = params.operationType ?? 'chat';
|
|
211
|
+
const costUnits = this.calculateRequestUnits(op, { chunkCount: params.chunkCount });
|
|
212
|
+
|
|
213
|
+
if (costUnits === 0) {
|
|
214
|
+
return { allowed: true, costUnits: 0 };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (params.inputTokens && params.inputTokens > FREE_TIER_QUOTAS.MAX_INPUT_TOKENS) {
|
|
218
|
+
return {
|
|
219
|
+
allowed: false,
|
|
220
|
+
reason: 'Token limit exceeded. Upgrade your plan.',
|
|
221
|
+
costUnits,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
if (params.outputTokens && params.outputTokens > FREE_TIER_QUOTAS.MAX_OUTPUT_TOKENS) {
|
|
225
|
+
return {
|
|
226
|
+
allowed: false,
|
|
227
|
+
reason: 'Token limit exceeded. Upgrade your plan.',
|
|
228
|
+
costUnits,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (params.concurrentRequests && params.concurrentRequests > FREE_TIER_QUOTAS.MAX_CONCURRENT_REQUESTS) {
|
|
233
|
+
return {
|
|
234
|
+
allowed: false,
|
|
235
|
+
reason: `Concurrent limit exceeded (max ${FREE_TIER_QUOTAS.MAX_CONCURRENT_REQUESTS} active requests). Please wait for active streams to finish.`,
|
|
236
|
+
costUnits,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const totalUnits = params.totalUnitsUsed ?? 0;
|
|
241
|
+
const trialStatus = this.checkTrialStatus(params.trialStartedAt, totalUnits, params.nowServerTime);
|
|
242
|
+
|
|
243
|
+
if (trialStatus.status === 'expired' || trialStatus.status === 'grace_period') {
|
|
244
|
+
return {
|
|
245
|
+
allowed: false,
|
|
246
|
+
reason: trialStatus.reason || 'Trial limit reached. Upgrade your plan.',
|
|
247
|
+
costUnits,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (totalUnits + costUnits > FREE_TIER_QUOTAS.MAX_TRIAL_REQUEST_UNITS) {
|
|
252
|
+
return {
|
|
253
|
+
allowed: false,
|
|
254
|
+
reason: `Total trial request unit limit reached (${totalUnits}/${FREE_TIER_QUOTAS.MAX_TRIAL_REQUEST_UNITS}). Upgrade your plan.`,
|
|
255
|
+
costUnits,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const dailyUnits = params.dailyUnitsUsed ?? 0;
|
|
260
|
+
if (dailyUnits + costUnits > FREE_TIER_QUOTAS.MAX_DAILY_REQUEST_UNITS) {
|
|
261
|
+
return {
|
|
262
|
+
allowed: false,
|
|
263
|
+
reason: `Daily request quota limit reached (${dailyUnits}/${FREE_TIER_QUOTAS.MAX_DAILY_REQUEST_UNITS} units/day). Quota resets tomorrow.`,
|
|
264
|
+
costUnits,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return { allowed: true, costUnits };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
static checkQueryAllowed(dailyQueriesCount = 0): { allowed: boolean; reason?: string } {
|
|
272
|
+
if (dailyQueriesCount >= FREE_TIER_QUOTAS.MAX_DAILY_REQUEST_UNITS) {
|
|
273
|
+
return {
|
|
274
|
+
allowed: false,
|
|
275
|
+
reason: `Daily query limit reached (${FREE_TIER_QUOTAS.MAX_DAILY_REQUEST_UNITS}/day). Quota resets tomorrow.`,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return { allowed: true };
|
|
280
|
+
}
|
|
281
|
+
}
|
|
@@ -14,7 +14,6 @@ import { RagConfig } from "../config/RagConfig";
|
|
|
14
14
|
* - Invocation uses `{ messages: [...] }` instead of `{ input, chat_history }`.
|
|
15
15
|
*/
|
|
16
16
|
export class LangChainAgent {
|
|
17
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
18
17
|
private agent?: any;
|
|
19
18
|
|
|
20
19
|
constructor(private pipeline: Pipeline, private config: RagConfig) {}
|
|
@@ -75,7 +74,6 @@ export class LangChainAgent {
|
|
|
75
74
|
}
|
|
76
75
|
|
|
77
76
|
this.agent = createAgent({
|
|
78
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
79
77
|
model: chatModel as any,
|
|
80
78
|
tools: [searchTool],
|
|
81
79
|
systemPrompt: finalSystemPrompt,
|
|
@@ -2,31 +2,79 @@ import { SDK_VERSION } from '../version';
|
|
|
2
2
|
import { SDKVersionUnsupportedError, LicenseValidationError } from '../exceptions';
|
|
3
3
|
import { LicenseVerifier } from './LicenseVerifier';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Input contract for `LicenseValidator.validate()`.
|
|
7
|
+
*
|
|
8
|
+
* At least one source must yield a license key: `licenseKey` field, an
|
|
9
|
+
* `x-license-key` entry in `headers`, or the process environment variables
|
|
10
|
+
* (NEXT_PUBLIC_RETRIVORA_LICENSE_KEY / RETRIVORA_LICENSE_KEY).
|
|
11
|
+
*/
|
|
5
12
|
export interface LicenseValidationRequest {
|
|
13
|
+
/** Explicit `rtv_`-prefixed license key passed as a prop (highest priority). */
|
|
6
14
|
licenseKey: string;
|
|
15
|
+
/** Project ID the caller is running under, checked for license binding match. */
|
|
7
16
|
projectId?: string;
|
|
17
|
+
/** SDK version reported for minimum-supported-version enforcement (defaults to SDK_VERSION). */
|
|
8
18
|
sdkVersion?: string;
|
|
19
|
+
/** SDK package identifier sent to server for observability. */
|
|
9
20
|
sdk?: string;
|
|
21
|
+
/** browser | node — reported for observability only. */
|
|
10
22
|
platform?: string;
|
|
23
|
+
/** Override base URL of the Retrivora API endpoint. Defaults to env or '/api/retrivora'. */
|
|
11
24
|
retrivoraApiBase?: string;
|
|
25
|
+
/** Custom headers that may contain `x-license-key` or `Authorization: Bearer`. */
|
|
12
26
|
headers?: Record<string, string>;
|
|
13
27
|
}
|
|
14
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Response contract returned from `LicenseValidator.validate()`.
|
|
31
|
+
*
|
|
32
|
+
* Status enumerations are ordered — anything other than ACTIVE means the
|
|
33
|
+
* widget/chat must be disabled and must not pass server-side handler checks.
|
|
34
|
+
*/
|
|
15
35
|
export interface LicenseValidationResponse {
|
|
36
|
+
/** True only when `licenseStatus` is ACTIVE and SDK version is supported. */
|
|
16
37
|
valid: boolean;
|
|
38
|
+
/** Lifecycle state of the license record. TERMINATED/SUSPENDED/EXPIRED/REVOKED must block chat. */
|
|
17
39
|
licenseStatus: 'ACTIVE' | 'SUSPENDED' | 'TERMINATED' | 'EXPIRED' | 'REVOKED';
|
|
40
|
+
/** Minimum SDK version that the server will accept (semver-compare). */
|
|
18
41
|
minimumSupportedVersion: string;
|
|
42
|
+
/** Latest generally-available SDK version (for upgrade prompts). */
|
|
19
43
|
latestVersion: string;
|
|
44
|
+
/** True when the SDK version is older than minimumSupportedVersion — UI must force upgrade. */
|
|
20
45
|
forceUpgrade: boolean;
|
|
46
|
+
/** RFC3339 expiration timestamp or null for perpetual licenses. */
|
|
21
47
|
expiresAt: string | null;
|
|
48
|
+
/** Human-readable status/reason string, suitable for tooltips/toasts. */
|
|
22
49
|
message: string;
|
|
23
50
|
}
|
|
24
51
|
|
|
52
|
+
/** Internal cache entry shape for successful validations only (failures are never cached). */
|
|
25
53
|
interface CachedSuccessValidation {
|
|
26
54
|
response: LicenseValidationResponse;
|
|
27
55
|
timestamp: number;
|
|
28
56
|
}
|
|
29
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Client-side orchestrator for license validation.
|
|
60
|
+
*
|
|
61
|
+
* Flow:
|
|
62
|
+
* 1. Resolve the license key (prop → header → env)
|
|
63
|
+
* 2. Return any fresh (< 5 min old) successful cached result
|
|
64
|
+
* 3. POST to `{retrivoraApiBase}/v1/license/validate` for server-side DB-backed status
|
|
65
|
+
* 4. On network or HTTP error, fall back to zero-latency local RSA signature
|
|
66
|
+
* verification via `LicenseVerifier.verify()`.
|
|
67
|
+
*
|
|
68
|
+
* Caching policy:
|
|
69
|
+
* - ONLY successful (valid + ACTIVE + !forceUpgrade) responses are cached
|
|
70
|
+
* - Any failed validation immediately purges the cache entry
|
|
71
|
+
* - TTL is 5 minutes for success cache to allow TERMINATED/REVOKED status
|
|
72
|
+
* changes on the server to propagate quickly.
|
|
73
|
+
*
|
|
74
|
+
* Thread safety: Singleton pattern via `getInstance()`. Used by ChatWidget,
|
|
75
|
+
* useRagChat hook, and the server-side `createLicenseHandler` indirectly through
|
|
76
|
+
* its own `LicenseVerifier.verify()` path.
|
|
77
|
+
*/
|
|
30
78
|
export class LicenseValidator {
|
|
31
79
|
private static instance: LicenseValidator;
|
|
32
80
|
private cache: Map<string, CachedSuccessValidation> = new Map();
|
|
@@ -34,6 +82,7 @@ export class LicenseValidator {
|
|
|
34
82
|
|
|
35
83
|
private constructor() {}
|
|
36
84
|
|
|
85
|
+
/** @returns Singleton instance of the validator (shared cache across all consumers). */
|
|
37
86
|
public static getInstance(): LicenseValidator {
|
|
38
87
|
if (!LicenseValidator.instance) {
|
|
39
88
|
LicenseValidator.instance = new LicenseValidator();
|
|
@@ -42,7 +91,12 @@ export class LicenseValidator {
|
|
|
42
91
|
}
|
|
43
92
|
|
|
44
93
|
/**
|
|
45
|
-
*
|
|
94
|
+
* Purge entries from the local success cache.
|
|
95
|
+
*
|
|
96
|
+
* @param licenseKey - Optional. If provided, only that key's cache entry is
|
|
97
|
+
* removed. If omitted the entire cache is cleared.
|
|
98
|
+
* Callers use this after any 401/403 to ensure the next
|
|
99
|
+
* validate() call re-validates against the server.
|
|
46
100
|
*/
|
|
47
101
|
public purgeCache(licenseKey?: string): void {
|
|
48
102
|
if (licenseKey) {
|
|
@@ -53,7 +107,17 @@ export class LicenseValidator {
|
|
|
53
107
|
}
|
|
54
108
|
|
|
55
109
|
/**
|
|
56
|
-
*
|
|
110
|
+
* Primary validation entry-point for UI / hook consumers.
|
|
111
|
+
*
|
|
112
|
+
* Resolves the license key, consults the local success cache, performs a
|
|
113
|
+
* server POST for authoritative status, and falls back to local RSA
|
|
114
|
+
* signature verification if the server can't be reached.
|
|
115
|
+
*
|
|
116
|
+
* @throws LicenseValidationError - When no key is provided OR both the
|
|
117
|
+
* server call AND the local-RSA fallback report an invalid/expired key.
|
|
118
|
+
* @throws SDKVersionUnsupportedError - When server reports forceUpgrade=true.
|
|
119
|
+
* @returns A `LicenseValidationResponse` that the UI can use to enable or
|
|
120
|
+
* lock the chat widget.
|
|
57
121
|
*/
|
|
58
122
|
public async validate(request: LicenseValidationRequest): Promise<LicenseValidationResponse> {
|
|
59
123
|
const {
|
|
@@ -1,15 +1,43 @@
|
|
|
1
1
|
import * as crypto from 'crypto';
|
|
2
2
|
import { ConfigurationException } from '../exceptions';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Decoded payload of a Retrivora license JWT.
|
|
6
|
+
*
|
|
7
|
+
* Retrivora licenses are signed with RS256 using a key held exclusively by
|
|
8
|
+
* Retrivora SaaS. They carry the project binding, the expiration time, and
|
|
9
|
+
* the customer tier, which the SDK uses to enforce per-tier feature limits
|
|
10
|
+
* (vector DB provider whitelist).
|
|
11
|
+
*/
|
|
4
12
|
export interface LicensePayload {
|
|
13
|
+
/** Project ID this license is bound to — used to prevent cross-tenant reuse. */
|
|
5
14
|
projectId: string;
|
|
6
|
-
|
|
15
|
+
/** UNIX timestamp in seconds after which the license must be considered expired. */
|
|
16
|
+
expiresAt: number;
|
|
17
|
+
/** Customer tier (hobby | pro | enterprise). */
|
|
7
18
|
tier: string;
|
|
19
|
+
/** Optional: License lifecycle status propagated by the server validator (ACTIVE/TERMINATED/etc). */
|
|
20
|
+
licenseStatus?: string;
|
|
8
21
|
}
|
|
9
22
|
|
|
10
23
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
24
|
+
* Zero-latency cryptographic license verifier.
|
|
25
|
+
*
|
|
26
|
+
* Responsibilities:
|
|
27
|
+
* 1. Parse the `rtv_`-prefixed JWT (base64url header.payload.signature)
|
|
28
|
+
* 2. Verify the RS256 signature against Retrivora's embedded PUBLIC_KEY
|
|
29
|
+
* 3. Match the license's projectId to the currently configured projectId
|
|
30
|
+
* 4. Enforce expiration (current UNIX seconds > expiresAt)
|
|
31
|
+
* 5. Enforce per-tier vector DB provider whitelist in production
|
|
32
|
+
*
|
|
33
|
+
* Fail-open policy: In non-production environments a missing license key
|
|
34
|
+
* produces a console-warning and a synthetic 24h "hobby" payload so that
|
|
35
|
+
* local development continues to work without signing up. In production
|
|
36
|
+
* any missing/invalid key throws `ConfigurationException`.
|
|
37
|
+
*
|
|
38
|
+
* Caching: Cryptographic verification is CPU-expensive — successful results
|
|
39
|
+
* are cached for 60 seconds keyed by (licenseKey, projectId, provider,
|
|
40
|
+
* publicKeyOverride). Cache hits still re-verify expiresAt.
|
|
13
41
|
*/
|
|
14
42
|
export class LicenseVerifier {
|
|
15
43
|
// A simple in-memory cache to save CPU overhead of cryptographic signature checks.
|
package/src/core/VectorPlugin.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { IngestDocument, ChatResponse } from '../types';
|
|
|
7
7
|
import { ChatMessage } from '../types';
|
|
8
8
|
import { LicenseVerifier } from './LicenseVerifier';
|
|
9
9
|
import { wrapError, RetrivoraErrorCode } from '../exceptions';
|
|
10
|
+
import { FreeTierLimitsGuard } from './FreeTierLimitsGuard';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* VectorPlugin — main orchestrator class.
|
|
@@ -78,6 +79,13 @@ export class VectorPlugin {
|
|
|
78
79
|
);
|
|
79
80
|
}
|
|
80
81
|
|
|
82
|
+
private estimateInputTokens(message: string, history: ChatMessage[] = []): number {
|
|
83
|
+
const allContent =
|
|
84
|
+
message.length +
|
|
85
|
+
history.reduce((acc, m) => acc + ((m as any)?.content?.length || (m as any)?.text?.length || 0), 0);
|
|
86
|
+
return Math.ceil(allContent / 4);
|
|
87
|
+
}
|
|
88
|
+
|
|
81
89
|
/**
|
|
82
90
|
* Run a chat query.
|
|
83
91
|
*/
|
|
@@ -88,6 +96,14 @@ export class VectorPlugin {
|
|
|
88
96
|
throw wrapError(err, 'CONFIGURATION_ERROR');
|
|
89
97
|
}
|
|
90
98
|
try {
|
|
99
|
+
const inputTokens = this.estimateInputTokens(message, history);
|
|
100
|
+
const tokenCheck = (FreeTierLimitsGuard as any).checkRequestAllowed({
|
|
101
|
+
operationType: 'chat',
|
|
102
|
+
inputTokens,
|
|
103
|
+
});
|
|
104
|
+
if (!tokenCheck.allowed && tokenCheck.reason?.toLowerCase().includes('token')) {
|
|
105
|
+
throw wrapError(new Error(tokenCheck.reason), 'RATE_LIMITED');
|
|
106
|
+
}
|
|
91
107
|
return await this.pipeline.ask(message, history, namespace);
|
|
92
108
|
} catch (err) {
|
|
93
109
|
const msg = String(err);
|
|
@@ -95,6 +111,9 @@ export class VectorPlugin {
|
|
|
95
111
|
if (msg.includes('Embed') || msg.includes('embed')) {
|
|
96
112
|
defaultCode = 'EMBEDDING_FAILED';
|
|
97
113
|
}
|
|
114
|
+
if (msg.toLowerCase().includes('token limit')) {
|
|
115
|
+
defaultCode = 'RATE_LIMITED';
|
|
116
|
+
}
|
|
98
117
|
throw wrapError(err, defaultCode);
|
|
99
118
|
}
|
|
100
119
|
}
|
|
@@ -109,6 +128,14 @@ export class VectorPlugin {
|
|
|
109
128
|
throw wrapError(err, 'CONFIGURATION_ERROR');
|
|
110
129
|
}
|
|
111
130
|
try {
|
|
131
|
+
const inputTokens = this.estimateInputTokens(message, history);
|
|
132
|
+
const tokenCheck = (FreeTierLimitsGuard as any).checkRequestAllowed({
|
|
133
|
+
operationType: 'chat_stream',
|
|
134
|
+
inputTokens,
|
|
135
|
+
});
|
|
136
|
+
if (!tokenCheck.allowed && tokenCheck.reason?.toLowerCase().includes('token')) {
|
|
137
|
+
throw wrapError(new Error(tokenCheck.reason), 'RATE_LIMITED');
|
|
138
|
+
}
|
|
112
139
|
const stream = this.pipeline.askStream(message, history, namespace);
|
|
113
140
|
for await (const chunk of stream) {
|
|
114
141
|
yield chunk;
|
|
@@ -119,6 +146,9 @@ export class VectorPlugin {
|
|
|
119
146
|
if (msg.includes('Embed') || msg.includes('embed')) {
|
|
120
147
|
defaultCode = 'EMBEDDING_FAILED';
|
|
121
148
|
}
|
|
149
|
+
if (msg.toLowerCase().includes('token limit')) {
|
|
150
|
+
defaultCode = 'RATE_LIMITED';
|
|
151
|
+
}
|
|
122
152
|
throw wrapError(err, defaultCode);
|
|
123
153
|
}
|
|
124
154
|
}
|