oblien 1.2.3 → 1.2.5

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.
@@ -0,0 +1,570 @@
1
+ /**
2
+ * Oblien CDN Module
3
+ * Upload and manage files on Oblien CDN with automatic token management
4
+ */
5
+
6
+ export class OblienCDN {
7
+ constructor(client, config = {}) {
8
+ if (!client) throw new Error('Oblien client is required');
9
+ this.client = client;
10
+ this.cdnURL = config.cdnURL || 'https://cdn.oblien.com/api';
11
+
12
+ // Token cache for reuse within expiration window
13
+ this._tokenCache = {
14
+ user: null,
15
+ admin: null
16
+ };
17
+ }
18
+
19
+ /**
20
+ * Generate a user CDN token (upload/process permissions)
21
+ * Tokens expire in 1 minute for security
22
+ *
23
+ * @returns {Promise<Object>} Token response with token, scope, and permissions
24
+ *
25
+ * @example
26
+ * const tokenData = await cdn.generateUserToken();
27
+ * // Returns:
28
+ * // {
29
+ * // success: true,
30
+ * // token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
31
+ * // scope: 'user',
32
+ * // permissions: ['upload', 'process'],
33
+ * // expiresIn: '1m'
34
+ * // }
35
+ */
36
+ async generateUserToken() {
37
+ const response = await this.client.post('cdn/token');
38
+
39
+ // Cache token for reuse
40
+ if (response.success && response.token) {
41
+ this._tokenCache.user = {
42
+ token: response.token,
43
+ expiresAt: Date.now() + 50000 // 50 seconds (buffer before 1m expiry)
44
+ };
45
+ }
46
+
47
+ return response;
48
+ }
49
+
50
+ /**
51
+ * Generate an admin CDN token (full access permissions)
52
+ * Requires admin privileges on the API account
53
+ * Tokens expire in 1 minute for security
54
+ *
55
+ * @returns {Promise<Object>} Token response with token, scope, and permissions
56
+ * @throws {Error} If user doesn't have admin privileges
57
+ *
58
+ * @example
59
+ * const tokenData = await cdn.generateAdminToken();
60
+ * // Returns:
61
+ * // {
62
+ * // success: true,
63
+ * // token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
64
+ * // scope: 'admin',
65
+ * // permissions: ['upload', 'process', 'read', 'list', 'info', 'delete'],
66
+ * // expiresIn: '1m'
67
+ * // }
68
+ */
69
+ async generateAdminToken() {
70
+ const response = await this.client.post('cdn/token/admin');
71
+
72
+ // Cache token for reuse
73
+ if (response.success && response.token) {
74
+ this._tokenCache.admin = {
75
+ token: response.token,
76
+ expiresAt: Date.now() + 50000 // 50 seconds (buffer before 1m expiry)
77
+ };
78
+ }
79
+
80
+ return response;
81
+ }
82
+
83
+ /**
84
+ * Get a valid token (user or admin)
85
+ * Automatically generates new token if cache is empty or expired
86
+ * @private
87
+ */
88
+ async _getToken(scope = 'user') {
89
+ const cached = this._tokenCache[scope];
90
+
91
+ // Use cached token if still valid
92
+ if (cached && cached.expiresAt > Date.now()) {
93
+ return cached.token;
94
+ }
95
+
96
+ // Generate new token
97
+ const tokenData = scope === 'admin'
98
+ ? await this.generateAdminToken()
99
+ : await this.generateUserToken();
100
+
101
+ return tokenData.token;
102
+ }
103
+
104
+ /**
105
+ * Normalize file input to buffer and filename
106
+ * Supports: Buffer, Base64, Multer files, Stream
107
+ * @private
108
+ */
109
+ _normalizeFile(file, providedFilename) {
110
+ let buffer;
111
+ let filename = providedFilename;
112
+ let mimetype;
113
+
114
+ // Handle Multer file object (from Express.js file uploads)
115
+ if (file && file.buffer && file.originalname) {
116
+ buffer = file.buffer;
117
+ filename = filename || file.originalname;
118
+ mimetype = file.mimetype;
119
+ }
120
+ // Handle Base64 string
121
+ else if (typeof file === 'string') {
122
+ // Check if it's a data URL (data:image/jpeg;base64,...)
123
+ const base64Match = file.match(/^data:([^;]+);base64,(.+)$/);
124
+ if (base64Match) {
125
+ mimetype = base64Match[1];
126
+ buffer = Buffer.from(base64Match[2], 'base64');
127
+ // Extract extension from mimetype if no filename
128
+ if (!filename) {
129
+ const ext = mimetype.split('/')[1] || 'bin';
130
+ filename = `file.${ext}`;
131
+ }
132
+ } else {
133
+ // Plain base64 string
134
+ buffer = Buffer.from(file, 'base64');
135
+ filename = filename || 'file.bin';
136
+ }
137
+ }
138
+ // Handle Buffer or Uint8Array
139
+ else if (file instanceof Buffer || file instanceof Uint8Array) {
140
+ buffer = file;
141
+ filename = filename || 'file.bin';
142
+ }
143
+ // Handle Node.js ReadableStream
144
+ else if (file && typeof file.pipe === 'function') {
145
+ throw new Error('Streams are not directly supported. Please convert to Buffer first using stream.toArray() or similar.');
146
+ }
147
+ // Handle other types (File, Blob, etc.) - pass through
148
+ else {
149
+ return { file, filename: filename || 'file' };
150
+ }
151
+
152
+ return { buffer, filename, mimetype };
153
+ }
154
+
155
+ /**
156
+ * Upload a single file to CDN
157
+ * Automatically handles token generation if not provided
158
+ * Supports: Buffer, Base64 strings, Multer file objects
159
+ *
160
+ * @param {Buffer|string|Object} file - File to upload
161
+ * - Buffer: Raw file buffer
162
+ * - string: Base64 encoded file or data URL (data:image/jpeg;base64,...)
163
+ * - Object: Multer file object ({ buffer, originalname, mimetype })
164
+ * @param {Object} options - Upload options
165
+ * @param {string} options.token - CDN token (optional, will auto-generate if not provided)
166
+ * @param {string} options.filename - Custom filename (optional)
167
+ * @param {string} options.mimetype - File mimetype (optional)
168
+ * @param {string} options.scope - Token scope to use: 'user' or 'admin' (default: 'user')
169
+ * @returns {Promise<Object>} Upload result with file URL and metadata
170
+ *
171
+ * @example
172
+ * // Upload from Buffer
173
+ * const fs = require('fs');
174
+ * const fileBuffer = fs.readFileSync('image.jpg');
175
+ * const result = await cdn.upload(fileBuffer, { filename: 'image.jpg' });
176
+ *
177
+ * @example
178
+ * // Upload from Base64
179
+ * const base64 = 'data:image/jpeg;base64,/9j/4AAQSkZJRg...';
180
+ * const result = await cdn.upload(base64, { filename: 'image.jpg' });
181
+ *
182
+ * @example
183
+ * // Upload from Base64 (plain)
184
+ * const base64 = '/9j/4AAQSkZJRg...';
185
+ * const result = await cdn.upload(base64, { filename: 'image.jpg' });
186
+ *
187
+ * @example
188
+ * // Upload from Multer (Express.js)
189
+ * app.post('/upload', upload.single('file'), async (req, res) => {
190
+ * const result = await cdn.upload(req.file);
191
+ * res.json(result);
192
+ * });
193
+ *
194
+ * // Returns:
195
+ * // {
196
+ * // success: true,
197
+ * // file: {
198
+ * // url: 'https://cdn.oblien.com/abc123/image.jpg',
199
+ * // filename: 'image.jpg',
200
+ * // size: 102400,
201
+ * // mimetype: 'image/jpeg'
202
+ * // }
203
+ * // }
204
+ */
205
+ async upload(file, options = {}) {
206
+ const { token, filename, mimetype, scope = 'user' } = options;
207
+
208
+ // Get token (use provided or generate new one)
209
+ const cdnToken = token || await this._getToken(scope);
210
+
211
+ // Normalize file input
212
+ const normalized = this._normalizeFile(file, filename);
213
+
214
+ // Create FormData
215
+ const FormData = globalThis.FormData || (await import('form-data')).default;
216
+ const formData = new FormData();
217
+
218
+ // Add file to form data
219
+ if (normalized.buffer) {
220
+ const Blob = globalThis.Blob || (await import('buffer')).Blob;
221
+ const fileBlob = new Blob([normalized.buffer], {
222
+ type: mimetype || normalized.mimetype || 'application/octet-stream'
223
+ });
224
+ formData.append('file', fileBlob, normalized.filename);
225
+ } else {
226
+ formData.append('file', normalized.file, normalized.filename);
227
+ }
228
+
229
+ // Upload to CDN
230
+ const response = await fetch(`${this.cdnURL}/`, {
231
+ method: 'POST',
232
+ headers: {
233
+ 'Authorization': `Bearer ${cdnToken}`
234
+ },
235
+ body: formData
236
+ });
237
+
238
+ if (!response.ok) {
239
+ const error = await response.json().catch(() => ({}));
240
+ throw new Error(error.message || `Upload failed: ${response.status}`);
241
+ }
242
+
243
+ return response.json();
244
+ }
245
+
246
+ /**
247
+ * Upload multiple files to CDN
248
+ * Automatically handles token generation if not provided
249
+ * Supports: Buffers, Base64 strings, Multer file objects
250
+ *
251
+ * @param {Array<Buffer|string|Object>} files - Array of files to upload
252
+ * - Buffer: Raw file buffers
253
+ * - string: Base64 encoded files
254
+ * - Object: Multer file objects
255
+ * @param {Object} options - Upload options
256
+ * @param {string} options.token - CDN token (optional, will auto-generate if not provided)
257
+ * @param {Array<string>} options.filenames - Array of custom filenames (optional)
258
+ * @param {string} options.scope - Token scope to use: 'user' or 'admin' (default: 'user')
259
+ * @returns {Promise<Object>} Upload result with array of file URLs and metadata
260
+ *
261
+ * @example
262
+ * // Upload multiple buffers
263
+ * const files = [
264
+ * fs.readFileSync('image1.jpg'),
265
+ * fs.readFileSync('image2.jpg')
266
+ * ];
267
+ * const result = await cdn.uploadMultiple(files, {
268
+ * filenames: ['image1.jpg', 'image2.jpg']
269
+ * });
270
+ *
271
+ * @example
272
+ * // Upload multiple base64 strings
273
+ * const files = [
274
+ * 'data:image/jpeg;base64,/9j/4AAQ...',
275
+ * 'data:image/png;base64,iVBORw0KGgo...'
276
+ * ];
277
+ * const result = await cdn.uploadMultiple(files);
278
+ *
279
+ * @example
280
+ * // Upload from Multer (Express.js)
281
+ * app.post('/upload', upload.array('files'), async (req, res) => {
282
+ * const result = await cdn.uploadMultiple(req.files);
283
+ * res.json(result);
284
+ * });
285
+ *
286
+ * // Returns:
287
+ * // {
288
+ * // success: true,
289
+ * // files: [
290
+ * // { url: 'https://cdn.oblien.com/abc123/image1.jpg', ... },
291
+ * // { url: 'https://cdn.oblien.com/def456/image2.jpg', ... }
292
+ * // ]
293
+ * // }
294
+ */
295
+ async uploadMultiple(files, options = {}) {
296
+ if (!Array.isArray(files) || files.length === 0) {
297
+ throw new Error('Files must be a non-empty array');
298
+ }
299
+
300
+ const { token, filenames = [], scope = 'user' } = options;
301
+
302
+ // Get token (use provided or generate new one)
303
+ const cdnToken = token || await this._getToken(scope);
304
+
305
+ // Create FormData
306
+ const FormData = globalThis.FormData || (await import('form-data')).default;
307
+ const formData = new FormData();
308
+
309
+ // Add all files
310
+ for (let i = 0; i < files.length; i++) {
311
+ const file = files[i];
312
+ const filename = filenames[i];
313
+
314
+ // Normalize file input
315
+ const normalized = this._normalizeFile(file, filename);
316
+
317
+ if (normalized.buffer) {
318
+ const Blob = globalThis.Blob || (await import('buffer')).Blob;
319
+ const fileBlob = new Blob([normalized.buffer], {
320
+ type: normalized.mimetype || 'application/octet-stream'
321
+ });
322
+ formData.append('files', fileBlob, normalized.filename);
323
+ } else {
324
+ formData.append('files', normalized.file, normalized.filename);
325
+ }
326
+ }
327
+
328
+ // Upload to CDN
329
+ const response = await fetch(`${this.cdnURL}/multiple`, {
330
+ method: 'POST',
331
+ headers: {
332
+ 'Authorization': `Bearer ${cdnToken}`
333
+ },
334
+ body: formData
335
+ });
336
+
337
+ if (!response.ok) {
338
+ const error = await response.json().catch(() => ({}));
339
+ throw new Error(error.message || `Upload failed: ${response.status}`);
340
+ }
341
+
342
+ return response.json();
343
+ }
344
+
345
+ /**
346
+ * Upload files from URLs (download and upload to CDN)
347
+ * CDN will download the images from URLs and process them
348
+ * Automatically handles token generation if not provided
349
+ *
350
+ * @param {Array<string>} urls - Array of image URLs to download and upload
351
+ * @param {Object} options - Upload options
352
+ * @param {string} options.token - CDN token (optional, will auto-generate if not provided)
353
+ * @param {string} options.scope - Token scope to use: 'user' or 'admin' (default: 'user')
354
+ * @returns {Promise<Object>} Upload result with processed file URLs
355
+ *
356
+ * @example
357
+ * const result = await cdn.uploadFromUrls([
358
+ * 'https://example.com/image1.jpg',
359
+ * 'https://example.com/image2.png'
360
+ * ]);
361
+ *
362
+ * // Returns:
363
+ * // {
364
+ * // success: true,
365
+ * // files: [
366
+ * // {
367
+ * // url: 'https://cdn.oblien.com/abc123/image1.jpg',
368
+ * // originalUrl: 'https://example.com/image1.jpg',
369
+ * // filename: 'image1.jpg',
370
+ * // size: 102400,
371
+ * // mimetype: 'image/jpeg',
372
+ * // variants: { ... }
373
+ * // },
374
+ * // {
375
+ * // url: 'https://cdn.oblien.com/def456/image2.png',
376
+ * // originalUrl: 'https://example.com/image2.png',
377
+ * // filename: 'image2.png',
378
+ * // size: 204800,
379
+ * // mimetype: 'image/png',
380
+ * // variants: { ... }
381
+ * // }
382
+ * // ]
383
+ * // }
384
+ */
385
+ async uploadFromUrls(urls, options = {}) {
386
+ if (!Array.isArray(urls) || urls.length === 0) {
387
+ throw new Error('URLs must be a non-empty array');
388
+ }
389
+
390
+ const { token, scope = 'user' } = options;
391
+
392
+ // Get token (use provided or generate new one)
393
+ const cdnToken = token || await this._getToken(scope);
394
+
395
+ // Upload from URLs
396
+ const response = await fetch(`${this.cdnURL}/process-urls`, {
397
+ method: 'POST',
398
+ headers: {
399
+ 'Authorization': `Bearer ${cdnToken}`,
400
+ 'Content-Type': 'application/json'
401
+ },
402
+ body: JSON.stringify({ urls })
403
+ });
404
+
405
+ if (!response.ok) {
406
+ const error = await response.json().catch(() => ({}));
407
+ throw new Error(error.message || `Upload from URLs failed: ${response.status}`);
408
+ }
409
+
410
+ return response.json();
411
+ }
412
+
413
+ /**
414
+ * @deprecated Use uploadFromUrls() instead
415
+ * Process URLs (download and process images from URLs)
416
+ */
417
+ async processUrls(urls, options = {}) {
418
+ console.warn('cdn.processUrls() is deprecated. Use cdn.uploadFromUrls() instead.');
419
+ return this.uploadFromUrls(urls, options);
420
+ }
421
+
422
+ /**
423
+ * Get file information (ADMIN ONLY)
424
+ * Requires admin token
425
+ *
426
+ * @param {string} filePath - File path on CDN
427
+ * @param {Object} options - Options
428
+ * @param {string} options.token - Admin CDN token (optional, will auto-generate if not provided)
429
+ * @returns {Promise<Object>} File information
430
+ *
431
+ * @example
432
+ * const info = await cdn.getFileInfo('abc123/image.jpg');
433
+ *
434
+ * // Returns:
435
+ * // {
436
+ * // success: true,
437
+ * // file: {
438
+ * // path: 'abc123/image.jpg',
439
+ * // size: 102400,
440
+ * // mimetype: 'image/jpeg',
441
+ * // created: '2024-01-01T00:00:00.000Z'
442
+ * // }
443
+ * // }
444
+ */
445
+ async getFileInfo(filePath, options = {}) {
446
+ if (!filePath) {
447
+ throw new Error('File path is required');
448
+ }
449
+
450
+ const { token } = options;
451
+
452
+ // Get admin token (use provided or generate new one)
453
+ const cdnToken = token || await this._getToken('admin');
454
+
455
+ // Get file info
456
+ const response = await fetch(`${this.cdnURL}/info/${filePath}`, {
457
+ method: 'GET',
458
+ headers: {
459
+ 'Authorization': `Bearer ${cdnToken}`
460
+ }
461
+ });
462
+
463
+ if (!response.ok) {
464
+ const error = await response.json().catch(() => ({}));
465
+ throw new Error(error.message || `Failed to get file info: ${response.status}`);
466
+ }
467
+
468
+ return response.json();
469
+ }
470
+
471
+ /**
472
+ * Get available image variant options
473
+ *
474
+ * @param {Object} options - Options
475
+ * @param {string} options.token - CDN token (optional, will auto-generate if not provided)
476
+ * @returns {Promise<Object>} Available variants
477
+ *
478
+ * @example
479
+ * const variants = await cdn.getVariants();
480
+ *
481
+ * // Returns:
482
+ * // {
483
+ * // variants: {
484
+ * // thumb: { width: 150, height: 150, fit: 'inside' },
485
+ * // medium: { width: 800, height: 600, fit: 'inside' },
486
+ * // blur: { width: 75, height: 75, fit: 'cover', blur: 5 }
487
+ * // }
488
+ * // }
489
+ */
490
+ async getVariants(options = {}) {
491
+ const { token } = options;
492
+
493
+ // Get token (use provided or generate new one)
494
+ const cdnToken = token || await this._getToken('user');
495
+
496
+ const response = await fetch(`${this.cdnURL}/variants`, {
497
+ method: 'GET',
498
+ headers: {
499
+ 'Authorization': `Bearer ${cdnToken}`
500
+ }
501
+ });
502
+
503
+ if (!response.ok) {
504
+ const error = await response.json().catch(() => ({}));
505
+ throw new Error(error.message || `Failed to get variants: ${response.status}`);
506
+ }
507
+
508
+ return response.json();
509
+ }
510
+
511
+ /**
512
+ * Get file size limits
513
+ *
514
+ * @param {Object} options - Options
515
+ * @param {string} options.token - CDN token (optional, will auto-generate if not provided)
516
+ * @returns {Promise<Object>} File size limits
517
+ *
518
+ * @example
519
+ * const limits = await cdn.getLimits();
520
+ *
521
+ * // Returns:
522
+ * // {
523
+ * // limits: {
524
+ * // 'image/jpeg': 5242880,
525
+ * // 'image/png': 8388608,
526
+ * // 'default': 10485760
527
+ * // }
528
+ * // }
529
+ */
530
+ async getLimits(options = {}) {
531
+ const { token } = options;
532
+
533
+ // Get token (use provided or generate new one)
534
+ const cdnToken = token || await this._getToken('user');
535
+
536
+ const response = await fetch(`${this.cdnURL}/limits`, {
537
+ method: 'GET',
538
+ headers: {
539
+ 'Authorization': `Bearer ${cdnToken}`
540
+ }
541
+ });
542
+
543
+ if (!response.ok) {
544
+ const error = await response.json().catch(() => ({}));
545
+ throw new Error(error.message || `Failed to get limits: ${response.status}`);
546
+ }
547
+
548
+ return response.json();
549
+ }
550
+
551
+ /**
552
+ * Clear token cache
553
+ * Useful when you want to force fresh token generation
554
+ *
555
+ * @param {string} scope - Scope to clear: 'user', 'admin', or 'all' (default: 'all')
556
+ *
557
+ * @example
558
+ * cdn.clearTokenCache(); // Clear all
559
+ * cdn.clearTokenCache('user'); // Clear user token only
560
+ */
561
+ clearTokenCache(scope = 'all') {
562
+ if (scope === 'all') {
563
+ this._tokenCache.user = null;
564
+ this._tokenCache.admin = null;
565
+ } else if (scope === 'user' || scope === 'admin') {
566
+ this._tokenCache[scope] = null;
567
+ }
568
+ }
569
+ }
570
+
package/src/chat/index.js CHANGED
@@ -95,7 +95,8 @@ export class OblienChat {
95
95
  namespace,
96
96
  endUserId,
97
97
  metadata = {},
98
- workspace
98
+ workspace,
99
+ useLocalStorage = false
99
100
  } = options;
