oblien 1.0.5 → 1.0.6

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/README.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  Server-side SDK for building AI-powered applications with Oblien platform.
4
4
 
5
+ ## Features
6
+
7
+ - 🔐 **Direct header authentication** - Uses `x-client-id` and `x-client-secret` headers
8
+ - 👤 **Dual-layer guest identification** - IP + fingerprint for better guest tracking
9
+ - 🔄 **Smart guest matching** - Detects same guest even when IP or fingerprint changes
10
+ - 📊 **Namespace support** - Pass user ID for authenticated session tracking
11
+ - ⚡ **Automatic rate limiting** - Built-in limits for guest sessions
12
+ - 💾 **Flexible storage** - NodeCache (default), Redis, or custom adapters
13
+ - 🎯 **Single function for guest lookup** - `getGuest(ip, fingerprint)` handles both
14
+
5
15
  ## Installation
6
16
 
7
17
  ```bash
@@ -27,9 +37,9 @@ npm install oblien
27
37
  import { OblienClient } from 'oblien';
28
38
 
29
39
  const client = new OblienClient({
30
- apiKey: 'your-api-key',
31
- apiSecret: 'your-api-secret', // For server-side
32
- });
40
+ apiKey: 'your-client-id', // Your Oblien Client ID
41
+ apiSecret: 'your-client-secret', // Your Oblien Client Secret (required)
42
+ });
33
43
  ```
34
44
 
35
45
  ### 2. Create Chat Session
@@ -39,9 +49,10 @@ import { OblienChat } from 'oblien/chat';
39
49
 
40
50
  const chat = new OblienChat(client);
41
51
 
42
- // Create session
52
+ // Create session for authenticated user
43
53
  const session = await chat.createSession({
44
54
  agentId: 'your-agent-id',
55
+ namespace: 'user_123', // Optional: User ID for rate limiting/tracking
45
56
  // workflowId: 'workflow-id', // Optional
46
57
  // workspace: {}, // Optional
47
58
  });
@@ -51,7 +62,7 @@ console.log(session);
51
62
  // sessionId: 'session-xxx',
52
63
  // token: 'jwt-token-for-client',
53
64
  // agentId: 'agent-id',
54
- // namespace: null
65
+ // namespace: 'user_123'
55
66
  // }
56
67
  ```
57
68
 
@@ -102,7 +113,7 @@ function App() {
102
113
 
103
114
  ## Guest Sessions (Rate Limited)
104
115
 
105
- For anonymous users, create guest sessions based on IP address:
116
+ For anonymous users, create guest sessions with **dual-layer identification** using IP + fingerprint:
106
117
 
107
118
  ```javascript
108
119
  import { OblienChat } from 'oblien/chat';
@@ -112,9 +123,11 @@ const chat = new OblienChat(client);
112
123
  // Express route
