@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.
Files changed (41) hide show
  1. package/dist/{LicenseValidator-CENvo9o2.d.mts → BatchProcessor-7yV-UCHW.d.mts} +142 -3
  2. package/dist/{LicenseValidator-CsjJp2PP.d.ts → BatchProcessor-BfzuU4cK.d.ts} +142 -3
  3. package/dist/handlers/index.d.mts +1 -1
  4. package/dist/handlers/index.d.ts +1 -1
  5. package/dist/handlers/index.js +881 -331
  6. package/dist/handlers/index.mjs +881 -331
  7. package/dist/index-DR_O_B-W.d.ts +394 -0
  8. package/dist/index-fnpaCuma.d.mts +394 -0
  9. package/dist/index.css +58 -0
  10. package/dist/index.d.mts +63 -3
  11. package/dist/index.d.ts +63 -3
  12. package/dist/index.js +454 -36
  13. package/dist/index.mjs +453 -38
  14. package/dist/server.d.mts +35 -73
  15. package/dist/server.d.ts +35 -73
  16. package/dist/server.js +911 -345
  17. package/dist/server.mjs +911 -345
  18. package/package.json +3 -2
  19. package/src/components/ChatWidget.tsx +149 -48
  20. package/src/components/ChatWindow.tsx +52 -9
  21. package/src/components/CodeViewer.tsx +1 -1
  22. package/src/core/BatchProcessor.ts +42 -4
  23. package/src/core/CircuitBreaker.ts +118 -0
  24. package/src/core/DatabaseStorage.ts +55 -24
  25. package/src/core/FreeTierLimitsGuard.ts +281 -0
  26. package/src/core/LangChainAgent.ts +0 -2
  27. package/src/core/LicenseValidator.ts +66 -2
  28. package/src/core/LicenseVerifier.ts +31 -3
  29. package/src/core/VectorPlugin.ts +30 -0
  30. package/src/handlers/index.ts +491 -36
  31. package/src/index.css +58 -0
  32. package/src/index.ts +4 -0
  33. package/src/providers/vectordb/MilvusProvider.ts +0 -1
  34. package/src/providers/vectordb/MongoDBProvider.ts +0 -1
  35. package/src/providers/vectordb/MultiTablePostgresProvider.ts +2 -2
  36. package/src/providers/vectordb/RedisProvider.ts +0 -1
  37. package/src/providers/vectordb/WeaviateProvider.ts +0 -1
  38. package/src/rag/LlamaIndexIngestor.ts +0 -1
  39. package/src/server.ts +1 -0
  40. package/dist/index-BPJ3KDYI.d.ts +0 -195
  41. package/dist/index-Dmq5lH0j.d.mts +0 -195
@@ -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
- onAuthorize?: (req: NextRequest) => boolean | Promise<boolean> | Response | Promise<Response>;
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> | Response | Promise<Response>
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
- * createChatHandler — factory that returns a Next.js App Router POST handler.
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
- // 2. Input sanitization
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
- * createStreamHandler — factory for a streaming SSE Next.js App Router POST handler.
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
- // 2. Input sanitization
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
- * createIngestHandler factory for the document ingestion endpoint.
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
- * createHealthHandler factory for the health-check endpoint.
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
- * createLicenseHandler factory for the license validation endpoint (/v1/license/validate).
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,
@@ -657,7 +966,21 @@ export function createLicenseHandler(
657
966
  }
658
967
 
659
968
  /**
660
- * createUploadHandler factory for the file upload ingestion endpoint.
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.
661
984
  */
