@retrivora-ai/rag-engine 2.2.9 → 2.3.1
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 +883 -328
- package/dist/handlers/index.mjs +883 -328
- 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 +456 -37
- package/dist/index.mjs +455 -39
- package/dist/server.d.mts +35 -73
- package/dist/server.d.ts +35 -73
- package/dist/server.js +914 -342
- package/dist/server.mjs +914 -342
- package/package.json +1 -1
- package/src/components/ChatWidget.tsx +147 -46
- package/src/components/ChatWindow.tsx +52 -8
- 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/LicenseValidator.ts +69 -3
- package/src/core/LicenseVerifier.ts +43 -14
- package/src/core/VectorPlugin.ts +30 -0
- package/src/handlers/index.ts +495 -38
- package/src/index.css +58 -0
- package/src/index.ts +4 -0
- package/src/server.ts +1 -0
- package/dist/index-BPJ3KDYI.d.ts +0 -195
- package/dist/index-Dmq5lH0j.d.mts +0 -195
package/src/handlers/index.ts
CHANGED
|
@@ -5,16 +5,38 @@ import { ChatMessage, ChatResponse } from '../types';
|
|
|
5
5
|
import { DocumentParser } from '../utils/DocumentParser';
|
|
6
6
|
import { UITransformer } from '../utils/UITransformer';
|
|
7
7
|
import { DatabaseStorage, HistoryMessage } from '../core/DatabaseStorage';
|
|
8
|
-
import { LicenseVerifier } from '../core/LicenseVerifier';
|
|
8
|
+
import { LicenseVerifier, LicensePayload } from '../core/LicenseVerifier';
|
|
9
9
|
import { SDK_VERSION } from '../version';
|
|
10
|
+
import { FreeTierLimitsGuard, FREE_TIER_QUOTAS } from '../core/FreeTierLimitsGuard';
|
|
10
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Shared options accepted by every handler factory.
|
|
14
|
+
*
|
|
15
|
+
* Pass `onAuthorize` to plug in per-request auth rules; the callback is
|
|
16
|
+
* called before rate-limiting, license enforcement, or any business logic
|
|
17
|
+
* runs. Return `true` or the resolved promise `true` to allow, `false`
|
|
18
|
+
* to get a 401, or return a `Response` to fully control the denial (for
|
|
19
|
+
* example redirecting an OAuth-style flow to a login page).
|
|
20
|
+
*/
|
|
11
21
|
export interface HandlerOptions {
|
|
12
|
-
|
|
22
|
+
/** Optional synchronous or asynchronous authorization gate. */
|
|
23
|
+
onAuthorize?: (req: NextRequest) => boolean | Response | Promise<boolean | Response>;
|
|
24
|
+
/**
|
|
25
|
+
* Optional scope resolver for multi-tenant deployments.
|
|
26
|
+
*
|
|
27
|
+
* Returns an allow-list of project ids that the caller is authorized to
|
|
28
|
+
* access on the `sessions`, `history`, and `feedback` endpoints. When
|
|
29
|
+
* the callback returns a non-empty array, server-side filtering is
|
|
30
|
+
* applied to every `listSessions`, `getHistory`, `clearHistory`,
|
|
31
|
+
* `listFeedback`, and `getFeedback` call so the caller can never leak
|
|
32
|
+
* another tenant's data even when client-side filters are tampered with.
|
|
33
|
+
*/
|
|
34
|
+
onResolveScope?: (req: NextRequest) => string[] | undefined | Promise<string[] | undefined>;
|
|
13
35
|
}
|
|
14
36
|
|
|
15
37
|
async function checkAuth(
|
|
16
38
|
req: NextRequest,
|
|
17
|
-
onAuthorize?: (req: NextRequest) => boolean | Promise<boolean
|
|
39
|
+
onAuthorize?: (req: NextRequest) => boolean | Response | Promise<boolean | Response>
|
|
18
40
|
): Promise<Response | null> {
|
|
19
41
|
if (!onAuthorize) return null;
|
|
20
42
|
try {
|
|
@@ -109,6 +131,115 @@ function checkRateLimit(req: NextRequest): Response | null {
|
|
|
109
131
|
return null;
|
|
110
132
|
}
|
|
111
133
|
|
|
134
|
+
// ─── Free Tier Limits Guard ───────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
const FREE_TIER_NAMES = new Set(['hobby', 'free', 'free_trial', 'trial']);
|
|
137
|
+
|
|
138
|
+
function isFreeTier(tier?: string | null): boolean {
|
|
139
|
+
if (!tier) return true;
|
|
140
|
+
return FREE_TIER_NAMES.has(tier.toLowerCase().trim());
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const _freeTierGuardInstance: any = _g.__retrivoraFreeTierGuard ??
|
|
144
|
+
(_g.__retrivoraFreeTierGuard = new (FreeTierLimitsGuard as any)());
|
|
145
|
+
|
|
146
|
+
function createPaymentRequiredResponse(reason: string, details?: Record<string, unknown>): Response {
|
|
147
|
+
const body = {
|
|
148
|
+
type: 'https://retrivora.com/docs/errors/payment-required',
|
|
149
|
+
title: 'Payment Required',
|
|
150
|
+
status: 402,
|
|
151
|
+
detail: reason,
|
|
152
|
+
code: 'FREE_TIER_LIMIT_EXCEEDED',
|
|
153
|
+
instance: undefined as string | undefined,
|
|
154
|
+
...(details ?? {}),
|
|
155
|
+
quota: {
|
|
156
|
+
maxDailyRequestUnits: FREE_TIER_QUOTAS.MAX_DAILY_REQUEST_UNITS,
|
|
157
|
+
maxTrialRequestUnits: FREE_TIER_QUOTAS.MAX_TRIAL_REQUEST_UNITS,
|
|
158
|
+
maxDocuments: FREE_TIER_QUOTAS.MAX_DOCUMENTS,
|
|
159
|
+
maxStorageBytes: FREE_TIER_QUOTAS.MAX_STORAGE_BYTES,
|
|
160
|
+
upgradeUrl: 'https://retrivora.com/pricing',
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
return new Response(JSON.stringify(body), {
|
|
164
|
+
status: 402,
|
|
165
|
+
headers: {
|
|
166
|
+
'Content-Type': 'application/problem+json',
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ─── License Enforcement ──────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Resolve the license key from a request, trying (in priority order):
|
|
175
|
+
* 1. `x-license-key` header
|
|
176
|
+
* 2. Body property `licenseKey` (already-parsed body, optional)
|
|
177
|
+
* 3. `Authorization: Bearer <key>` header
|
|
178
|
+
* 4. SDK/plugin config and environment variables
|
|
179
|
+
*/
|
|
180
|
+
function resolveLicenseKey(
|
|
181
|
+
req: NextRequest,
|
|
182
|
+
config: RagConfig,
|
|
183
|
+
bodyLicenseKey?: string
|
|
184
|
+
): string {
|
|
185
|
+
return (
|
|
186
|
+
req.headers.get('x-license-key') ||
|
|
187
|
+
bodyLicenseKey ||
|
|
188
|
+
req.headers.get('authorization')?.replace(/^Bearer\s+/i, '') ||
|
|
189
|
+
config.licenseKey ||
|
|
190
|
+
process.env.NEXT_PUBLIC_RETRIVORA_LICENSE_KEY ||
|
|
191
|
+
process.env.RETRIVORA_LICENSE_KEY ||
|
|
192
|
+
process.env.RAG_LICENSE_KEY ||
|
|
193
|
+
''
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Enforce a valid Retrivora license key.
|
|
199
|
+
*
|
|
200
|
+
* - Extracts license key from headers / body / env / config
|
|
201
|
+
* - Performs cryptographic + expiration validation via `LicenseVerifier.verify`
|
|
202
|
+
* - Returns `null` when validation passes (contains the decoded payload for future tier checks)
|
|
203
|
+
* - Returns a 403 Response ready to be sent when validation fails
|
|
204
|
+
*
|
|
205
|
+
* In development (`NODE_ENV !== 'production'`) a missing key still produces a
|
|
206
|
+
* console warning but passes (fail-open local-dev behavior); in production any
|
|
207
|
+
* invalid / expired / missing key produces a hard 403.
|
|
208
|
+
*/
|
|
209
|
+
function enforceLicense(
|
|
210
|
+
req: NextRequest,
|
|
211
|
+
config: RagConfig,
|
|
212
|
+
bodyLicenseKey?: string
|
|
213
|
+
): { ok: true; payload: LicensePayload } | { ok: false; response: Response } {
|
|
214
|
+
const rawKey = resolveLicenseKey(req, config, bodyLicenseKey);
|
|
215
|
+
const licenseKey = (rawKey || '').trim().replace(/^["']|["']$/g, '').trim();
|
|
216
|
+
const projectId = config.projectId || 'my-rag-app';
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
const payload = LicenseVerifier.verify(licenseKey, projectId);
|
|
220
|
+
return { ok: true, payload };
|
|
221
|
+
} catch (err: any) {
|
|
222
|
+
const message =
|
|
223
|
+
err instanceof Error
|
|
224
|
+
? err.message
|
|
225
|
+
: 'Missing or invalid RETRIVORA_LICENSE_KEY. Set NEXT_PUBLIC_RETRIVORA_LICENSE_KEY in your environment or contact your administrator.';
|
|
226
|
+
const body = {
|
|
227
|
+
error: {
|
|
228
|
+
code: 'LICENSE_REVOKED',
|
|
229
|
+
message: 'Your Retrivora license key has been terminated, suspended, or revoked. Access denied.',
|
|
230
|
+
},
|
|
231
|
+
details: message,
|
|
232
|
+
};
|
|
233
|
+
return {
|
|
234
|
+
ok: false,
|
|
235
|
+
response: new Response(JSON.stringify(body), {
|
|
236
|
+
status: 403,
|
|
237
|
+
headers: { 'Content-Type': 'application/json' },
|
|
238
|
+
}),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
112
243
|
// ─── Input Sanitization ───────────────────────────────────────────────────────
|
|
113
244
|
|
|
114
245
|
const MAX_MESSAGE_LENGTH = 8000;
|
|
@@ -205,8 +336,6 @@ function reportTelemetry(
|
|
|
205
336
|
details,
|
|
206
337
|
};
|
|
207
338
|
|
|
208
|
-
console.log(`[Retrivora SDK Telemetry] Dispatching telemetry -> Feature: "${action}", Status: "${status}", Project: "${projectId}", Latency: ${latencyMs}ms`);
|
|
209
|
-
|
|
210
339
|
fetch(absoluteUrl, {
|
|
211
340
|
method: 'POST',
|
|
212
341
|
headers: {
|
|
@@ -272,7 +401,29 @@ const SSE_HEADERS: Record<string, string> = {
|
|
|
272
401
|
// ─── Handler Factories ────────────────────────────────────────────────────────
|
|
273
402
|
|
|
274
403
|
/**
|
|
275
|
-
*
|
|
404
|
+
* Build a Next.js App Router POST handler for **synchronous** RAG chat.
|
|
405
|
+
*
|
|
406
|
+
* Pipeline (in execution order):
|
|
407
|
+
* 1. `options.onAuthorize` — custom authorization gate (optional)
|
|
408
|
+
* 2. In-memory IP rate limiter (30 req / 60 s sliding window)
|
|
409
|
+
* 3. JSON body parse → `enforceLicense()` RSA signature validation
|
|
410
|
+
* 4. Input sanitization + length / toxicity / PII / prompt-injection heuristics
|
|
411
|
+
* 5. User-message persistence via `DatabaseStorage`
|
|
412
|
+
* 6. LLM-driven RAG retrieval via `plugin.chat()`
|
|
413
|
+
* 7. Observability UI transformation + response persistence
|
|
414
|
+
* 8. JSON response (`ChatResponse` shape)
|
|
415
|
+
*
|
|
416
|
+
* Use this handler when you don't need streaming (e.g. server-side SDKs).
|
|
417
|
+
* For streaming use `createStreamHandler` — the same pipeline but with
|
|
418
|
+
* Server-Sent Events for incremental tokens.
|
|
419
|
+
*
|
|
420
|
+
* @param configOrPlugin - Either a partial `RagConfig` (a plugin will be
|
|
421
|
+
* created or reused for you) or an already-constructed
|
|
422
|
+
* `VectorPlugin` instance.
|
|
423
|
+
* @param options - See `HandlerOptions`. `onAuthorize` is recommended
|
|
424
|
+
* for multi-tenant deployments to gate access.
|
|
425
|
+
* @returns An async `POST(req, ctx)` function suitable for being the default
|
|
426
|
+
* export of a Next.js route file.
|
|
276
427
|
*/
|
|
277
428
|
export function createChatHandler(
|
|
278
429
|
configOrPlugin?: Partial<RagConfig> | VectorPlugin,
|
|
@@ -290,9 +441,38 @@ export function createChatHandler(
|
|
|
290
441
|
const rateLimited = checkRateLimit(req);
|
|
291
442
|
if (rateLimited) return rateLimited;
|
|
292
443
|
|
|
444
|
+
const config = plugin.getConfig();
|
|
445
|
+
|
|
293
446
|
try {
|
|
294
447
|
const body = await req.json();
|
|
295
448
|
const bodyObj = (body ?? {}) as any;
|
|
449
|
+
|
|
450
|
+
// 2. License enforcement (after body parse so we can read body.licenseKey)
|
|
451
|
+
const licenseCheck = enforceLicense(req as NextRequest, config, bodyObj?.licenseKey);
|
|
452
|
+
if (!licenseCheck.ok) return licenseCheck.response as any;
|
|
453
|
+
|
|
454
|
+
// 2b. Free Tier limits check (hobby / free / free_trial tiers)
|
|
455
|
+
if (isFreeTier(licenseCheck.payload.tier)) {
|
|
456
|
+
const estimatedInputTokens = Math.ceil(
|
|
457
|
+
((bodyObj.message?.length || 0) +
|
|
458
|
+
(Array.isArray(bodyObj.messages)
|
|
459
|
+
? bodyObj.messages.reduce((a: number, m: any) => a + (m?.content?.length || m?.text?.length || 0), 0)
|
|
460
|
+
: 0) +
|
|
461
|
+
(bodyObj.history?.reduce((a: number, m: any) => a + (m?.content?.length || m?.text?.length || 0), 0) || 0)) / 4
|
|
462
|
+
);
|
|
463
|
+
const chatCheck = (FreeTierLimitsGuard as any).checkRequestAllowed({
|
|
464
|
+
operationType: 'chat',
|
|
465
|
+
inputTokens: estimatedInputTokens,
|
|
466
|
+
projectId: licenseCheck.payload.projectId,
|
|
467
|
+
});
|
|
468
|
+
if (!chatCheck.allowed) {
|
|
469
|
+
return createPaymentRequiredResponse(
|
|
470
|
+
chatCheck.reason || 'Free tier quota exceeded.',
|
|
471
|
+
{ tier: licenseCheck.payload.tier, projectId: licenseCheck.payload.projectId }
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
296
476
|
let rawMessage = bodyObj.message;
|
|
297
477
|
if (!rawMessage && Array.isArray(bodyObj.messages) && bodyObj.messages.length > 0) {
|
|
298
478
|
const lastMsg = bodyObj.messages[bodyObj.messages.length - 1];
|
|
@@ -304,7 +484,7 @@ export function createChatHandler(
|
|
|
304
484
|
const history = bodyObj.history || (Array.isArray(bodyObj.messages) ? bodyObj.messages.slice(0, -1) : []);
|
|
305
485
|
const { namespace, sessionId = 'default', messageId, userMessageId } = bodyObj;
|
|
306
486
|
|
|
307
|
-
//
|
|
487
|
+
// 3. Input sanitization
|
|
308
488
|
const sanitized = sanitizeInput(rawMessage ?? '');
|
|
309
489
|
if (!sanitized.ok) {
|
|
310
490
|
return NextResponse.json({ error: { code: 'INVALID_INPUT', message: sanitized.error } }, { status: 400 });
|
|
@@ -348,7 +528,30 @@ export function createChatHandler(
|
|
|
348
528
|
}
|
|
349
529
|
|
|
350
530
|
/**
|
|
351
|
-
*
|
|
531
|
+
* Build a Next.js App Router POST handler for **streaming** RAG chat via SSE.
|
|
532
|
+
*
|
|
533
|
+
* Same pipeline as `createChatHandler`, but the `plugin.chat()` call is run
|
|
534
|
+
* in streaming mode and every token is immediately emitted as a Server-Sent
|
|
535
|
+
* Event frame. The stream contains frames for `text`, `thinking`,
|
|
536
|
+
* `metadata`, `ui_transformation`, `observability`, and a terminal `done`.
|
|
537
|
+
*
|
|
538
|
+
* Multipart upload short-circuit: When the request `Content-Type` is
|
|
539
|
+
* `multipart/form-data` the request is transparently delegated to
|
|
540
|
+
* `createUploadHandler` (which also runs its own `enforceLicense()` gate).
|
|
541
|
+
*
|
|
542
|
+
* Pipeline (in execution order):
|
|
543
|
+
* 1. `options.onAuthorize` — custom authorization gate (optional)
|
|
544
|
+
* 2. In-memory IP rate limiter (30 req / 60 s sliding window)
|
|
545
|
+
* 3. JSON body parse (or form-data upload delegation)
|
|
546
|
+
* 4. `enforceLicense()` RSA signature validation — 403 if missing / terminated
|
|
547
|
+
* 5. Input sanitization + length / toxicity / PII / prompt-injection heuristics
|
|
548
|
+
* 6. User-message persistence via `DatabaseStorage`
|
|
549
|
+
* 7. Streaming RAG retrieval via `plugin.chat({ stream: true })`
|
|
550
|
+
* 8. Persist assistant message + emit SSE frames
|
|
551
|
+
*
|
|
552
|
+
* @param configOrPlugin - Partial `RagConfig` or an existing `VectorPlugin`.
|
|
553
|
+
* @param options - See `HandlerOptions`.
|
|
554
|
+
* @returns Async `POST(req, ctx)` function for `app/.../route.ts`.
|
|
352
555
|
*/
|
|
353
556
|
export function createStreamHandler(
|
|
354
557
|
configOrPlugin?: Partial<RagConfig> | VectorPlugin,
|
|
@@ -366,6 +569,8 @@ export function createStreamHandler(
|
|
|
366
569
|
const rateLimited = checkRateLimit(req);
|
|
367
570
|
if (rateLimited) return rateLimited;
|
|
368
571
|
|
|
572
|
+
const config = plugin.getConfig();
|
|
573
|
+
|
|
369
574
|
let body: {
|
|
370
575
|
message?: string;
|
|
371
576
|
history?: ChatMessage[];
|
|
@@ -373,9 +578,11 @@ export function createStreamHandler(
|
|
|
373
578
|
sessionId?: string;
|
|
374
579
|
messageId?: string;
|
|
375
580
|
userMessageId?: string;
|
|
581
|
+
licenseKey?: string;
|
|
376
582
|
};
|
|
377
583
|
try {
|
|
378
584
|
if (req.headers.get('content-type')?.includes('multipart/form-data')) {
|
|
585
|
+
// Uploads bypass the JSON body parse — createUploadHandler enforces its own license check
|
|
379
586
|
const uploader = createUploadHandler(plugin, { onAuthorize });
|
|
380
587
|
return uploader(req);
|
|
381
588
|
}
|
|
@@ -388,6 +595,33 @@ export function createStreamHandler(
|
|
|
388
595
|
}
|
|
389
596
|
|
|
390
597
|
const bodyObj = (body ?? {}) as any;
|
|
598
|
+
|
|
599
|
+
// 2b. License enforcement (for JSON-body requests)
|
|
600
|
+
const licenseCheck = enforceLicense(req as NextRequest, config, bodyObj?.licenseKey);
|
|
601
|
+
if (!licenseCheck.ok) return licenseCheck.response as any;
|
|
602
|
+
|
|
603
|
+
// 2c. Free Tier limits check (hobby / free / free_trial tiers)
|
|
604
|
+
if (isFreeTier(licenseCheck.payload.tier)) {
|
|
605
|
+
const estimatedInputTokens = Math.ceil(
|
|
606
|
+
((bodyObj.message?.length || 0) +
|
|
607
|
+
(Array.isArray(bodyObj.messages)
|
|
608
|
+
? bodyObj.messages.reduce((a: number, m: any) => a + (m?.content?.length || m?.text?.length || 0), 0)
|
|
609
|
+
: 0) +
|
|
610
|
+
(bodyObj.history?.reduce((a: number, m: any) => a + (m?.content?.length || m?.text?.length || 0), 0) || 0)) / 4
|
|
611
|
+
);
|
|
612
|
+
const chatCheck = (FreeTierLimitsGuard as any).checkRequestAllowed({
|
|
613
|
+
operationType: 'chat_stream',
|
|
614
|
+
inputTokens: estimatedInputTokens,
|
|
615
|
+
projectId: licenseCheck.payload.projectId,
|
|
616
|
+
});
|
|
617
|
+
if (!chatCheck.allowed) {
|
|
618
|
+
return createPaymentRequiredResponse(
|
|
619
|
+
chatCheck.reason || 'Free tier quota exceeded.',
|
|
620
|
+
{ tier: licenseCheck.payload.tier, projectId: licenseCheck.payload.projectId }
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
391
625
|
let rawMessage = bodyObj.message;
|
|
392
626
|
if (!rawMessage && Array.isArray(bodyObj.messages) && bodyObj.messages.length > 0) {
|
|
393
627
|
const lastMsg = bodyObj.messages[bodyObj.messages.length - 1];
|
|
@@ -399,7 +633,7 @@ export function createStreamHandler(
|
|
|
399
633
|
const history = bodyObj.history || (Array.isArray(bodyObj.messages) ? bodyObj.messages.slice(0, -1) : []);
|
|
400
634
|
const { namespace, sessionId = 'default', messageId, userMessageId } = bodyObj;
|
|
401
635
|
|
|
402
|
-
//
|
|
636
|
+
// 3. Input sanitization
|
|
403
637
|
const sanitized = sanitizeInput(rawMessage ?? '');
|
|
404
638
|
if (!sanitized.ok) {
|
|
405
639
|
return new Response(
|
|
@@ -530,7 +764,19 @@ export function createStreamHandler(
|
|
|
530
764
|
}
|
|
531
765
|
|
|
532
766
|
/**
|
|
533
|
-
*
|
|
767
|
+
* Build a POST handler that ingests raw pre-parsed documents into the vector DB.
|
|
768
|
+
*
|
|
769
|
+
* Unlike `createUploadHandler` (which takes files and does parsing/splitting),
|
|
770
|
+
* this endpoint expects the caller to supply `documents[]` already split and
|
|
771
|
+
* prepared. This is useful for bulk backfill jobs, migrations, or programmatic
|
|
772
|
+
* sync pipelines.
|
|
773
|
+
*
|
|
774
|
+
* Security: Requires a valid license (RSA signature check via `enforceLicense`)
|
|
775
|
+
* AFTER JSON body parse so the optional `body.licenseKey` override is read.
|
|
776
|
+
*
|
|
777
|
+
* @param configOrPlugin - Partial `RagConfig` or existing `VectorPlugin`.
|
|
778
|
+
* @param options - Optional `onAuthorize` gate.
|
|
779
|
+
* @returns Async POST handler.
|
|
534
780
|
*/
|
|
535
781
|
export function createIngestHandler(
|
|
536
782
|
configOrPlugin?: Partial<RagConfig> | VectorPlugin,
|
|
@@ -543,8 +789,15 @@ export function createIngestHandler(
|
|
|
543
789
|
const authResult = await checkAuth(req, onAuthorize);
|
|
544
790
|
if (authResult) return authResult as any;
|
|
545
791
|
|
|
792
|
+
const config = plugin.getConfig();
|
|
793
|
+
|
|
546
794
|
try {
|
|
547
795
|
const body = await req.json();
|
|
796
|
+
|
|
797
|
+
// License enforcement before ingestion
|
|
798
|
+
const licenseCheck = enforceLicense(req as NextRequest, config, body?.licenseKey);
|
|
799
|
+
if (!licenseCheck.ok) return licenseCheck.response as any;
|
|
800
|
+
|
|
548
801
|
const { documents, namespace } = body as {
|
|
549
802
|
documents: Array<{ docId: string; content: string; metadata?: Record<string, unknown> }>;
|
|
550
803
|
namespace?: string;
|
|
@@ -554,6 +807,29 @@ export function createIngestHandler(
|
|
|
554
807
|
return NextResponse.json({ error: 'documents array is required' }, { status: 400 });
|
|
555
808
|
}
|
|
556
809
|
|
|
810
|
+
// Free Tier ingestion limits check
|
|
811
|
+
if (isFreeTier(licenseCheck.payload.tier)) {
|
|
812
|
+
const totalIncomingBytes = documents.reduce(
|
|
813
|
+
(sum, d) => sum + Buffer.byteLength(d.content || '', 'utf8'),
|
|
814
|
+
0
|
|
815
|
+
);
|
|
816
|
+
const ingestCheck = (FreeTierLimitsGuard as any).checkIngestionAllowed(
|
|
817
|
+
{ documentCount: documents.length },
|
|
818
|
+
totalIncomingBytes
|
|
819
|
+
);
|
|
820
|
+
if (!ingestCheck.allowed) {
|
|
821
|
+
return createPaymentRequiredResponse(
|
|
822
|
+
ingestCheck.reason || 'Free tier ingestion limit reached.',
|
|
823
|
+
{
|
|
824
|
+
tier: licenseCheck.payload.tier,
|
|
825
|
+
projectId: licenseCheck.payload.projectId,
|
|
826
|
+
documents: documents.length,
|
|
827
|
+
incomingBytes: totalIncomingBytes,
|
|
828
|
+
}
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
557
833
|
const results = await plugin.ingest(documents, namespace);
|
|
558
834
|
reportTelemetry(req, plugin, 'DOCUMENT_INGESTION', 'success', `Ingested ${documents.length} document(s)`);
|
|
559
835
|
return NextResponse.json({ results });
|
|
@@ -566,7 +842,28 @@ export function createIngestHandler(
|
|
|
566
842
|
}
|
|
567
843
|
|
|
568
844
|
/**
|
|
569
|
-
*
|
|
845
|
+
* Build a GET handler that returns health / readiness status.
|
|
846
|
+
*
|
|
847
|
+
* Response shape (200 OK):
|
|
848
|
+
* ```json
|
|
849
|
+
* {
|
|
850
|
+
* "ok": true,
|
|
851
|
+
* "status": "healthy",
|
|
852
|
+
* "sdkVersion": "x.y.z",
|
|
853
|
+
* "timestamp": "ISO string",
|
|
854
|
+
* "embedding": { "provider": "...", "ok": true },
|
|
855
|
+
* "vectorStore": { "provider": "...", "ok": true }
|
|
856
|
+
* }
|
|
857
|
+
* ```
|
|
858
|
+
*
|
|
859
|
+
* License check: This endpoint is intentionally **NOT** license-gated so
|
|
860
|
+
* orchestrators (Kubernetes, uptime probes, Vercel cron pings) can use it
|
|
861
|
+
* to assert the process is alive even when no license has been configured
|
|
862
|
+
* yet during deploy.
|
|
863
|
+
*
|
|
864
|
+
* @param configOrPlugin - Partial `RagConfig` or existing `VectorPlugin`.
|
|
865
|
+
* @param options - Optional `onAuthorize` gate (rarely used here).
|
|
866
|
+
* @returns Async GET handler.
|
|
570
867
|
*/
|
|
571
868
|
export function createHealthHandler(
|
|
572
869
|
configOrPlugin?: Partial<RagConfig> | VectorPlugin,
|
|
@@ -597,7 +894,19 @@ export function createHealthHandler(
|
|
|
597
894
|
}
|
|
598
895
|
|
|
599
896
|
/**
|
|
600
|
-
*
|
|
897
|
+
* Build a POST handler for the SDK-facing `/v1/license/validate` endpoint.
|
|
898
|
+
*
|
|
899
|
+
* Client flow: `LicenseValidator.validate()` calls this URL, receives
|
|
900
|
+
* authoritative DB-backed status (ACTIVE / TERMINATED / SUSPENDED / EXPIRED /
|
|
901
|
+
* REVOKED), and falls back to pure-local `LicenseVerifier.verify()` if the
|
|
902
|
+
* server is unreachable or returns an error.
|
|
903
|
+
*
|
|
904
|
+
* This handler does NOT enforce license — it is the verifier itself. It does,
|
|
905
|
+
* however, still apply rate limiting to mitigate abuse.
|
|
906
|
+
*
|
|
907
|
+
* @param configOrPlugin - Partial `RagConfig` or existing `VectorPlugin`.
|
|
908
|
+
* @param options - Optional `onAuthorize` gate.
|
|
909
|
+
* @returns Async POST handler returning `LicenseValidationResponse` JSON.
|
|
601
910
|
*/
|
|
602
911
|
export function createLicenseHandler(
|
|
603
912
|
configOrPlugin?: Partial<RagConfig> | VectorPlugin,
|
|
@@ -615,15 +924,17 @@ export function createLicenseHandler(
|
|
|
615
924
|
try {
|
|
616
925
|
const body = await req.json().catch(() => ({}));
|
|
617
926
|
const config = plugin.getConfig();
|
|
618
|
-
const
|
|
927
|
+
const rawKey =
|
|
619
928
|
req.headers.get('x-license-key') ||
|
|
620
929
|
body?.licenseKey ||
|
|
621
930
|
req.headers.get('authorization')?.replace(/^Bearer\s+/i, '') ||
|
|
622
931
|
config.licenseKey ||
|
|
623
|
-
process.env.RETRIVORA_LICENSE_KEY ||
|
|
624
932
|
process.env.NEXT_PUBLIC_RETRIVORA_LICENSE_KEY ||
|
|
933
|
+
process.env.RETRIVORA_LICENSE_KEY ||
|
|
625
934
|
'';
|
|
626
935
|
|
|
936
|
+
const licenseKey = (rawKey || '').trim().replace(/^["']|["']$/g, '').trim();
|
|
937
|
+
|
|
627
938
|
const projectId = body?.projectId || config.projectId || 'my-rag-app';
|
|
628
939
|
|
|
629
940
|
const payload = LicenseVerifier.verify(licenseKey, projectId);
|
|
@@ -655,7 +966,21 @@ export function createLicenseHandler(
|
|
|
655
966
|
}
|
|
656
967
|
|
|
657
968
|
/**
|
|
658
|
-
*
|
|
969
|
+
* Build a POST handler that accepts `multipart/form-data` file uploads.
|
|
970
|
+
*
|
|
971
|
+
* Workflow:
|
|
972
|
+
* 1. `onAuthorize` → license enforcement via `enforceLicense` (no JSON body,
|
|
973
|
+
* so the key must come from headers or the environment).
|
|
974
|
+
* 2. Read FormData files[] list → max 25 files per request, 100 MB each.
|
|
975
|
+
* 3. Per-file: use `DocumentParser` to extract raw text by MIME type (PDF,
|
|
976
|
+
* DOCX, TXT, MD, HTML, JSON, CSV, images via OCR fallback, etc.).
|
|
977
|
+
* 4. Chunk by the configured chunker (RecursiveCharacter, semantic, etc.).
|
|
978
|
+
* 5. Call `plugin.ingest()` to embed and insert into the vector store.
|
|
979
|
+
* 6. Return JSON summary per file (status, chunks ingested, docId).
|
|
980
|
+
*
|
|
981
|
+
* @param configOrPlugin - Partial `RagConfig` or existing `VectorPlugin`.
|
|
982
|
+
* @param options - Optional `onAuthorize` gate.
|
|
983
|
+
* @returns Async POST handler.
|
|
659
984
|
*/
|
|
660
985
|
export function createUploadHandler(
|
|
661
986
|
configOrPlugin?: Partial<RagConfig> | VectorPlugin,
|
|
@@ -668,6 +993,11 @@ export function createUploadHandler(
|
|
|
668
993
|
const authResult = await checkAuth(req, onAuthorize);
|
|
669
994
|
if (authResult) return authResult as any;
|
|
670
995
|
|
|
996
|
+
// License enforcement (uploads use headers/env for license key, no JSON body)
|
|
997
|
+
const config = plugin.getConfig();
|
|
998
|
+
const licenseCheck = enforceLicense(req as NextRequest, config);
|
|
999
|
+
if (!licenseCheck.ok) return licenseCheck.response as any;
|
|
1000
|
+
|
|
671
1001
|
try {
|
|
672
1002
|
const formData = await req.formData();
|
|
673
1003
|
const files = formData.getAll('files') as File[];
|
|
@@ -679,6 +1009,28 @@ export function createUploadHandler(
|
|
|
679
1009
|
return NextResponse.json({ error: 'No files provided' }, { status: 400 });
|
|
680
1010
|
}
|
|
681
1011
|
|
|
1012
|
+
// Free Tier upload limits check (before parsing to fail fast on large uploads)
|
|
1013
|
+
if (isFreeTier(licenseCheck.payload.tier)) {
|
|
1014
|
+
const totalUploadBytes = files.reduce((sum, f) => sum + (f.size || 0), 0);
|
|
1015
|
+
const uploadCheck = (FreeTierLimitsGuard as any).checkIngestionAllowed(
|
|
1016
|
+
{ documentCount: files.length },
|
|
1017
|
+
totalUploadBytes
|
|
1018
|
+
);
|
|
1019
|
+
if (!uploadCheck.allowed) {
|
|
1020
|
+
return createPaymentRequiredResponse(
|
|
1021
|
+
uploadCheck.reason || 'Free tier upload limit reached.',
|
|
1022
|
+
{
|
|
1023
|
+
tier: licenseCheck.payload.tier,
|
|
1024
|
+
projectId: licenseCheck.payload.projectId,
|
|
1025
|
+
fileCount: files.length,
|
|
1026
|
+
totalUploadBytes,
|
|
1027
|
+
maxFileSizeBytes: FREE_TIER_QUOTAS.MAX_FILE_SIZE_BYTES,
|
|
1028
|
+
maxStorageBytes: FREE_TIER_QUOTAS.MAX_STORAGE_BYTES,
|
|
1029
|
+
}
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
|
|
682
1034
|
const documents: { docId: string; content: string; metadata?: Record<string, unknown> }[] = [];
|
|
683
1035
|
|
|
684
1036
|
for (const file of files) {
|
|
@@ -803,8 +1155,21 @@ export function createUploadHandler(
|
|
|
803
1155
|
}
|
|
804
1156
|
|
|
805
1157
|
/**
|
|
806
|
-
*
|
|
807
|
-
*
|
|
1158
|
+
* Build a GET/POST handler for auto-suggestions (used by ChatWidget prompt chips).
|
|
1159
|
+
*
|
|
1160
|
+
* Requests:
|
|
1161
|
+
* - GET `?query=...&namespace=...`
|
|
1162
|
+
* - POST `{ query: string, namespace?: string, licenseKey?: string }`
|
|
1163
|
+
*
|
|
1164
|
+
* Response (200 OK): `{ suggestions: string[] }`
|
|
1165
|
+
*
|
|
1166
|
+
* License enforcement: Runs BEFORE body parse on headers/env, and if the POST
|
|
1167
|
+
* body contains a `licenseKey` override the check is repeated with the body
|
|
1168
|
+
* key included so the per-request override is honored.
|
|
1169
|
+
*
|
|
1170
|
+
* @param configOrPlugin - Partial `RagConfig` or existing `VectorPlugin`.
|
|
1171
|
+
* @param options - Optional `onAuthorize` gate.
|
|
1172
|
+
* @returns Async handler accepting both GET and POST methods.
|
|
808
1173
|
*/
|
|
809
1174
|
export function createSuggestionsHandler(
|
|
810
1175
|
configOrPlugin?: Partial<RagConfig> | VectorPlugin,
|
|
@@ -817,14 +1182,26 @@ export function createSuggestionsHandler(
|
|
|
817
1182
|
const authResult = await checkAuth(req, onAuthorize);
|
|
818
1183
|
if (authResult) return authResult as any;
|
|
819
1184
|
|
|
1185
|
+
// License enforcement for auto-suggestions endpoint
|
|
1186
|
+
const config = plugin.getConfig();
|
|
1187
|
+
const licenseCheck = enforceLicense(req, config);
|
|
1188
|
+
if (!licenseCheck.ok) return licenseCheck.response as any;
|
|
1189
|
+
|
|
820
1190
|
try {
|
|
821
1191
|
let query = '';
|
|
822
1192
|
let namespace: string | undefined = undefined;
|
|
1193
|
+
let bodyLicenseKey: string | undefined = undefined;
|
|
823
1194
|
|
|
824
1195
|
if (req.method === 'POST') {
|
|
825
1196
|
const body = await req.json().catch(() => ({}));
|
|
826
1197
|
query = body.query || '';
|
|
827
1198
|
namespace = body.namespace;
|
|
1199
|
+
bodyLicenseKey = body.licenseKey;
|
|
1200
|
+
// Re-check license if body contained a key override
|
|
1201
|
+
if (bodyLicenseKey) {
|
|
1202
|
+
const recheck = enforceLicense(req, config, bodyLicenseKey);
|
|
1203
|
+
if (!recheck.ok) return recheck.response as any;
|
|
1204
|
+
}
|
|
828
1205
|
} else {
|
|
829
1206
|
const url = new URL(req.url);
|
|
830
1207
|
query = url.searchParams.get('query') || '';
|
|
@@ -847,7 +1224,19 @@ export function createSuggestionsHandler(
|
|
|
847
1224
|
}
|
|
848
1225
|
|
|
849
1226
|
/**
|
|
850
|
-
*
|
|
1227
|
+
* Build a handler for chat-history listing and clearing.
|
|
1228
|
+
*
|
|
1229
|
+
* Routes dispatched by `createRagHandler`:
|
|
1230
|
+
* - GET `/history?sessionId=...&limit=...` → returns `{ messages: HistoryMessage[] }`
|
|
1231
|
+
* - POST `/history/clear` with `{ sessionId? }` JSON body → returns `{ ok: true }`
|
|
1232
|
+
*
|
|
1233
|
+
* Storage: Uses `DatabaseStorage` (default SQLite or custom configured driver).
|
|
1234
|
+
* License enforcement is handled by the upstream `createRagHandler` router when
|
|
1235
|
+
* this handler is composed through it; standalone usage is rare.
|
|
1236
|
+
*
|
|
1237
|
+
* @param configOrPlugin - Partial `RagConfig` or existing `VectorPlugin`.
|
|
1238
|
+
* @param options - Optional `onAuthorize` gate.
|
|
1239
|
+
* @returns Async handler accepting GET (list) or POST (clear) methods.
|
|
851
1240
|
*/
|
|
852
1241
|
export function createHistoryHandler(
|
|
853
1242
|
configOrPlugin?: Partial<RagConfig> | VectorPlugin,
|
|
@@ -856,17 +1245,20 @@ export function createHistoryHandler(
|
|
|
856
1245
|
const plugin = getOrCreatePlugin(configOrPlugin);
|
|
857
1246
|
const storage = new DatabaseStorage(plugin.getConfig());
|
|
858
1247
|
const onAuthorize = options?.onAuthorize;
|
|
1248
|
+
const onResolveScope = options?.onResolveScope;
|
|
859
1249
|
|
|
860
1250
|
return async function handler(req: NextRequest) {
|
|
861
1251
|
const authResult = await checkAuth(req, onAuthorize);
|
|
862
1252
|
if (authResult) return authResult as any;
|
|
863
1253
|
|
|
1254
|
+
const scope = onResolveScope ? await onResolveScope(req) : undefined;
|
|
1255
|
+
|
|
864
1256
|
const url = new URL(req.url);
|
|
865
1257
|
const sessionId = url.searchParams.get('sessionId') || 'default';
|
|
866
1258
|
|
|
867
1259
|
if (req.method === 'GET') {
|
|
868
1260
|
try {
|
|
869
|
-
const history = await storage.getHistory(sessionId);
|
|
1261
|
+
const history = await storage.getHistory(sessionId, scope);
|
|
870
1262
|
return NextResponse.json({ history });
|
|
871
1263
|
} catch (err) {
|
|
872
1264
|
const message = err instanceof Error ? err.message : 'Failed to fetch history';
|
|
@@ -876,7 +1268,7 @@ export function createHistoryHandler(
|
|
|
876
1268
|
try {
|
|
877
1269
|
const body = await req.json().catch(() => ({}));
|
|
878
1270
|
const sid = body.sessionId || sessionId;
|
|
879
|
-
await storage.clearHistory(sid);
|
|
1271
|
+
await storage.clearHistory(sid, scope);
|
|
880
1272
|
return NextResponse.json({ success: true });
|
|
881
1273
|
} catch (err) {
|
|
882
1274
|
const message = err instanceof Error ? err.message : 'Failed to clear history';
|
|
@@ -888,7 +1280,17 @@ export function createHistoryHandler(
|
|
|
888
1280
|
}
|
|
889
1281
|
|
|
890
1282
|
/**
|
|
891
|
-
*
|
|
1283
|
+
* Build a POST handler for collecting thumbs-up / thumbs-down user feedback.
|
|
1284
|
+
*
|
|
1285
|
+
* Request: `{ messageId, sessionId?, rating: 1 | -1, comment?, reason? }`
|
|
1286
|
+
* Response: `{ ok: true, id: string }` (201 Created)
|
|
1287
|
+
*
|
|
1288
|
+
* Feedback is written to `DatabaseStorage.feedback` for RAG pipeline
|
|
1289
|
+
* evaluation, offline fine-tuning dataset curation, and support triage.
|
|
1290
|
+
*
|
|
1291
|
+
* @param configOrPlugin - Partial `RagConfig` or existing `VectorPlugin`.
|
|
1292
|
+
* @param options - Optional `onAuthorize` gate.
|
|
1293
|
+
* @returns Async POST handler.
|
|
892
1294
|
*/
|
|
893
1295
|
export function createFeedbackHandler(
|
|
894
1296
|
configOrPlugin?: Partial<RagConfig> | VectorPlugin,
|
|
@@ -897,20 +1299,23 @@ export function createFeedbackHandler(
|
|
|
897
1299
|
const plugin = getOrCreatePlugin(configOrPlugin);
|
|
898
1300
|
const storage = new DatabaseStorage(plugin.getConfig());
|
|
899
1301
|
const onAuthorize = options?.onAuthorize;
|
|
1302
|
+
const onResolveScope = options?.onResolveScope;
|
|
900
1303
|
|
|
901
1304
|
return async function handler(req: NextRequest) {
|
|
902
1305
|
const authResult = await checkAuth(req, onAuthorize);
|
|
903
1306
|
if (authResult) return authResult as any;
|
|
904
1307
|
|
|
1308
|
+
const scope = onResolveScope ? await onResolveScope(req) : undefined;
|
|
1309
|
+
|
|
905
1310
|
if (req.method === 'GET') {
|
|
906
1311
|
try {
|
|
907
1312
|
const url = new URL(req.url);
|
|
908
1313
|
const messageId = url.searchParams.get('messageId');
|
|
909
1314
|
if (!messageId) {
|
|
910
|
-
const feedbackList = await storage.listFeedback();
|
|
1315
|
+
const feedbackList = await storage.listFeedback(scope);
|
|
911
1316
|
return NextResponse.json({ feedback: feedbackList });
|
|
912
1317
|
}
|
|
913
|
-
const feedback = await storage.getFeedback(messageId);
|
|
1318
|
+
const feedback = await storage.getFeedback(messageId, scope);
|
|
914
1319
|
return NextResponse.json({ feedback });
|
|
915
1320
|
} catch (err) {
|
|
916
1321
|
const message = err instanceof Error ? err.message : 'Failed to fetch feedback';
|
|
@@ -932,6 +1337,17 @@ export function createFeedbackHandler(
|
|
|
932
1337
|
if (!rating) {
|
|
933
1338
|
return NextResponse.json({ error: 'rating is required' }, { status: 400 });
|
|
934
1339
|
}
|
|
1340
|
+
if (scope && scope.length > 0) {
|
|
1341
|
+
const matches = scope.some(
|
|
1342
|
+
(pid) => pid && (sessionId === pid || sessionId.startsWith(pid) || sessionId.includes(pid))
|
|
1343
|
+
);
|
|
1344
|
+
if (!matches) {
|
|
1345
|
+
return NextResponse.json(
|
|
1346
|
+
{ error: 'sessionId is outside the authorized project scope' },
|
|
1347
|
+
{ status: 403 }
|
|
1348
|
+
);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
935
1351
|
|
|
936
1352
|
await storage.saveFeedback({ messageId, sessionId, rating, comment });
|
|
937
1353
|
return NextResponse.json({ success: true });
|
|
@@ -945,7 +1361,17 @@ export function createFeedbackHandler(
|
|
|
945
1361
|
}
|
|
946
1362
|
|
|
947
1363
|
/**
|
|
948
|
-
*
|
|
1364
|
+
* Build a GET handler that lists the set of chat sessions.
|
|
1365
|
+
*
|
|
1366
|
+
* Response: `{ sessions: { id, title, lastMessageAt, preview }[] }`
|
|
1367
|
+
*
|
|
1368
|
+
* Sessions are read from `DatabaseStorage`. Combined with `createHistoryHandler`
|
|
1369
|
+
* this enables a sidebar of prior conversations for custom UIs built on top of
|
|
1370
|
+
* the `useRagChat` headless hook.
|
|
1371
|
+
*
|
|
1372
|
+
* @param configOrPlugin - Partial `RagConfig` or existing `VectorPlugin`.
|
|
1373
|
+
* @param options - Optional `onAuthorize` gate.
|
|
1374
|
+
* @returns Async GET handler returning a session listing.
|
|
949
1375
|
*/
|
|
950
1376
|
export function createSessionsHandler(
|
|
951
1377
|
configOrPlugin?: Partial<RagConfig> | VectorPlugin,
|
|
@@ -954,15 +1380,16 @@ export function createSessionsHandler(
|
|
|
954
1380
|
const plugin = getOrCreatePlugin(configOrPlugin);
|
|
955
1381
|
const storage = new DatabaseStorage(plugin.getConfig());
|
|
956
1382
|
const onAuthorize = options?.onAuthorize;
|
|
1383
|
+
const onResolveScope = options?.onResolveScope;
|
|
957
1384
|
|
|
958
1385
|
return async function GET(req?: NextRequest) {
|
|
959
1386
|
if (req) {
|
|
960
1387
|
const authResult = await checkAuth(req, onAuthorize);
|
|
961
1388
|
if (authResult) return authResult as any;
|
|
962
1389
|
}
|
|
963
|
-
void req;
|
|
964
1390
|
try {
|
|
965
|
-
const
|
|
1391
|
+
const scope = req && onResolveScope ? await onResolveScope(req) : undefined;
|
|
1392
|
+
const sessions = await storage.listSessions(scope);
|
|
966
1393
|
return NextResponse.json({ sessions });
|
|
967
1394
|
} catch (err) {
|
|
968
1395
|
const message = err instanceof Error ? err.message : 'Failed to list sessions';
|
|
@@ -972,8 +1399,36 @@ export function createSessionsHandler(
|
|
|
972
1399
|
}
|
|
973
1400
|
|
|
974
1401
|
/**
|
|
975
|
-
*
|
|
976
|
-
*
|
|
1402
|
+
* Build a Next.js catch-all route that dispatches to every sub-handler by path.
|
|
1403
|
+
*
|
|
1404
|
+
* Intended drop-in export:
|
|
1405
|
+
* ```ts
|
|
1406
|
+
* // app/api/retrivora/[[...retrivora]]/route.ts
|
|
1407
|
+
* import { createRagHandler } from '@retrivora-ai/rag-engine';
|
|
1408
|
+
* const { GET, POST } = createRagHandler({ /* RagConfig *\/ });
|
|
1409
|
+
* export { GET, POST };
|
|
1410
|
+
* ```
|
|
1411
|
+
*
|
|
1412
|
+
* Segments dispatched (POST unless marked GET):
|
|
1413
|
+
* - `chat` → createStreamHandler (SSE streaming chat)
|
|
1414
|
+
* - `upload` → createUploadHandler (file ingestion)
|
|
1415
|
+
* - `suggestions` → createSuggestionsHandler (GET/POST)
|
|
1416
|
+
* - `v1/license/validate` → createLicenseHandler
|
|
1417
|
+
* - `health` → createHealthHandler (GET)
|
|
1418
|
+
* - `feedback` → createFeedbackHandler
|
|
1419
|
+
* - `history` → createHistoryHandler (GET/POST)
|
|
1420
|
+
* - `sessions` → createSessionsHandler (GET)
|
|
1421
|
+
* - Any other segment → 404 with a JSON error body
|
|
1422
|
+
*
|
|
1423
|
+
* This is the **recommended** way to mount the SDK in a Next.js app. Using
|
|
1424
|
+
* any single handler individually without its license enforcement gate is a
|
|
1425
|
+
* security anti-pattern — prefer `createRagHandler` unless you have a very
|
|
1426
|
+
* specific reason to split routes up.
|
|
1427
|
+
*
|
|
1428
|
+
* @param configOrPlugin - Partial `RagConfig` or existing `VectorPlugin`.
|
|
1429
|
+
* @param options - Optional `onAuthorize` gate (propagated to every
|
|
1430
|
+
* sub-handler so auth rules apply uniformly).
|
|
1431
|
+
* @returns An object `{ GET, POST }` to re-export as your route file defaults.
|
|
977
1432
|
*/
|
|
978
1433
|
export function createRagHandler(
|
|
979
1434
|
configOrPlugin?: Partial<RagConfig> | VectorPlugin,
|
|
@@ -981,16 +1436,18 @@ export function createRagHandler(
|
|
|
981
1436
|
) {
|
|
982
1437
|
const plugin = getOrCreatePlugin(configOrPlugin);
|
|
983
1438
|
const onAuthorize = options?.onAuthorize;
|
|
984
|
-
|
|
985
|
-
const
|
|
986
|
-
|
|
987
|
-
const
|
|
988
|
-
const
|
|
989
|
-
const
|
|
990
|
-
const
|
|
991
|
-
const
|
|
992
|
-
const
|
|
993
|
-
const
|
|
1439
|
+
const onResolveScope = options?.onResolveScope;
|
|
1440
|
+
const scopeableOptions: HandlerOptions = { onAuthorize, onResolveScope };
|
|
1441
|
+
|
|
1442
|
+
const chatHandler = createChatHandler(plugin, scopeableOptions);
|
|
1443
|
+
const streamHandler = createStreamHandler(plugin, scopeableOptions);
|
|
1444
|
+
const uploadHandler = createUploadHandler(plugin, scopeableOptions);
|
|
1445
|
+
const healthHandler = createHealthHandler(plugin, scopeableOptions);
|
|
1446
|
+
const suggestionsHandler = createSuggestionsHandler(plugin, scopeableOptions);
|
|
1447
|
+
const historyHandler = createHistoryHandler(plugin, scopeableOptions);
|
|
1448
|
+
const feedbackHandler = createFeedbackHandler(plugin, scopeableOptions);
|
|
1449
|
+
const sessionsHandler = createSessionsHandler(plugin, scopeableOptions);
|
|
1450
|
+
const licenseHandler = createLicenseHandler(plugin, scopeableOptions);
|
|
994
1451
|
|
|
995
1452
|
async function routePostRequest(req: any, segment: string) {
|
|
996
1453
|
switch (segment) {
|