113
124
  app.post('/api/guest-session', async (req, res) => {
114
125
  const ip = req.ip || req.headers['x-forwarded-for'];
126
+ const fingerprint = req.body.fingerprint; // Browser fingerprint
115
127
 
116
128
  const session = await chat.createGuestSession({
117
- ip: ip,
129
+ ip,
130
+ fingerprint, // NEW: Enables dual-layer identification
118
131
  agentId: 'your-agent-id',
119
132
  metadata: {
120
133
  userAgent: req.headers['user-agent'],
@@ -133,12 +146,24 @@ app.post('/api/guest-session', async (req, res) => {
133
146
 
134
147
  ### Guest Features:
135
148
 
149
+ - ✅ **Dual-layer identification**: IP + fingerprint for better guest tracking
150
+ - ✅ **Smart guest matching**: Same guest detected even if IP or fingerprint changes
136
151
  - ✅ Automatic rate limiting (100K tokens/day, 50 messages/day)
137
- - ✅ IP-based identification (privacy-friendly)
152
+ - ✅ Privacy-friendly (IP masked, fingerprint hashed)
138
153
  - ✅ Auto-expiring sessions (24h TTL)
139
154
  - ✅ Built-in caching with `node-cache` (no Redis required!)
140
155
  - ✅ Optional Redis support for distributed systems
141
156
 
157
+ ### How Dual-Layer Identification Works:
158
+
159
+ The package automatically tracks guests using both IP and fingerprint:
160
+
161
+ - **Fingerprint changes, IP stays** → Same guest detected ✅
162
+ - **IP changes, fingerprint stays** → Same guest detected ✅
163
+ - **Both change** → New guest created
164
+
165
+ This provides better continuity for users on mobile networks or using VPNs.
166
+
142
167
  ## Guest Storage Options
143
168
 
144
169
  ### Default: NodeCache (Recommended)
@@ -215,11 +240,23 @@ Session management:
215
240
  ```javascript
216
241
  const chat = new OblienChat(client, options?);
217
242
 
218
- // Create regular session
219
- await chat.createSession({ agentId, workflowId?, workspace? });
243
+ // Create regular session (authenticated users)
244
+ await chat.createSession({
245
+ agentId,
246
+ namespace?, // Optional: user_id for tracking
247
+ workflowId?,
248
+ workspace?
249
+ });
220
250
 
221
- // Create guest session
222
- await chat.createGuestSession({ ip, agentId, workflowId?, metadata?, workspace? });
251
+ // Create guest session (with dual-layer identification)
252
+ await chat.createGuestSession({
253
+ ip,
254
+ fingerprint?, // NEW: Browser fingerprint for better tracking
255
+ agentId,
256
+ workflowId?,
257
+ metadata?,
258
+ workspace?
259
+ });
223
260
 
224
261
  // Get session info
225
262
  await chat.getSession(sessionId);
@@ -231,7 +268,7 @@ await chat.listSessions({ page?, limit? });
231
268
  await chat.deleteSession(sessionId);
232
269
 
233
270
  // Guest management
234
- await chat.getGuestByIP(ip);
271
+ await chat.getGuest(ip, fingerprint?); // NEW: Unified function for IP and/or fingerprint lookup
235
272
  await chat.getAllGuests(); // Admin only
236
273
  await chat.cleanupGuests(); // Clean expired
237
274
  ```
@@ -249,7 +286,12 @@ const guestManager = new GuestManager({
249
286
  onGuestCreated?: (guest) => void,
250
287
  });
251
288
 
252
- await guestManager.getOrCreateGuest(ip, metadata?);
289
+ // With dual-layer identification
290
+ await guestManager.getOrCreateGuest(ip, fingerprint?, metadata?);
291
+
292
+ // Find existing guest by IP and/or fingerprint
293
+ await guestManager.findExistingGuest(fingerprint, ip);
294
+
253
295
  await guestManager.getGuest(guestId);
254
296
  await guestManager.updateGuest(guestId, updates);
255
297
  await guestManager.deleteGuest(guestId);
@@ -281,6 +323,7 @@ app.post('/api/session', async (req, res) => {
281
323
  try {
282
324
  const session = await chat.createSession({
283
325
  agentId: req.body.agentId,
326
+ namespace: req.user.id, // Pass user ID as namespace
284
327
  });
285
328
 
286
329
  res.json(session);
@@ -289,14 +332,36 @@ app.post('/api/session', async (req, res) => {
289
332
  }
290
333
  });
291
334
 
292
- // Create guest session
335
+ // Create guest session with dual-layer identification
293
336
  app.post('/api/guest-session', async (req, res) => {
294
337
  try {
295
- const ip = req.ip;
338
+ const ip = req.ip || req.headers['x-forwarded-for'];
339
+ const fingerprint = req.body.fingerprint; // From client
340
+
341
+ // Check for existing guest first
342
+ const existingGuest = await chat.getGuest(ip, fingerprint);
296
343
 
344
+ if (existingGuest && existingGuest.sessions.length > 0) {
345
+ // Return existing session if available
346
+ const latestSession = existingGuest.sessions[existingGuest.sessions.length - 1];
347
+ const sessionDetails = await chat.getSession(latestSession);
348
+
349
+ if (sessionDetails?.token) {
350
+ return res.json({
351
+ ...sessionDetails,
352
+ isExisting: true,
353
+ });
354
+ }
355
+ }
356
+
357
+ // Create new guest session
297
358
  const session = await chat.createGuestSession({
298
359
  ip,
360
+ fingerprint,
299
361
  agentId: req.body.agentId,
362
+ metadata: {
363
+ userAgent: req.headers['user-agent'],
364
+ },
300
365
  });
301
366
 
302
367
  res.json(session);
@@ -330,9 +395,11 @@ export default async function handler(req, res) {
330
395
  // For guests
331
396
  if (!req.headers.authorization) {
332
397
  const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
398
+ const fingerprint = req.body.fingerprint;
333
399
 
334
400
  const session = await chat.createGuestSession({
335
401
  ip,
402
+ fingerprint, // Dual-layer identification
336
403
  agentId: req.body.agentId,
337
404
  });
338
405
 
@@ -342,6 +409,7 @@ export default async function handler(req, res) {
342
409
  // For authenticated users
343
410
  const session = await chat.createSession({
344
411
  agentId: req.body.agentId,
412
+ namespace: req.user.id, // User ID for tracking
345
413
  });
346
414
 
347
415
  res.json(session);
@@ -381,10 +449,17 @@ OBLIEN_BASE_URL=https://api.oblien.com # Optional
381
449
 
382
450
  Check the `/examples` folder for complete examples:
383
451
 
452
+ - `raw.js` - Complete test suite demonstrating all features
384
453
  - `express-server.js` - Full Express.js implementation
385
454
  - `nextjs-api-route.js` - Next.js API routes (App Router & Pages Router)
386
455
  - `with-redis.js` - Production setup with Redis
387
456
 
457
+ Run the test example:
458
+ ```bash
459
+ cd node_modules/oblien
460
+ node examples/raw.js
461
+ ```
462
+
388
463
  ## TypeScript Support
389
464
 
390
465
  Full TypeScript definitions included:
@@ -413,9 +488,38 @@ const chat: OblienChat = new OblienChat(client);
413
488
  - 50 messages/day
414
489
  - 20 messages/hour
415
490
 
491
+ ### Q: How does dual-layer identification work?
492
+
493
+ **A:** The package tracks guests using both IP address and browser fingerprint:
494
+
495
+ ```javascript
496
+ // First visit: Creates guest with IP + fingerprint
497
+ await chat.createGuestSession({
498
+ ip: '1.2.3.4',
499
+ fingerprint: 'abc123',
500
+ agentId: 'agent-id',
501
+ });
502
+
503
+ // Same user, different IP (e.g., mobile network)
504
+ // → Same guest detected by fingerprint ✅
505
+ await chat.createGuestSession({
506
+ ip: '5.6.7.8', // Different IP
507
+ fingerprint: 'abc123', // Same fingerprint
508
+ agentId: 'agent-id',
509
+ });
510
+
511
+ // Same user, different fingerprint (e.g., cleared browser data)
512
+ // → Same guest detected by IP ✅
513
+ await chat.createGuestSession({
514
+ ip: '1.2.3.4', // Same IP
515
+ fingerprint: 'xyz789', // Different fingerprint
516
+ agentId: 'agent-id',
517
+ });
518
+ ```
519
+
416
520
  ### Q: Can I use custom guest IDs instead of IP?
417
521
 
418
- **A:** Yes! Just create your own guest ID and pass it as the `namespace`:
522
+ **A:** Yes! Pass it as the `namespace`:
419
523
 
420
524
  ```javascript
421
525
  await chat.createSession({
package/index.d.ts CHANGED
@@ -133,11 +133,13 @@ export interface ChatOptions {
133
133
  export interface CreateSessionOptions {
134
134
  agentId: string;
135
135
  workflowId?: string;
136
+ namespace?: string; // For authenticated users, typically user_id
136
137
  workspace?: Record<string, any>;
137
138
  }
138
139
 
139
140
  export interface CreateGuestSessionOptions {
140
141
  ip: string;
142
+ fingerprint?: string;
141
143
  agentId: string;
142
144
  workflowId?: string;
143
145
  metadata?: Record<string, any>;
@@ -157,7 +159,7 @@ export class OblienChat {
157
159
 
158
160
  createSession(options: CreateSessionOptions): Promise<SessionData>;
159
161
  createGuestSession(options: CreateGuestSessionOptions): Promise<GuestSessionData>;
160
- getGuestByIP(ip: string): Promise<Guest | null>;
162
+ getGuest(ip: string, fingerprint?: string): Promise<Guest | null>;
161
163
  getSession(sessionId: string): Promise<any>;
162
164
  listSessions(options?: Record<string, any>): Promise<any[]>;
163
165
  deleteSession(sessionId: string): Promise<any>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oblien",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "Server-side SDK for Oblien AI Platform - Build AI-powered applications with chat, agents, and workflows",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/src/chat/index.js CHANGED
@@ -46,9 +46,10 @@ export class OblienChat {
46
46
  }
47
47
 
48
48
  /**
49
- * Create a guest session based on IP
49
+ * Create a guest session based on IP and fingerprint (dual-layer identification)
50
50
  * @param {Object} options - Guest session options
51
51
  * @param {string} options.ip - Client IP address
52
+ * @param {string} [options.fingerprint] - Client fingerprint for identification
52
53
  * @param {string} options.agentId - Agent ID to chat with
53
54
  * @param {string} [options.workflowId] - Workflow ID
54
55
  * @param {Object} [options.metadata] - Additional guest metadata
@@ -56,14 +57,18 @@ export class OblienChat {
56
57
  * @returns {Promise<Object>} Session data with token and guest info
57
58
  */
58
59
  async createGuestSession(options) {
59
- const { ip, agentId, workflowId, metadata, workspace } = options;
60
+ const { ip, fingerprint, agentId, workflowId, metadata = {}, workspace } = options;
60
61
 
61
62
  if (!ip) {
62
63
  throw new Error('IP address is required for guest sessions');
63
64
  }
64
65
 
65
- // Get or create guest user
66
- const guest = await this.guestManager.getOrCreateGuest(ip, metadata);
66
+ // Get or create guest user (handles fingerprint and IP mapping internally)
67
+ const guest = await this.guestManager.getOrCreateGuest(ip, fingerprint, {
68
+ ...metadata,
69
+ fingerprint,
70
+ ip,
71
+ });
67
72
 
68
73
  // Create session
69
74
  const session = new ChatSession({
@@ -91,13 +96,35 @@ export class OblienChat {
91
96
  }
92
97
 
93
98
  /**
94
- * Get guest by IP
99
+ * Get guest by IP and/or fingerprint (dual-layer identification)
95
100
  * @param {string} ip - IP address
101
+ * @param {string} [fingerprint] - Client fingerprint (optional)
96
102
  * @returns {Promise<Object|null>} Guest object or null
97
103
  */
98
- async getGuestByIP(ip) {
99
- const guestId = this.guestManager.generateGuestId(ip);
100
- return await this.guestManager.getGuest(guestId);
104
+ async getGuest(ip, fingerprint = null) {
105
+ // Try fingerprint first if provided
106
+ if (fingerprint) {
107
+ const guestIdByFingerprint = await this.guestManager.storage.get(`fingerprint:${fingerprint}`);
108
+ if (guestIdByFingerprint) {
109
+ const guest = await this.guestManager.getGuest(guestIdByFingerprint);
110
+ if (guest) return guest;
111
+ }
112
+ }
113
+
114
+ // Fallback to IP
115
+ if (ip) {
116
+ const guestIdByIp = await this.guestManager.storage.get(`ip:${ip}`);
117
+ if (guestIdByIp) {
118
+ const guest = await this.guestManager.getGuest(guestIdByIp);
119
+ if (guest) return guest;
120
+ }
121
+
122
+ // Fallback to old method (generate guest ID from IP)
123
+ const fallbackGuestId = this.guestManager.generateGuestId(ip);
124
+ return await this.guestManager.getGuest(fallbackGuestId);
125
+ }
126
+
127
+ return null;
101
128
  }
102
129
 
103
130
  /**
@@ -41,21 +41,21 @@ export class ChatSession {
41
41
  async create() {
42
42
  const payload = {
43
43
  agent_id: this.agentId,
44
- workflow_id: this.workflowId,
44
+ app_id: this.workflowId, // Backend uses app_id for workflow_id
45
45
  is_guest: this.isGuest,
46
46
  namespace: this.namespace,
47
47
  workspace: this.workspace,
48
48
  };
49
49
 
50
- this.data = await this.client.post('ai/session', payload);
50
+ this.data = await this.client.post('ai/session/create', payload);
51
51
  this.sessionId = this.data.sessionId || this.data.session_id;
52
- this.token = this.data.token || this.data.tokens?.token;
52
+ this.token = this.data.token || this.data.accessToken || this.data.tokens?.token;
53
53
 
54
54
  return {
55
55
  sessionId: this.sessionId,
56
56
  token: this.token,
57
57
  agentId: this.data.agentId || this.agentId,
58
- workflowId: this.data.workflowId || this.workflowId,
58
+ workflowId: this.data.workflowId || this.data.appId || this.workflowId,
59
59
  namespace: this.namespace,
60
60
  };
61
61
  }
package/src/client.js CHANGED
@@ -6,22 +6,22 @@
6
6
  export class OblienClient {
7
7
  /**
8
8
  * @param {Object} config - Configuration options
9
- * @param {string} config.apiKey - Your Oblien API key
10
- * @param {string} [config.apiSecret] - Your Oblien API secret (for server-side)
9
+ * @param {string} config.apiKey - Your Oblien Client ID (x-client-id)
10
+ * @param {string} config.apiSecret - Your Oblien Client Secret (x-client-secret)
11
11
  * @param {string} [config.baseURL] - Base URL for API (default: https://api.oblien.com)
12
12
  * @param {string} [config.version] - API version (default: v1)
13
13
  */
14
14
  constructor(config) {
15
15
  if (!config || !config.apiKey) {
16
- throw new Error('Oblien API key is required');
16
+ throw new Error('Oblien API key (client ID) is required');
17
+ }
18
+ if (!config.apiSecret) {
19
+ throw new Error('Oblien API secret (client secret) is required');
17
20
  }
18
21
 
19
- this.apiKey = config.apiKey;
20
- this.apiSecret = config.apiSecret;
22
+ this.clientId = config.apiKey;
23
+ this.clientSecret = config.apiSecret;
21
24
  this.baseURL = config.baseURL || 'https://api.oblien.com';
22
- this.version = config.version || 'v1';
23
- this.token = null;
24
- this.tokenExpiry = null;
25
25
  }
26
26
 
27
27
  /**
@@ -30,55 +30,18 @@ export class OblienClient {
30
30
  */
31
31
  _buildURL(path) {
32
32
  const cleanPath = path.startsWith('/') ? path.slice(1) : path;
33
- return `${this.baseURL}/${this.version}/${cleanPath}`;
34
- }
35
-
36
- /**
37
- * Authenticate and get access token
38
- * @returns {Promise<string>} Access token
39
- */
40
- async authenticate() {
41
- // Check if we have a valid token
42
- if (this.token && this.tokenExpiry && Date.now() < this.tokenExpiry) {
43
- return this.token;
44
- }
45
-
46
- try {
47
- const response = await fetch(this._buildURL('auth/authenticate'), {
48
- method: 'POST',
49
- headers: {
50
- 'Content-Type': 'application/json',
51
- },
52
- body: JSON.stringify({
53
- apiKey: this.apiKey,
54
- apiSecret: this.apiSecret,
55
- }),
56
- });
57
-
58
- if (!response.ok) {
59
- const error = await response.json();
60
- throw new Error(error.message || 'Authentication failed');
61
- }
62
-
63
- const data = await response.json();
64
- this.token = data.token;
65
- // Token expires in 1 hour, refresh 5 min before
66
- this.tokenExpiry = Date.now() + (55 * 60 * 1000);
67
-
68
- return this.token;
69
- } catch (error) {
70
- throw new Error(`Authentication error: ${error.message}`);
71
- }
33
+ // Backend doesn't use version prefix, routes are directly mounted
34
+ return `${this.baseURL}/${cleanPath}`;
72
35
  }
73
36
 
74
37
  /**
75
38
  * Get authentication headers
76
- * @returns {Promise<Object>} Headers object
39
+ * @returns {Object} Headers object with x-client-id and x-client-secret
77
40
  */
78
- async getAuthHeaders() {
79
- const token = await this.authenticate();
41
+ getAuthHeaders() {
80
42
  return {
81
- 'Authorization': `Bearer ${token}`,
43
+ 'x-client-id': this.clientId,
44
+ 'x-client-secret': this.clientSecret,
82
45
  'Content-Type': 'application/json',
83
46
  };
84
47
  }
@@ -90,7 +53,7 @@ export class OblienClient {
90
53
  * @returns {Promise<any>} Response data
91
54
  */
92
55
  async get(path, params = {}) {
93
- const headers = await this.getAuthHeaders();
56
+ const headers = this.getAuthHeaders();
94
57
  const url = new URL(this._buildURL(path));
95
58
 
96
59
  // Add query parameters
@@ -115,7 +78,7 @@ export class OblienClient {
115
78
  * @returns {Promise<any>} Response data
116
79
  */
117
80
  async post(path, body = {}) {
118
- const headers = await this.getAuthHeaders();
81
+ const headers = this.getAuthHeaders();
119
82
 
120
83
  const response = await fetch(this._buildURL(path), {
121
84
  method: 'POST',
@@ -133,7 +96,7 @@ export class OblienClient {
133
96
  * @returns {Promise<any>} Response data
134
97
  */
135
98
  async put(path, body = {}) {
136
- const headers = await this.getAuthHeaders();
99
+ const headers = this.getAuthHeaders();
137
100
 
138
101
  const response = await fetch(this._buildURL(path), {
139
102
  method: 'PUT',
@@ -150,7 +113,7 @@ export class OblienClient {
150
113
  * @returns {Promise<any>} Response data
151
114
  */
152
115
  async delete(path) {
153
- const headers = await this.getAuthHeaders();
116
+ const headers = this.getAuthHeaders();
154
117
 
155
118
  const response = await fetch(this._buildURL(path), {
156
119
  method: 'DELETE',
@@ -31,38 +31,139 @@ export class GuestManager {
31
31
  }
32
32
 
33
33
  /**
34
- * Get or create guest user by IP
34
+ * Find existing guest by fingerprint or IP (dual-layer identification)
35
+ * @param {string} fingerprint - Client fingerprint
35
36
  * @param {string} ip - IP address
36
- * @param {Object} [metadata] - Additional metadata to store
37
+ * @returns {Promise<Object|null>} Guest object or null
38
+ */
39
+ async findExistingGuest(fingerprint, ip) {
40
+ // Layer 1: Try to find by fingerprint first
41
+ const guestIdByFingerprint = await this.storage.get(`fingerprint:${fingerprint}`);
42
+ if (guestIdByFingerprint) {
43
+ const guest = await this.getGuest(guestIdByFingerprint);
44
+ if (guest) {
45
+ // Update IP mapping if it changed
46
+ if (guest.metadata?.ip !== ip) {
47
+ await this._updateMappings(guest.id, fingerprint, ip);
48
+ await this.updateGuest(guest.id, {
49
+ ip: this._maskIP(ip),
50
+ metadata: {
51
+ ...guest.metadata,
52
+ ip,
53
+ previousIps: [...(guest.metadata?.previousIps || []), guest.metadata?.ip].filter(Boolean),
54
+ },
55
+ });
56
+ }
57
+ return guest;
58
+ }
59
+ }
60
+
61
+ // Layer 2: Fallback to IP if fingerprint didn't match
62
+ const guestIdByIp = await this.storage.get(`ip:${ip}`);
63
+ if (guestIdByIp) {
64
+ const guest = await this.getGuest(guestIdByIp);
65
+ if (guest) {
66
+ // Update fingerprint mapping
67
+ await this._updateMappings(guest.id, fingerprint, ip);
68
+ await this.updateGuest(guest.id, {
69
+ metadata: {
70
+ ...guest.metadata,
71
+ fingerprint,
72
+ previousFingerprints: [...(guest.metadata?.previousFingerprints || []), guest.metadata?.fingerprint].filter(Boolean),
73
+ },
74
+ });
75
+ return guest;
76
+ }
77
+ }
78
+
79
+ return null;
80
+ }
81
+
82
+ /**
83
+ * Update fingerprint and IP mappings
84
+ * @private
85
+ */
86
+ async _updateMappings(guestId, fingerprint, ip) {
87
+ await this.storage.set(`fingerprint:${fingerprint}`, guestId, this.ttl);
88
+ await this.storage.set(`ip:${ip}`, guestId, this.ttl);
89
+ }
90
+
91
+ /**
92
+ * Get or create guest user by IP and fingerprint (dual-layer identification)
93
+ * Supports both old signature: getOrCreateGuest(ip, metadata)
94
+ * and new signature: getOrCreateGuest(ip, fingerprint, metadata)
95
+ * @param {string} ip - IP address
96
+ * @param {string|Object} fingerprintOrMetadata - Client fingerprint (string) or metadata (object)
97
+ * @param {Object} [metadata] - Additional metadata to store (only if fingerprint is provided)
37
98
  * @returns {Promise<Object>} Guest user object
38
99
  */
39
- async getOrCreateGuest(ip, metadata = {}) {
40
- const guestId = this.generateGuestId(ip);
41
-
42
- // Try to get existing guest
43
- let guest = await this.storage.get(`guest:${guestId}`);
44
-
45
- if (guest) {
46
- // Update last seen
47
- guest.lastSeen = new Date().toISOString();
48
- await this.storage.set(`guest:${guestId}`, guest, this.ttl);
49
- return guest;
100
+ async getOrCreateGuest(ip, fingerprintOrMetadata = null, metadata = {}) {
101
+ // Handle backward compatibility: detect if second param is metadata (object) or fingerprint (string)
102
+ let fingerprint = null;
103
+ let finalMetadata = {};
104
+
105
+ if (fingerprintOrMetadata === null || fingerprintOrMetadata === undefined) {
106
+ // No second parameter
107
+ finalMetadata = metadata || {};
108
+ } else if (typeof fingerprintOrMetadata === 'string') {
109
+ // New signature: (ip, fingerprint, metadata)
110
+ fingerprint = fingerprintOrMetadata;
111
+ finalMetadata = metadata || {};
112
+ } else if (typeof fingerprintOrMetadata === 'object') {
113
+ // Old signature: (ip, metadata) - fingerprint is in metadata
114
+ finalMetadata = fingerprintOrMetadata;
115
+ fingerprint = finalMetadata.fingerprint || null;
116
+ }
117
+
118
+ // Try to find existing guest by fingerprint or IP
119
+ if (fingerprint) {
120
+ const existingGuest = await this.findExistingGuest(fingerprint, ip);
121
+ if (existingGuest) {
122
+ // Update last seen
123
+ existingGuest.lastSeen = new Date().toISOString();
124
+ await this.storage.set(`guest:${existingGuest.id}`, existingGuest, this.ttl);
125
+ return existingGuest;
126
+ }
127
+ } else {
128
+ // Try to find by IP only
129
+ const guestIdByIp = await this.storage.get(`ip:${ip}`);
130
+ if (guestIdByIp) {
131
+ const guest = await this.getGuest(guestIdByIp);
132
+ if (guest) {
133
+ guest.lastSeen = new Date().toISOString();
134
+ await this.storage.set(`guest:${guest.id}`, guest, this.ttl);
135
+ return guest;
136
+ }
137
+ }
50
138
  }
51
139
 
52
140
  // Create new guest
53
- guest = {
141
+ const guestId = this.generateGuestId(ip);
142
+ const guest = {
54
143
  id: guestId,
55
144
  namespace: guestId, // For rate limiting
56
145
  ip: this._maskIP(ip), // Store masked IP for privacy
57
146
  isGuest: true,
58
147
  createdAt: new Date().toISOString(),
59
148
  lastSeen: new Date().toISOString(),
60
- metadata,
149
+ metadata: {
150
+ ...finalMetadata,
151
+ ip,
152
+ fingerprint,
153
+ },
61
154
  sessions: [],
62
155
  };
63
156
 
64
157
  await this.storage.set(`guest:${guestId}`, guest, this.ttl);
65
158
 
159
+ // Store mappings
160
+ if (fingerprint) {
161
+ await this._updateMappings(guestId, fingerprint, ip);
162
+ } else {
163
+ // Fallback: just store IP mapping if no fingerprint
164
+ await this.storage.set(`ip:${ip}`, guestId, this.ttl);
165
+ }
166
+
66
167
  // Call callback if provided
67
168
  if (this.onGuestCreated) {
68
169
  this.onGuestCreated(guest);