oblien 1.0.0
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/LICENSE +21 -0
- package/README.md +455 -0
- package/chat.js +21 -0
- package/index.d.ts +180 -0
- package/index.js +23 -0
- package/package.json +67 -0
- package/src/chat/index.js +149 -0
- package/src/chat/session.js +99 -0
- package/src/client.js +189 -0
- package/src/utils/guest-manager.js +349 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guest Manager
|
|
3
|
+
* Manages guest users based on IP addresses or custom identifiers
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import crypto from 'crypto';
|
|
7
|
+
import NodeCache from 'node-cache';
|
|
8
|
+
|
|
9
|
+
export class GuestManager {
|
|
10
|
+
/**
|
|
11
|
+
* @param {Object} options - Configuration options
|
|
12
|
+
* @param {Object} [options.storage] - Custom storage adapter (must implement get, set, delete)
|
|
13
|
+
* @param {number} [options.ttl] - Time to live for guest sessions in seconds (default: 24 hours)
|
|
14
|
+
* @param {Function} [options.onGuestCreated] - Callback when a new guest is created
|
|
15
|
+
*/
|
|
16
|
+
constructor(options = {}) {
|
|
17
|
+
this.storage = options.storage || new NodeCacheStorage(options.ttl);
|
|
18
|
+
this.ttl = options.ttl || 24 * 60 * 60; // 24 hours
|
|
19
|
+
this.onGuestCreated = options.onGuestCreated;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Generate guest ID from IP address
|
|
24
|
+
* @param {string} ip - IP address
|
|
25
|
+
* @returns {string} Guest ID
|
|
26
|
+
*/
|
|
27
|
+
generateGuestId(ip) {
|
|
28
|
+
// Hash IP to create consistent guest ID
|
|
29
|
+
const hash = crypto.createHash('sha256').update(ip).digest('hex');
|
|
30
|
+
return `guest_${hash.substring(0, 16)}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get or create guest user by IP
|
|
35
|
+
* @param {string} ip - IP address
|
|
36
|
+
* @param {Object} [metadata] - Additional metadata to store
|
|
37
|
+
* @returns {Promise<Object>} Guest user object
|
|
38
|
+
*/
|
|
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;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Create new guest
|
|
53
|
+
guest = {
|
|
54
|
+
id: guestId,
|
|
55
|
+
namespace: guestId, // For rate limiting
|
|
56
|
+
ip: this._maskIP(ip), // Store masked IP for privacy
|
|
57
|
+
isGuest: true,
|
|
58
|
+
createdAt: new Date().toISOString(),
|
|
59
|
+
lastSeen: new Date().toISOString(),
|
|
60
|
+
metadata,
|
|
61
|
+
sessions: [],
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
await this.storage.set(`guest:${guestId}`, guest, this.ttl);
|
|
65
|
+
|
|
66
|
+
// Call callback if provided
|
|
67
|
+
if (this.onGuestCreated) {
|
|
68
|
+
this.onGuestCreated(guest);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return guest;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get guest by ID
|
|
76
|
+
* @param {string} guestId - Guest ID
|
|
77
|
+
* @returns {Promise<Object|null>} Guest object or null
|
|
78
|
+
*/
|
|
79
|
+
async getGuest(guestId) {
|
|
80
|
+
return await this.storage.get(`guest:${guestId}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Update guest metadata
|
|
85
|
+
* @param {string} guestId - Guest ID
|
|
86
|
+
* @param {Object} updates - Fields to update
|
|
87
|
+
* @returns {Promise<Object>} Updated guest object
|
|
88
|
+
*/
|
|
89
|
+
async updateGuest(guestId, updates) {
|
|
90
|
+
const guest = await this.storage.get(`guest:${guestId}`);
|
|
91
|
+
|
|
92
|
+
if (!guest) {
|
|
93
|
+
throw new Error(`Guest not found: ${guestId}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const updated = {
|
|
97
|
+
...guest,
|
|
98
|
+
...updates,
|
|
99
|
+
lastSeen: new Date().toISOString(),
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
await this.storage.set(`guest:${guestId}`, updated, this.ttl);
|
|
103
|
+
return updated;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Add session to guest
|
|
108
|
+
* @param {string} guestId - Guest ID
|
|
109
|
+
* @param {string} sessionId - Session ID
|
|
110
|
+
* @returns {Promise<Object>} Updated guest object
|
|
111
|
+
*/
|
|
112
|
+
async addSession(guestId, sessionId) {
|
|
113
|
+
const guest = await this.storage.get(`guest:${guestId}`);
|
|
114
|
+
|
|
115
|
+
if (!guest) {
|
|
116
|
+
throw new Error(`Guest not found: ${guestId}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!guest.sessions.includes(sessionId)) {
|
|
120
|
+
guest.sessions.push(sessionId);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
guest.lastSeen = new Date().toISOString();
|
|
124
|
+
await this.storage.set(`guest:${guestId}`, guest, this.ttl);
|
|
125
|
+
|
|
126
|
+
return guest;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Delete guest
|
|
131
|
+
* @param {string} guestId - Guest ID
|
|
132
|
+
* @returns {Promise<boolean>} Success status
|
|
133
|
+
*/
|
|
134
|
+
async deleteGuest(guestId) {
|
|
135
|
+
return await this.storage.delete(`guest:${guestId}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Mask IP address for privacy (keep first 2 octets)
|
|
140
|
+
* @private
|
|
141
|
+
*/
|
|
142
|
+
_maskIP(ip) {
|
|
143
|
+
if (!ip) return 'unknown';
|
|
144
|
+
|
|
145
|
+
// IPv4
|
|
146
|
+
if (ip.includes('.')) {
|
|
147
|
+
const parts = ip.split('.');
|
|
148
|
+
return `${parts[0]}.${parts[1]}.xxx.xxx`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// IPv6 - keep first 4 groups
|
|
152
|
+
if (ip.includes(':')) {
|
|
153
|
+
const parts = ip.split(':');
|
|
154
|
+
return `${parts.slice(0, 4).join(':')}:xxxx:xxxx:xxxx:xxxx`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return 'unknown';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Get all active guests (for admin/monitoring)
|
|
162
|
+
* @returns {Promise<Array>} Array of guest objects
|
|
163
|
+
*/
|
|
164
|
+
async getAllGuests() {
|
|
165
|
+
return await this.storage.getAll('guest:*');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Clean up expired guests
|
|
170
|
+
* @returns {Promise<number>} Number of cleaned guests
|
|
171
|
+
*/
|
|
172
|
+
async cleanup() {
|
|
173
|
+
const guests = await this.getAllGuests();
|
|
174
|
+
const now = Date.now();
|
|
175
|
+
let cleaned = 0;
|
|
176
|
+
|
|
177
|
+
for (const guest of guests) {
|
|
178
|
+
const lastSeen = new Date(guest.lastSeen).getTime();
|
|
179
|
+
const age = now - lastSeen;
|
|
180
|
+
|
|
181
|
+
if (age > this.ttl * 1000) {
|
|
182
|
+
await this.deleteGuest(guest.id);
|
|
183
|
+
cleaned++;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return cleaned;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* NodeCache Storage Adapter (Default)
|
|
193
|
+
* Uses node-cache for automatic expiration and memory management
|
|
194
|
+
*/
|
|
195
|
+
class NodeCacheStorage {
|
|
196
|
+
constructor(ttl = 24 * 60 * 60) {
|
|
197
|
+
this.cache = new NodeCache({
|
|
198
|
+
stdTTL: ttl,
|
|
199
|
+
checkperiod: 600, // Check for expired keys every 10 minutes
|
|
200
|
+
useClones: false, // Better performance
|
|
201
|
+
maxKeys: 100000, // Limit memory usage
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async get(key) {
|
|
206
|
+
return this.cache.get(key) || null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async set(key, value, ttl) {
|
|
210
|
+
return this.cache.set(key, value, ttl || undefined);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async delete(key) {
|
|
214
|
+
return this.cache.del(key) > 0;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async getAll(pattern) {
|
|
218
|
+
const prefix = pattern.replace('*', '');
|
|
219
|
+
const keys = this.cache.keys();
|
|
220
|
+
const results = [];
|
|
221
|
+
|
|
222
|
+
for (const key of keys) {
|
|
223
|
+
if (key.startsWith(prefix)) {
|
|
224
|
+
const value = this.cache.get(key);
|
|
225
|
+
if (value) results.push(value);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return results;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Get cache statistics
|
|
234
|
+
*/
|
|
235
|
+
getStats() {
|
|
236
|
+
return this.cache.getStats();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Clear all cache
|
|
241
|
+
*/
|
|
242
|
+
clear() {
|
|
243
|
+
this.cache.flushAll();
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Simple In-Memory Storage (Fallback)
|
|
249
|
+
* Use NodeCacheStorage or RedisStorage for production
|
|
250
|
+
*/
|
|
251
|
+
export class InMemoryStorage {
|
|
252
|
+
constructor() {
|
|
253
|
+
this.store = new Map();
|
|
254
|
+
this.expirations = new Map();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async get(key) {
|
|
258
|
+
// Check if expired
|
|
259
|
+
const expiry = this.expirations.get(key);
|
|
260
|
+
if (expiry && Date.now() > expiry) {
|
|
261
|
+
this.store.delete(key);
|
|
262
|
+
this.expirations.delete(key);
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return this.store.get(key) || null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async set(key, value, ttl) {
|
|
270
|
+
this.store.set(key, value);
|
|
271
|
+
|
|
272
|
+
if (ttl) {
|
|
273
|
+
this.expirations.set(key, Date.now() + (ttl * 1000));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async delete(key) {
|
|
280
|
+
this.expirations.delete(key);
|
|
281
|
+
return this.store.delete(key);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async getAll(pattern) {
|
|
285
|
+
const results = [];
|
|
286
|
+
const prefix = pattern.replace('*', '');
|
|
287
|
+
|
|
288
|
+
for (const [key, value] of this.store.entries()) {
|
|
289
|
+
if (key.startsWith(prefix)) {
|
|
290
|
+
// Check expiration
|
|
291
|
+
const expiry = this.expirations.get(key);
|
|
292
|
+
if (!expiry || Date.now() <= expiry) {
|
|
293
|
+
results.push(value);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return results;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Redis Storage Adapter
|
|
304
|
+
* For production use with Redis
|
|
305
|
+
* Requires 'redis' package: npm install redis
|
|
306
|
+
*/
|
|
307
|
+
export class RedisStorage {
|
|
308
|
+
constructor(redisClient) {
|
|
309
|
+
if (!redisClient) {
|
|
310
|
+
throw new Error('Redis client is required');
|
|
311
|
+
}
|
|
312
|
+
this.redis = redisClient;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async get(key) {
|
|
316
|
+
const data = await this.redis.get(key);
|
|
317
|
+
return data ? JSON.parse(data) : null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async set(key, value, ttl) {
|
|
321
|
+
const data = JSON.stringify(value);
|
|
322
|
+
if (ttl) {
|
|
323
|
+
await this.redis.setEx(key, ttl, data);
|
|
324
|
+
} else {
|
|
325
|
+
await this.redis.set(key, data);
|
|
326
|
+
}
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async delete(key) {
|
|
331
|
+
await this.redis.del(key);
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async getAll(pattern) {
|
|
336
|
+
const keys = await this.redis.keys(pattern);
|
|
337
|
+
const results = [];
|
|
338
|
+
|
|
339
|
+
for (const key of keys) {
|
|
340
|
+
const data = await this.get(key);
|
|
341
|
+
if (data) results.push(data);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return results;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export { NodeCacheStorage };
|
|
349
|
+
export default GuestManager;
|