100
101
 
101
102
  if (!ip) {
@@ -106,45 +107,35 @@ export class OblienChat {
106
107
  throw new Error('Agent ID is required for guest sessions');
107
108
  }
108
109
 
109
- // Get or create guest user (handles fingerprint and IP mapping internally)
110
- const guest = await this.guestManager.getOrCreateGuest(ip, fingerprint, {
111
- ...metadata,
112
- fingerprint,
113
- ip,
114
- });
115
-
116
- // Create session
110
+ // Create session via API (API will handle guest creation in Oblien database)
117
111
  const session = new ChatSession({
118
112
  client: this.client,
119
113
  });
120
114
 
121
- // Use custom namespace if provided, otherwise use guest's default namespace
122
- const sessionNamespace = namespace || guest.id;
123
-
124
115
  const sessionData = await session.create({
125
116
  agentId,
126
117
  workflowId,
127
118
  workspace,
128
119
  isGuest: true,
129
- namespace: sessionNamespace,
120
+ namespace: namespace,
130
121
  ipAddress: ip,
131
122
  userAgent: metadata.userAgent,
132
- guestNamespace: guest.namespace,
133
123
  fingerprint: fingerprint,
134
- endUserId: endUserId || guest.id, // Optional: Your end user identifier
124
+ endUserId: endUserId,
135
125
  });
136
126
 
137
- // Link session to guest
138
- await this.guestManager.addSession(guest.id, sessionData.sessionId);
127
+ // If using local storage, also store guest locally for caching
128
+ if (useLocalStorage) {
129
+ const guest = await this.guestManager.getOrCreateGuest(ip, fingerprint, {
130
+ ...metadata,
131
+ fingerprint,
132
+ ip,
133
+ });
134
+
135
+ await this.guestManager.addSession(guest.id, sessionData.sessionId);
136
+ }
139
137
 
140
- return {
141
- ...sessionData,
142
- guest: {
143
- id: guest.id,
144
- namespace: guest.namespace,
145
- createdAt: guest.createdAt,
146
- },
147
- };
138
+ return sessionData
148
139
  }
