oblien 1.0.5 → 1.0.7
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 +121 -17
- package/index.d.ts +3 -1
- package/package.json +1 -1
- package/src/chat/index.js +38 -8
- package/src/chat/session.js +13 -4
- package/src/client.js +18 -55
- package/src/utils/guest-manager.js +113 -12
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-
|
|
31
|
-
apiSecret: 'your-
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
- ✅
|
|
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({
|
|
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({
|
|
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.
|
|
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
|
-
|
|
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!
|
|
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
|
-
|
|
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
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,
|
|
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({
|
|
@@ -73,6 +78,9 @@ export class OblienChat {
|
|
|
73
78
|
workspace,
|
|
74
79
|
isGuest: true,
|
|
75
80
|
namespace: guest.namespace,
|
|
81
|
+
ipAddress: ip,
|
|
82
|
+
userAgent: metadata.userAgent,
|
|
83
|
+
fingerprint: fingerprint,
|
|
76
84
|
});
|
|
77
85
|
|
|
78
86
|
const sessionData = await session.create();
|
|
@@ -91,13 +99,35 @@ export class OblienChat {
|
|
|
91
99
|
}
|
|
92
100
|
|
|
93
101
|
/**
|
|
94
|
-
* Get guest by IP
|
|
102
|
+
* Get guest by IP and/or fingerprint (dual-layer identification)
|
|
95
103
|
* @param {string} ip - IP address
|
|
104
|
+
* @param {string} [fingerprint] - Client fingerprint (optional)
|
|
96
105
|
* @returns {Promise<Object|null>} Guest object or null
|
|
97
106
|
*/
|
|
98
|
-
async
|
|
99
|
-
|
|
100
|
-
|
|
107
|
+
async getGuest(ip, fingerprint = null) {
|
|
108
|
+
// Try fingerprint first if provided
|
|
109
|
+
if (fingerprint) {
|
|
110
|
+
const guestIdByFingerprint = await this.guestManager.storage.get(`fingerprint:${fingerprint}`);
|
|
111
|
+
if (guestIdByFingerprint) {
|
|
112
|
+
const guest = await this.guestManager.getGuest(guestIdByFingerprint);
|
|
113
|
+
if (guest) return guest;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Fallback to IP
|
|
118
|
+
if (ip) {
|
|
119
|
+
const guestIdByIp = await this.guestManager.storage.get(`ip:${ip}`);
|
|
120
|
+
if (guestIdByIp) {
|
|
121
|
+
const guest = await this.guestManager.getGuest(guestIdByIp);
|
|
122
|
+
if (guest) return guest;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Fallback to old method (generate guest ID from IP)
|
|
126
|
+
const fallbackGuestId = this.guestManager.generateGuestId(ip);
|
|
127
|
+
return await this.guestManager.getGuest(fallbackGuestId);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return null;
|
|
101
131
|
}
|
|
102
132
|
|
|
103
133
|
/**
|
package/src/chat/session.js
CHANGED
|
@@ -13,6 +13,9 @@ export class ChatSession {
|
|
|
13
13
|
* @param {boolean} [options.isGuest] - Is this a guest session
|
|
14
14
|
* @param {string} [options.namespace] - Guest namespace for rate limiting
|
|
15
15
|
* @param {Object} [options.workspace] - Workspace configuration
|
|
16
|
+
* @param {string} [options.ipAddress] - IP address of the user
|
|
17
|
+
* @param {string} [options.userAgent] - User agent of the user
|
|
18
|
+
* @param {string} [options.fingerprint] - Fingerprint of the user
|
|
16
19
|
*/
|
|
17
20
|
constructor(options) {
|
|
18
21
|
if (!options.client) {
|
|
@@ -32,6 +35,9 @@ export class ChatSession {
|
|
|
32
35
|
this.workspace = options.workspace;
|
|
33
36
|
this.token = null;
|
|
34
37
|
this.data = null;
|
|
38
|
+
this.ipAddress = options.ipAddress || null;
|
|
39
|
+
this.userAgent = options.userAgent || null;
|
|
40
|
+
this.fingerprint = options.fingerprint || null;
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
/**
|
|
@@ -41,21 +47,24 @@ export class ChatSession {
|
|
|
41
47
|
async create() {
|
|
42
48
|
const payload = {
|
|
43
49
|
agent_id: this.agentId,
|
|
44
|
-
|
|
50
|
+
app_id: this.workflowId, // Backend uses app_id for workflow_id
|
|
45
51
|
is_guest: this.isGuest,
|
|
46
52
|
namespace: this.namespace,
|
|
47
53
|
workspace: this.workspace,
|
|
54
|
+
ip_address: this.ipAddress,
|
|
55
|
+
user_agent: this.userAgent,
|
|
56
|
+
fingerprint: this.fingerprint,
|
|
48
57
|
};
|
|
49
58
|
|
|
50
|
-
this.data = await this.client.post('ai/session', payload);
|
|
59
|
+
this.data = await this.client.post('ai/session/create', payload);
|
|
51
60
|
this.sessionId = this.data.sessionId || this.data.session_id;
|
|
52
|
-
this.token = this.data.token || this.data.tokens?.token;
|
|
61
|
+
this.token = this.data.token || this.data.accessToken || this.data.tokens?.token;
|
|
53
62
|
|
|
54
63
|
return {
|
|
55
64
|
sessionId: this.sessionId,
|
|
56
65
|
token: this.token,
|
|
57
66
|
agentId: this.data.agentId || this.agentId,
|
|
58
|
-
workflowId: this.data.workflowId || this.workflowId,
|
|
67
|
+
workflowId: this.data.workflowId || this.data.appId || this.workflowId,
|
|
59
68
|
namespace: this.namespace,
|
|
60
69
|
};
|
|
61
70
|
}
|
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
|
|
10
|
-
* @param {string}
|
|
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.
|
|
20
|
-
this.
|
|
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
|
-
|
|
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 {
|
|
39
|
+
* @returns {Object} Headers object with x-client-id and x-client-secret
|
|
77
40
|
*/
|
|
78
|
-
|
|
79
|
-
const token = await this.authenticate();
|
|
41
|
+
getAuthHeaders() {
|
|
80
42
|
return {
|
|
81
|
-
'
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
*
|
|
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
|
-
* @
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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);
|
|
45
132
|
if (guest) {
|
|
46
|
-
// Update last seen
|
|
47
133
|
guest.lastSeen = new Date().toISOString();
|
|
48
|
-
|
|
134
|
+
await this.storage.set(`guest:${guest.id}`, guest, this.ttl);
|
|
49
135
|
return guest;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
50
138
|
}
|
|
51
139
|
|
|
52
140
|
// Create new guest
|
|
53
|
-
|
|
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);
|