@retrivora-ai/rag-engine 2.2.6 → 2.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,6 +4,7 @@ import { ChatWindow } from './ChatWindow';
4
4
  import { useConfig } from './ConfigProvider';
5
5
  import { ChatWidgetProps } from '../types';
6
6
  import { SDK_VERSION } from '../version';
7
+ import { LicenseValidator } from '../core/LicenseValidator';
7
8
 
8
9
  const DEFAULT_DIMENSIONS = { width: 380, height: 600 };
9
10
 
@@ -45,8 +46,8 @@ const GEMINI_STYLES = `
45
46
  }
46
47
  `;
47
48
 
48
- export function ChatWidget({ position = 'bottom-right', onAddToCart, apiUrl, retrivoraApiBase, headers }: ChatWidgetProps) {
49
- const { ui } = useConfig();
49
+ export function ChatWidget({ position = 'bottom-right', onAddToCart, apiUrl, retrivoraApiBase, licenseKey: licenseKeyProp, headers }: ChatWidgetProps) {
50
+ const { ui, projectId } = useConfig();
50
51
  const [isOpen, setIsOpen] = useState(false);
51
52
  const [hasUnread, setHasUnread] = useState(false);
52
53
  const [dimensions, setDimensions] = useState(DEFAULT_DIMENSIONS);
@@ -71,44 +72,37 @@ export function ChatWidget({ position = 'bottom-right', onAddToCart, apiUrl, ret
71
72
  const handleOpen = async () => {
72
73
  setIsValidating(true);
73
74
  try {
74
- const base = retrivoraApiBase || '/api/retrivora';
75
- const configUrl = `${base.replace(/\/$/, '')}/config?sdkVersion=${SDK_VERSION}`;
76
- const res = await fetch(configUrl, {
77
- method: 'GET',
78
- cache: 'no-store',
79
- headers: {
80
- 'Content-Type': 'application/json',
81
- 'x-sdk-version': SDK_VERSION,
82
- ...(headers || {})
83
- },
84
- });
85
-
86
- if (!res.ok) {
87
- const data = await res.json().catch(() => ({}));
88
- if (res.status === 426 || data.code === 'SDK_VERSION_OUTDATED') {
89
- const msg = data.error || 'Your Retrivora SDK package version is outdated. Please update your package to the latest version to continue.';
90
- setWidgetError(msg);
91
- setIsOpen(false); // DO NOT OPEN CHAT WIDGET
92
- return;
75
+ const getHeader = (name: string) => {
76
+ if (!headers) return undefined;
77
+ const target = name.toLowerCase();
78
+ for (const [k, v] of Object.entries(headers)) {
79
+ if (k.toLowerCase() === target) return v;
93
80
  }
94
- if (
95
- res.status === 403 ||
96
- res.status === 401 ||
97
- (typeof data.error === 'string' && data.error.toLowerCase().includes('license'))
98
- ) {
99
- const msg = (typeof data.error === 'string' ? data.error : data.error?.message) || 'Your Retrivora license is no longer valid. Please contact your administrator or obtain a valid license key.';
100
- setWidgetError(msg);
101
- setIsOpen(false); // DO NOT OPEN CHAT WIDGET
102
- return;
103
- }
104
- }
81
+ return undefined;
82
+ };
83
+
84
+ const resolvedLicenseKey =
85
+ licenseKeyProp ||
86
+ getHeader('x-license-key') ||
87
+ getHeader('authorization')?.replace(/^Bearer\s+/i, '') ||
88
+ (typeof process !== 'undefined'
89
+ ? (process.env.NEXT_PUBLIC_RETRIVORA_LICENSE_KEY || process.env.RETRIVORA_LICENSE_KEY)
90
+ : '') ||
91
+ '';
92
+
93
+ await LicenseValidator.getInstance().validate({
94
+ licenseKey: resolvedLicenseKey,
95
+ projectId,
96
+ retrivoraApiBase,
97
+ headers,
98
+ });
105
99
 
106
100
  setWidgetError(null);
107
101
  setIsOpen(true);
108
102
  setHasUnread(false);
109
- } catch {
110
- // Fallback
111
- setIsOpen(true);
103
+ } catch (err: any) {
104
+ setWidgetError(err?.message || 'Access denied.');
105
+ setIsOpen(false); // DO NOT OPEN CHAT WIDGET
112
106
  } finally {
113
107
  setIsValidating(false);
114
108
  }
@@ -190,6 +184,7 @@ export function ChatWidget({ position = 'bottom-right', onAddToCart, apiUrl, ret
190
184
  onAddToCart={onAddToCart}
191
185
  apiUrl={apiUrl}
192
186
  retrivoraApiBase={retrivoraApiBase}
187
+ licenseKey={licenseKeyProp}
193
188
  headers={headers}
194
189
  />
195
190
  {/* Pointer (Tail) */}
@@ -23,6 +23,7 @@ import { useRagChat } from '../hooks/useRagChat';
23
23
  import { BORDER_RADIUS_MAP, CHAT_SUGGESTIONS } from '../config/uiConstants';
24
24
  import { ChatWindowProps } from '../types';
25
25
  import { SDK_VERSION } from '../version';
26
+ import { LicenseValidator } from '../core/LicenseValidator';
26
27
 
27
28
  interface SpeechRecognitionEvent {
28
29
  resultIndex: number;
@@ -67,6 +68,7 @@ export function ChatWindow({
67
68
  onAddToCart,
68
69
  apiUrl,
69
70
  retrivoraApiBase,
71
+ licenseKey: licenseKeyProp,
70
72
  headers
71
73
  }: ChatWindowProps) {
72
74
  const { ui, projectId } = useConfig();
@@ -80,6 +82,7 @@ export function ChatWindow({
80
82
  namespace: projectId,
81
83
  apiUrl,
82
84
  retrivoraApiBase,
85
+ licenseKey: licenseKeyProp,
83
86
  headers,
84
87
  });
85
88
 
@@ -97,48 +100,49 @@ export function ChatWindow({
97
100
  let isMounted = true;
98
101
  async function validateSessionLicense() {
99
102
  try {
100
- const base = retrivoraApiBase || '/api/retrivora';
101
- const configUrl = `${base.replace(/\/$/, '')}/config?sdkVersion=${SDK_VERSION}`;
102
- const res = await fetch(configUrl, {
103
- method: 'GET',
104
- cache: 'no-store',
105
- headers: {
106
- 'Content-Type': 'application/json',
107
- 'x-sdk-version': SDK_VERSION,
108
- ...(headers || {})
109
- },
103
+ const getHeader = (name: string) => {
104
+ if (!headers) return undefined;
105
+ const target = name.toLowerCase();
106
+ for (const [k, v] of Object.entries(headers)) {
107
+ if (k.toLowerCase() === target) return v;
108
+ }
109
+ return undefined;
110
+ };
111
+
112
+ const resolvedLicenseKey =
113
+ licenseKeyProp ||
114
+ getHeader('x-license-key') ||
115
+ getHeader('authorization')?.replace(/^Bearer\s+/i, '') ||
116
+ (typeof process !== 'undefined'
117
+ ? (process.env.NEXT_PUBLIC_RETRIVORA_LICENSE_KEY || process.env.RETRIVORA_LICENSE_KEY)
118
+ : '') ||
119
+ '';
120
+
121
+ await LicenseValidator.getInstance().validate({
122
+ licenseKey: resolvedLicenseKey,
123
+ projectId,
124
+ retrivoraApiBase,
125
+ headers,
110
126
  });
111
127
 
112
- if (!res.ok) {
113
- const data = await res.json().catch(() => ({}));
114
- if (res.status === 426 || data.code === 'SDK_VERSION_OUTDATED') {
115
- if (isMounted) {
116
- setVersionError(data.error || 'Your Retrivora SDK package is outdated. Please update your package to the latest version to continue.');
117
- }
118
- } else if (
119
- res.status === 403 ||
120
- res.status === 401 ||
121
- (typeof data.error === 'string' && data.error.toLowerCase().includes('license'))
122
- ) {
123
- if (isMounted) {
124
- const errMsg = typeof data.error === 'string' ? data.error : data.error?.message || 'Your Retrivora license is no longer valid. Please contact your administrator or obtain a valid license key.';
125
- setLicenseError(errMsg);
126
- }
127
- }
128
- } else {
129
- if (isMounted) {
130
- setLicenseError(null);
131
- setVersionError(null);
128
+ if (isMounted) {
129
+ setLicenseError(null);
130
+ setVersionError(null);
131
+ }
132
+ } catch (err: any) {
133
+ if (isMounted) {
134
+ if (err?.code === 'SDK_VERSION_UNSUPPORTED') {
135
+ setVersionError(err?.message || 'SDK version unsupported.');
136
+ } else {
137
+ setLicenseError(err?.message || 'License validation failed.');
132
138
  }
133
139
  }
134
- } catch {
135
- // Non-blocking fallback
136
140
  }
137
141
  }
138
142
 
139
143
  validateSessionLicense();
140
144
  return () => { isMounted = false; };
141
- }, [retrivoraApiBase, headers]);
145
+ }, [retrivoraApiBase, headers, projectId]);
142
146
 
143
147
  // Initialize SpeechRecognition
144
148
  useEffect(() => {
@@ -0,0 +1,198 @@
1
+ import { SDK_VERSION } from '../version';
2
+ import { SDKVersionUnsupportedError, LicenseValidationError } from '../exceptions';
3
+ import { LicenseVerifier } from './LicenseVerifier';
4
+
5
+ export interface LicenseValidationRequest {
6
+ licenseKey: string;
7
+ projectId?: string;
8
+ sdkVersion?: string;
9
+ sdk?: string;
10
+ platform?: string;
11
+ retrivoraApiBase?: string;
12
+ headers?: Record<string, string>;
13
+ }
14
+
15
+ export interface LicenseValidationResponse {
16
+ valid: boolean;
17
+ licenseStatus: 'ACTIVE' | 'SUSPENDED' | 'TERMINATED' | 'EXPIRED' | 'REVOKED';
18
+ minimumSupportedVersion: string;
19
+ latestVersion: string;
20
+ forceUpgrade: boolean;
21
+ expiresAt: string | null;
22
+ message: string;
23
+ }
24
+
25
+ interface CachedSuccessValidation {
26
+ response: LicenseValidationResponse;
27
+ timestamp: number;
28
+ }
29
+
30
+ export class LicenseValidator {
31
+ private static instance: LicenseValidator;
32
+ private cache: Map<string, CachedSuccessValidation> = new Map();
33
+ private static readonly SUCCESS_CACHE_TTL_MS = 5 * 60 * 1000; // 5-minute TTL for successful validation only
34
+
35
+ private constructor() {}
36
+
37
+ public static getInstance(): LicenseValidator {
38
+ if (!LicenseValidator.instance) {
39
+ LicenseValidator.instance = new LicenseValidator();
40
+ }
41
+ return LicenseValidator.instance;
42
+ }
43
+
44
+ /**
45
+ * Invalidate local validation cache for a license key
46
+ */
47
+ public purgeCache(licenseKey?: string): void {
48
+ if (licenseKey) {
49
+ this.cache.delete(licenseKey);
50
+ } else {
51
+ this.cache.clear();
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Validate license status and SDK version against Retrivora License Service
57
+ */
58
+ public async validate(request: LicenseValidationRequest): Promise<LicenseValidationResponse> {
59
+ const {
60
+ licenseKey,
61
+ projectId,
62
+ sdkVersion = SDK_VERSION,
63
+ sdk = '@retrivora-ai/rag-engine',
64
+ platform = typeof window !== 'undefined' ? 'browser' : 'node',
65
+ retrivoraApiBase,
66
+ headers: customHeaders,
67
+ } = request;
68
+
69
+ const getHeader = (name: string): string | undefined => {
70
+ if (!customHeaders) return undefined;
71
+ const target = name.toLowerCase();
72
+ for (const [k, v] of Object.entries(customHeaders)) {
73
+ if (k.toLowerCase() === target) return v;
74
+ }
75
+ return undefined;
76
+ };
77
+
78
+ const effectiveLicenseKey =
79
+ licenseKey ||
80
+ getHeader('x-license-key') ||
81
+ getHeader('authorization')?.replace(/^Bearer\s+/i, '') ||
82
+ (typeof process !== 'undefined'
83
+ ? (process.env.NEXT_PUBLIC_RETRIVORA_LICENSE_KEY || process.env.RETRIVORA_LICENSE_KEY)
84
+ : '') ||
85
+ '';
86
+
87
+ if (!effectiveLicenseKey) {
88
+ this.purgeCache(effectiveLicenseKey);
89
+ throw new LicenseValidationError('Missing RETRIVORA_LICENSE_KEY in request.');
90
+ }
91
+
92
+ // 1. Check successful 5-minute cache
93
+ const cached = this.cache.get(effectiveLicenseKey);
94
+ if (cached) {
95
+ const isFresh = Date.now() - cached.timestamp < LicenseValidator.SUCCESS_CACHE_TTL_MS;
96
+ if (isFresh && cached.response.valid && cached.response.licenseStatus === 'ACTIVE' && !cached.response.forceUpgrade) {
97
+ return cached.response;
98
+ } else {
99
+ this.cache.delete(effectiveLicenseKey);
100
+ }
101
+ }
102
+
103
+ // 2. Fetch live validation from Retrivora backend API
104
+ const base = retrivoraApiBase || (typeof process !== 'undefined' ? process.env.NEXT_PUBLIC_RETRIVORA_API_BASE || process.env.RETRIVORA_API_BASE : '') || '/api/retrivora';
105
+ const validateUrl = `${base.replace(/\/$/, '')}/v1/license/validate`;
106
+
107
+ let res: Response | null = null;
108
+ try {
109
+ res = await fetch(validateUrl, {
110
+ method: 'POST',
111
+ cache: 'no-store',
112
+ headers: {
113
+ 'Content-Type': 'application/json',
114
+ 'x-license-key': effectiveLicenseKey,
115
+ 'x-sdk-version': sdkVersion,
116
+ ...(customHeaders || {}),
117
+ },
118
+ body: JSON.stringify({
119
+ licenseKey,
120
+ projectId,
121
+ sdkVersion,
122
+ sdk,
123
+ platform,
124
+ }),
125
+ });
126
+ } catch (err: any) {
127
+ // Fallback: If network error occurs, verify locally via RSA LicenseVerifier
128
+ try {
129
+ const payload = LicenseVerifier.verify(licenseKey, projectId || 'my-rag-app');
130
+ const fallbackResp: LicenseValidationResponse = {
131
+ valid: true,
132
+ licenseStatus: 'ACTIVE',
133
+ minimumSupportedVersion: '2.1.0',
134
+ latestVersion: SDK_VERSION,
135
+ forceUpgrade: false,
136
+ expiresAt: payload.expiresAt ? new Date(payload.expiresAt * 1000).toISOString() : null,
137
+ message: 'Local license verification active.',
138
+ };
139
+ this.cache.set(licenseKey, { response: fallbackResp, timestamp: Date.now() });
140
+ return fallbackResp;
141
+ } catch {
142
+ this.purgeCache(licenseKey);
143
+ throw new LicenseValidationError(`License validation request failed: ${err?.message || 'Network error'}`);
144
+ }
145
+ }
146
+
147
+ if (res && !res.ok) {
148
+ // Attempt local verification fallback if API route returned 404 or non-OK response
149
+ try {
150
+ const payload = LicenseVerifier.verify(licenseKey, projectId || 'my-rag-app');
151
+ const fallbackResp: LicenseValidationResponse = {
152
+ valid: true,
153
+ licenseStatus: 'ACTIVE',
154
+ minimumSupportedVersion: '2.1.0',
155
+ latestVersion: SDK_VERSION,
156
+ forceUpgrade: false,
157
+ expiresAt: payload.expiresAt ? new Date(payload.expiresAt * 1000).toISOString() : null,
158
+ message: 'Local license verification active.',
159
+ };
160
+ this.cache.set(licenseKey, { response: fallbackResp, timestamp: Date.now() });
161
+ return fallbackResp;
162
+ } catch { /* proceed to parse error response */ }
163
+ }
164
+
165
+ const data: LicenseValidationResponse = await res!.json().catch(() => ({
166
+ valid: false,
167
+ licenseStatus: 'REVOKED' as const,
168
+ minimumSupportedVersion: '2.1.0',
169
+ latestVersion: SDK_VERSION,
170
+ forceUpgrade: false,
171
+ expiresAt: null,
172
+ message: 'Failed to parse license validation response.',
173
+ }));
174
+
175
+ // 3. Negative validation responses immediately purge any existing cache
176
+ if (!res.ok || !data.valid || data.licenseStatus !== 'ACTIVE' || data.forceUpgrade) {
177
+ this.purgeCache(licenseKey);
178
+
179
+ if (data.forceUpgrade || res.status === 426) {
180
+ throw new SDKVersionUnsupportedError(
181
+ data.message || `This SDK version (${sdkVersion}) is no longer supported. Upgrade to version ${data.latestVersion || '2.2.6'} or later.`,
182
+ data
183
+ );
184
+ }
185
+
186
+ const errMsg = data.message || `License validation failed with status: ${data.licenseStatus}. Access denied.`;
187
+ throw new LicenseValidationError(errMsg, data);
188
+ }
189
+
190
+ // 4. Cache SUCCESSFUL validation only (TTL = 5 minutes)
191
+ this.cache.set(licenseKey, {
192
+ response: data,
193
+ timestamp: Date.now(),
194
+ });
195
+
196
+ return data;
197
+ }
198
+ }
@@ -8,7 +8,9 @@ export type RetrivoraErrorCode =
8
8
  | 'RETRIEVAL_FAILED'
9
9
  | 'RATE_LIMITED'
10
10
  | 'CONFIGURATION_ERROR'
11
- | 'AUTHENTICATION_ERROR';
11
+ | 'AUTHENTICATION_ERROR'
12
+ | 'SDK_VERSION_UNSUPPORTED'
13
+ | 'LICENSE_VALIDATION_ERROR';
12
14
 
13
15
  export class RetrivoraError extends Error {
14
16
  readonly code: RetrivoraErrorCode;
@@ -58,6 +60,18 @@ export class AuthenticationException extends RetrivoraError {
58
60
  }
59
61
  }
60
62
 
63
+ export class SDKVersionUnsupportedError extends RetrivoraError {
64
+ constructor(message = 'This SDK version is no longer supported.', details?: unknown) {
65
+ super(message, 'SDK_VERSION_UNSUPPORTED', details);
66
+ }
67
+ }
68
+
69
+ export class LicenseValidationError extends RetrivoraError {
70
+ constructor(message = 'License validation failed.', details?: unknown) {
71
+ super(message, 'LICENSE_VALIDATION_ERROR', details);
72
+ }
73
+ }
74
+
61
75
  /**
62
76
  * Wraps any unknown error into an appropriate RetrivoraError subclass.
63
77
  */
@@ -80,14 +94,20 @@ export function wrapError(
80
94
  return new RateLimitException(message, err);
81
95
  }
82
96
 
83
- // 2. Authentication Recognition
97
+ // 2. Authentication / License Recognition
84
98
  if (
85
99
  status === 401 ||
86
100
  status === 403 ||
87
- /unauthorized|auth|api[- ]?key/i.test(message) ||
88
- code === 'INVALID_API_KEY'
101
+ /unauthorized|auth|license|revoked|terminated|api[- ]?key/i.test(message) ||
102
+ code === 'INVALID_API_KEY' ||
103
+ code === 'LICENSE_DELETED' ||
104
+ code === 'LICENSE_INACTIVE'
89
105
  ) {
90
- return new AuthenticationException(message, err);
106
+ const isLicenseError = /403|license|revoked|terminated/i.test(message) || status === 403;
107
+ const cleanMsg = isLicenseError
108
+ ? 'Your Retrivora license key has been terminated, suspended, or revoked. Access denied.'
109
+ : message;
110
+ return new AuthenticationException(cleanMsg, err);
91
111
  }
92
112
 
93
113
  // Fallback Mapping based on defaultCode
@@ -5,6 +5,8 @@ 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';
9
+ import { SDK_VERSION } from '../version';
8
10
 
9
11
  export interface HandlerOptions {
10
12
  onAuthorize?: (req: NextRequest) => boolean | Promise<boolean> | Response | Promise<Response>;
@@ -147,8 +149,6 @@ function getOrCreatePlugin(configOrPlugin: Partial<RagConfig> | VectorPlugin | u
147
149
 
148
150
  // ─── Telemetry Reporter Helper ───────────────────────────────────────────────
149
151
 
150
- import { SDK_VERSION } from '../version';
151
-
152
152
  /**
153
153
  * Asynchronously sends telemetry payload to telemetryUrl when telemetry is enabled
154
154
  * or when a licenseKey is set. Non-blocking.
@@ -492,8 +492,18 @@ export function createStreamHandler(
492
492
  }
493
493
  } catch (streamError) {
494
494
  if (isActive) {
495
- const errorMessage =
495
+ let errorMessage =
496
496
  streamError instanceof Error ? streamError.message : String(streamError);
497
+
498
+ if (
499
+ errorMessage.includes('403') ||
500
+ errorMessage.toLowerCase().includes('license') ||
501
+ errorMessage.toLowerCase().includes('revoked') ||
502
+ errorMessage.toLowerCase().includes('terminated')
503
+ ) {
504
+ errorMessage = 'Your Retrivora license key has been terminated, suspended, or revoked. Access denied.';
505
+ }
506
+
497
507
  console.error('[createStreamHandler] Stream error:', streamError);
498
508
  reportTelemetry(req, plugin, 'QUERY_GENERATION', 'error', errorMessage);
499
509
  try {
@@ -586,6 +596,64 @@ export function createHealthHandler(
586
596
  };
587
597
  }
588
598
 
599
+ /**
600
+ * createLicenseHandler — factory for the license validation endpoint (/v1/license/validate).
601
+ */
602
+ export function createLicenseHandler(
603
+ configOrPlugin?: Partial<RagConfig> | VectorPlugin,
604
+ options?: HandlerOptions
605
+ ) {
606
+ const plugin = getOrCreatePlugin(configOrPlugin);
607
+ const onAuthorize = options?.onAuthorize;
608
+
609
+ return async function POST(req: NextRequest) {
610
+ if (req) {
611
+ const authResult = await checkAuth(req, onAuthorize);
612
+ if (authResult) return authResult as any;
613
+ }
614
+
615
+ try {
616
+ const body = await req.json().catch(() => ({}));
617
+ const config = plugin.getConfig();
618
+ const licenseKey =
619
+ req.headers.get('x-license-key') ||
620
+ body?.licenseKey ||
621
+ req.headers.get('authorization')?.replace(/^Bearer\s+/i, '') ||
622
+ config.licenseKey ||
623
+ process.env.RETRIVORA_LICENSE_KEY ||
624
+ process.env.NEXT_PUBLIC_RETRIVORA_LICENSE_KEY ||
625
+ '';
626
+
627
+ const projectId = body?.projectId || config.projectId || 'my-rag-app';
628
+
629
+ const payload = LicenseVerifier.verify(licenseKey, projectId);
630
+
631
+ return NextResponse.json({
632
+ valid: true,
633
+ licenseStatus: 'ACTIVE',
634
+ minimumSupportedVersion: '2.1.0',
635
+ latestVersion: SDK_VERSION,
636
+ forceUpgrade: false,
637
+ expiresAt: payload.expiresAt ? new Date(payload.expiresAt * 1000).toISOString() : null,
638
+ message: 'License key is active and valid.',
639
+ });
640
+ } catch (err: any) {
641
+ return NextResponse.json(
642
+ {
643
+ valid: false,
644
+ licenseStatus: 'REVOKED',
645
+ minimumSupportedVersion: '2.1.0',
646
+ latestVersion: SDK_VERSION,
647
+ forceUpgrade: false,
648
+ expiresAt: null,
649
+ message: err?.message || 'License validation failed.',
650
+ },
651
+ { status: 400 }
652
+ );
653
+ }
654
+ };
655
+ }
656
+
589
657
  /**
590
658
  * createUploadHandler — factory for the file upload ingestion endpoint.
591
659
  */
@@ -922,6 +990,7 @@ export function createRagHandler(
922
990
  const historyHandler = createHistoryHandler(plugin, { onAuthorize });
923
991
  const feedbackHandler = createFeedbackHandler(plugin, { onAuthorize });
924
992
  const sessionsHandler = createSessionsHandler(plugin, { onAuthorize });
993
+ const licenseHandler = createLicenseHandler(plugin, { onAuthorize });
925
994
 
926
995
  async function routePostRequest(req: any, segment: string) {
927
996
  switch (segment) {
@@ -940,6 +1009,9 @@ export function createRagHandler(
940
1009
  return historyHandler(req);
941
1010
  case 'feedback':
942
1011
  return feedbackHandler(req);
1012
+ case 'license/validate':
1013
+ case 'v1/license/validate':
1014
+ return licenseHandler(req);
943
1015
  default:
944
1016
  return NextResponse.json({ error: `Not Found: POST segment "${segment}" not supported.` }, { status: 404 });
945
1017
  }
@@ -973,6 +1045,7 @@ export function createRagHandler(
973
1045
  try {
974
1046
  const url = new URL(req.url);
975
1047
  const pathname = url.pathname;
1048
+ if (pathname.includes('/license/validate')) return 'v1/license/validate';
976
1049
  if (pathname.endsWith('/upload')) return 'upload';
977
1050
  if (pathname.endsWith('/chat') || pathname.endsWith('/chat-sync')) return 'chat';
978
1051
  if (pathname.endsWith('/health')) return 'health';
@@ -129,8 +129,27 @@ export function useRagChat(
129
129
  userMessageId: userMsgId,
130
130
  });
131
131
 
132
- const fetchHeaders = {
132
+ const getHeader = (headersObj: Record<string, string> | undefined, name: string) => {
133
+ if (!headersObj) return undefined;
134
+ const target = name.toLowerCase();
135
+ for (const [k, v] of Object.entries(headersObj)) {
136
+ if (k.toLowerCase() === target) return v;
137
+ }
138
+ return undefined;
139
+ };
140
+
141
+ const resolvedLicenseKey =
142
+ options.licenseKey ||
143
+ getHeader(options.headers, 'x-license-key') ||
144
+ getHeader(options.headers, 'authorization')?.replace(/^Bearer\s+/i, '') ||
145
+ (typeof process !== 'undefined'
146
+ ? (process.env.NEXT_PUBLIC_RETRIVORA_LICENSE_KEY || process.env.RETRIVORA_LICENSE_KEY)
147
+ : '') ||
148
+ '';
149
+
150
+ const fetchHeaders: Record<string, string> = {
133
151
  'Content-Type': 'application/json',
152
+ ...(resolvedLicenseKey ? { 'x-license-key': resolvedLicenseKey } : {}),
134
153
  ...(options.headers || {})
135
154
  };
136
155
 
@@ -292,7 +311,15 @@ export function useRagChat(
292
311
  console.log('[useRagChat] Streaming stopped by user.');
293
312
  return;
294
313
  }
295
- const msg = err instanceof Error ? err.message : 'Something went wrong. Please try again.';
314
+ let msg = err instanceof Error ? err.message : 'Something went wrong. Please try again.';
315
+ if (
316
+ msg.includes('403') ||
317
+ msg.toLowerCase().includes('license') ||
318
+ msg.toLowerCase().includes('revoked') ||
319
+ msg.toLowerCase().includes('terminated')
320
+ ) {
321
+ msg = 'Your Retrivora license key has been terminated, suspended, or revoked. Access denied.';
322
+ }
296
323
  setError(msg);
297
324
  onError?.(msg);
298
325
  } finally {
package/src/index.ts CHANGED
@@ -63,6 +63,8 @@ export type { ILLMProvider, EmbedOptions } from './llm/ILLMProvider';
63
63
  export type { Chunk, ChunkOptions } from './rag/DocumentChunker';
64
64
  export type { Retrivora } from './core/Retrivora';
65
65
  export type { RetrivoraErrorCode } from './exceptions';
66
+ export { LicenseValidator } from './core/LicenseValidator';
67
+ export type { LicenseValidationRequest, LicenseValidationResponse } from './core/LicenseValidator';
66
68
  export {
67
69
  RetrivoraError,
68
70
  ProviderNotFoundException,
@@ -71,6 +73,8 @@ export {
71
73
  RateLimitException,
72
74
  ConfigurationException,
73
75
  AuthenticationException,
76
+ SDKVersionUnsupportedError,
77
+ LicenseValidationError,
74
78
  } from './exceptions';
75
79
  export type {
76
80
  VectorMatch,
package/src/server.ts CHANGED
@@ -90,6 +90,10 @@ export {
90
90
  sseErrorFrame,
91
91
  } from './handlers';
92
92
 
93
+ // ── License Validator ─────────────────────────────────────────
94
+ export { LicenseValidator } from './core/LicenseValidator';
95
+ export type { LicenseValidationRequest, LicenseValidationResponse } from './core/LicenseValidator';
96
+
93
97
  // ── Exceptions ────────────────────────────────────────────────
94
98
  export type { RetrivoraErrorCode } from './exceptions';
95
99
  export {
@@ -100,6 +104,8 @@ export {
100
104
  RateLimitException,
101
105
  ConfigurationException,
102
106
  AuthenticationException,
107
+ SDKVersionUnsupportedError,
108
+ LicenseValidationError,
103
109
  wrapError,
104
110
  } from './exceptions';
105
111
 
package/src/types/chat.ts CHANGED
@@ -57,6 +57,8 @@ export interface UseRagChatOptions {
57
57
  onError?: (error: string) => void;
58
58
  /** Optional custom headers to send with the chat request */
59
59
  headers?: Record<string, string>;
60
+ /** Optional Retrivora license key override */
61
+ licenseKey?: string;
60
62
  /** Session ID for history and feedback tracking (default: 'default') */
61
63
  sessionId?: string;
62
64
  }