149
140
 
150
141
  /**
@@ -191,7 +182,7 @@ export class OblienChat {
191
182
  agent_id: agentId,
192
183
  session_id: sessionId,
193
184
  });
194
- return response.success ? response.guest : null;
185
+ return response;
195
186
  } catch (error) {
196
187
  // Return null if guest not found (404)
197
188
  if (error.message?.includes('not_found') || error.message?.includes('404')) {
@@ -541,6 +532,7 @@ export class OblienChat {
541
532
  * @param {string} [options.sortBy] - Sort by 'time' or 'tokens'
542
533
  * @param {string} [options.sortOrder] - 'asc' or 'desc'
543
534
  * @param {boolean} [options.includeStats] - Include message count and tokens
535
+ * @param {boolean} [options.includeGuests] - Include guest information
544
536
  * @returns {Promise<Array>} Array of sessions
545
537
  */
546
538
  async listSessions(options = {}) {
@@ -80,6 +80,7 @@ export class ChatSession {
80
80
  * @param {string} [options.search] - Search in title and user ID
81
81
  * @param {string} [options.sortBy] - Sort by 'time' or 'tokens'
82
82
  * @param {string} [options.sortOrder] - 'asc' or 'desc'
83
+ * @param {boolean} [options.includeGuests] - Include guest information
83
84
  * @param {boolean} [options.includeStats] - Include message count and tokens
84
85
  * @returns {Promise<Array>} Array of sessions
85
86
  */