662
985
  export function createUploadHandler(
663
986
  configOrPlugin?: Partial<RagConfig> | VectorPlugin,
@@ -670,6 +993,11 @@ export function createUploadHandler(
670
993
  const authResult = await checkAuth(req, onAuthorize);
671
994
  if (authResult) return authResult as any;
672
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
+
673
1001
  try {
674
1002
  const formData = await req.formData();
675
1003
  const files = formData.getAll('files') as File[];
@@ -681,6 +1009,28 @@ export function createUploadHandler(
681
1009
  return NextResponse.json({ error: 'No files provided' }, { status: 400 });
682
1010
  }
683
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
+
684
1034
  const documents: { docId: string; content: string; metadata?: Record<string, unknown> }[] = [];
685
1035
 
686
1036
  for (const file of files) {
@@ -805,8 +1155,21 @@ export function createUploadHandler(
805
1155
  }
806
1156
 
807
1157
  /**
808
- * createSuggestionsHandler factory for the auto-suggestions endpoint.
809
- * Supports both POST (JSON body) and GET (URL search parameters).
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.
810
1173
  */
811
1174
  export function createSuggestionsHandler(
812
1175
  configOrPlugin?: Partial<RagConfig> | VectorPlugin,
@@ -819,14 +1182,26 @@ export function createSuggestionsHandler(
819
1182
  const authResult = await checkAuth(req, onAuthorize);
820
1183
  if (authResult) return authResult as any;
821
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
+
822
1190
  try {
823
1191
  let query = '';
824
1192
  let namespace: string | undefined = undefined;
1193
+ let bodyLicenseKey: string | undefined = undefined;
825
1194
 
826
1195
  if (req.method === 'POST') {
827
1196
  const body = await req.json().catch(() => ({}));
828
1197
  query = body.query || '';
829
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
+ }
830
1205
  } else {
831
1206
  const url = new URL(req.url);
832
1207
  query = url.searchParams.get('query') || '';
@@ -849,7 +1224,19 @@ export function createSuggestionsHandler(
849
1224
  }
850
1225
 
851
1226
  /**
852
- * createHistoryHandler factory for GET /api/retrivora/history and POST /api/retrivora/history/clear.
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.
853
1240
  */
854
1241
  export function createHistoryHandler(
855
1242
  configOrPlugin?: Partial<RagConfig> | VectorPlugin,
@@ -858,17 +1245,20 @@ export function createHistoryHandler(
858
1245
  const plugin = getOrCreatePlugin(configOrPlugin);
859
1246
  const storage = new DatabaseStorage(plugin.getConfig());
860
1247
  const onAuthorize = options?.onAuthorize;
1248
+ const onResolveScope = options?.onResolveScope;
861
1249
 
862
1250
  return async function handler(req: NextRequest) {
863
1251
  const authResult = await checkAuth(req, onAuthorize);
864
1252
  if (authResult) return authResult as any;
865
1253
 
1254
+ const scope = onResolveScope ? await onResolveScope(req) : undefined;
1255
+
866
1256
  const url = new URL(req.url);
867
1257
  const sessionId = url.searchParams.get('sessionId') || 'default';
868
1258
 
869
1259
  if (req.method === 'GET') {
870
1260
  try {
871
- const history = await storage.getHistory(sessionId);
1261
+ const history = await storage.getHistory(sessionId, scope);
872
1262
  return NextResponse.json({ history });
873
1263
  } catch (err) {
874
1264
  const message = err instanceof Error ? err.message : 'Failed to fetch history';
@@ -878,7 +1268,7 @@ export function createHistoryHandler(
878
1268
  try {
879
1269
  const body = await req.json().catch(() => ({}));
880
1270
  const sid = body.sessionId || sessionId;
881
- await storage.clearHistory(sid);
1271
+ await storage.clearHistory(sid, scope);
882
1272
  return NextResponse.json({ success: true });
883
1273
  } catch (err) {
884
1274
  const message = err instanceof Error ? err.message : 'Failed to clear history';
@@ -890,7 +1280,17 @@ export function createHistoryHandler(
890
1280
  }
891
1281
 
892
1282
  /**
893
- * createFeedbackHandler factory for POST /api/retrivora/feedback.
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.
894
1294
  */
895
1295
  export function createFeedbackHandler(
896
1296
  configOrPlugin?: Partial<RagConfig> | VectorPlugin,
@@ -899,20 +1299,23 @@ export function createFeedbackHandler(
899
1299
  const plugin = getOrCreatePlugin(configOrPlugin);
900
1300
  const storage = new DatabaseStorage(plugin.getConfig());
901
1301
  const onAuthorize = options?.onAuthorize;
1302
+ const onResolveScope = options?.onResolveScope;
902
1303
 
903
1304
  return async function handler(req: NextRequest) {
904
1305
  const authResult = await checkAuth(req, onAuthorize);
905
1306
  if (authResult) return authResult as any;
906
1307
 
1308
+ const scope = onResolveScope ? await onResolveScope(req) : undefined;
1309
+
907
1310
  if (req.method === 'GET') {
908
1311
  try {
909
1312
  const url = new URL(req.url);
910
1313
  const messageId = url.searchParams.get('messageId');
911
1314
  if (!messageId) {
912
- const feedbackList = await storage.listFeedback();
1315
+ const feedbackList = await storage.listFeedback(scope);
913
1316
  return NextResponse.json({ feedback: feedbackList });
914
1317
  }
915
- const feedback = await storage.getFeedback(messageId);
1318
+ const feedback = await storage.getFeedback(messageId, scope);
916
1319
  return NextResponse.json({ feedback });
917
1320
  } catch (err) {
918
1321
  const message = err instanceof Error ? err.message : 'Failed to fetch feedback';
@@ -934,6 +1337,17 @@ export function createFeedbackHandler(
934
1337
  if (!rating) {
935
1338
  return NextResponse.json({ error: 'rating is required' }, { status: 400 });
936
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
+ }
937
1351
 
938
1352
  await storage.saveFeedback({ messageId, sessionId, rating, comment });
939
1353
  return NextResponse.json({ success: true });
@@ -947,7 +1361,17 @@ export function createFeedbackHandler(
947
1361
  }
948
1362
 
949
1363
  /**
950
- * createSessionsHandler factory for GET /api/retrivora/sessions.
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.
951
1375
  */
952
1376
  export function createSessionsHandler(
953
1377
  configOrPlugin?: Partial<RagConfig> | VectorPlugin,
@@ -956,15 +1380,16 @@ export function createSessionsHandler(
956
1380
  const plugin = getOrCreatePlugin(configOrPlugin);
957
1381
  const storage = new DatabaseStorage(plugin.getConfig());
958
1382
  const onAuthorize = options?.onAuthorize;
1383
+ const onResolveScope = options?.onResolveScope;
959
1384
 
960
1385
  return async function GET(req?: NextRequest) {
961
1386
  if (req) {
962
1387
  const authResult = await checkAuth(req, onAuthorize);
963
1388
  if (authResult) return authResult as any;
964
1389
  }
965
- void req;
966
1390
  try {
967
- const sessions = await storage.listSessions();
1391
+ const scope = req && onResolveScope ? await onResolveScope(req) : undefined;
1392
+ const sessions = await storage.listSessions(scope);
968
1393
  return NextResponse.json({ sessions });
969
1394
  } catch (err) {
970
1395
  const message = err instanceof Error ? err.message : 'Failed to list sessions';
@@ -974,8 +1399,36 @@ export function createSessionsHandler(
974
1399
  }
975
1400
 
976
1401
  /**
977
- * createRagHandler factory that returns GET and POST handlers for a catch-all route
978
- * (e.g. `src/app/api/retrivora/[[...retrivora]]/route.ts`).
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.
979
1432
  */
980
1433
  export function createRagHandler(
981
1434
  configOrPlugin?: Partial<RagConfig> | VectorPlugin,
@@ -983,16 +1436,18 @@ export function createRagHandler(
983
1436
  ) {
984
1437
  const plugin = getOrCreatePlugin(configOrPlugin);
985
1438
  const onAuthorize = options?.onAuthorize;
986
-
987
- const chatHandler = createChatHandler(plugin, { onAuthorize });
988
- const streamHandler = createStreamHandler(plugin, { onAuthorize });
989
- const uploadHandler = createUploadHandler(plugin, { onAuthorize });
990
- const healthHandler = createHealthHandler(plugin, { onAuthorize });
991
- const suggestionsHandler = createSuggestionsHandler(plugin, { onAuthorize });
992
- const historyHandler = createHistoryHandler(plugin, { onAuthorize });
993
- const feedbackHandler = createFeedbackHandler(plugin, { onAuthorize });
994
- const sessionsHandler = createSessionsHandler(plugin, { onAuthorize });
995
- const licenseHandler = createLicenseHandler(plugin, { onAuthorize });
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);
996
1451
 
997
1452
  async function routePostRequest(req: any, segment: string) {
998
1453
  switch (segment) {