bytex-sdk 1.8.0 → 1.8.1

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.
Files changed (2) hide show
  1. package/index.js +64 -745
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -3,17 +3,7 @@ import { createClient } from '@supabase/supabase-js';
3
3
  const SUPABASE_URL = 'https://vkiddclfbwmslaiyyftl.supabase.co';
4
4
  const SUPABASE_ANON_KEY = 'sb_publishable_hXMlH9OmJG1_n1s-3lbXKg_6V-88Lj9';
5
5
  const WORKER_URL = 'https://api.bytex.work/';
6
- const DEFAULT_ENC_KEY = 'btx-default-secret-key-32-chars!!';
7
- const DEFAULT_IV = 'btx-fixed-iv-16b';
8
6
 
9
- /**
10
- * ByteX Cloud SDK — Full-featured client for the ByteX Storage Platform.
11
- * Supports upload, download, streaming, encryption, bulk ops, and more.
12
- *
13
- * @example
14
- * const bytex = new BytexCloud({ apiKey: 'BTX-USER-XXXXXXXX' });
15
- * await bytex.upload('photo.jpg', fileData);
16
- */
17
7
  export class BytexCloud {
18
8
  constructor(config = {}) {
19
9
  this.apiKey = config.apiKey || null;
@@ -24,7 +14,6 @@ export class BytexCloud {
24
14
  const url = config.supabaseUrl || SUPABASE_URL;
25
15
  const key = config.supabaseKey || SUPABASE_ANON_KEY;
26
16
 
27
- // Add custom storage support for React Native
28
17
  const supabaseConfig = {
29
18
  auth: {
30
19
  persistSession: true,
@@ -37,800 +26,130 @@ export class BytexCloud {
37
26
  }
38
27
 
39
28
  this.supabase = createClient(url, key, supabaseConfig);
40
- } else if (this.provider === 'firebase') {
41
- this.fbProjectId = config.fbProjectId;
42
- this.fbApiKey = config.fbApiKey;
43
29
  }
44
-
45
30
  this.user = null;
46
31
  this._listeners = {};
47
- this._middlewares = [];
48
- this._projectScope = 'production';
49
- }
50
-
51
- /**
52
- * Dynamically update infrastructure settings (BYOC).
53
- * @param {object} config - { supabaseUrl, supabaseKey, workerUrl, dbProvider }
54
- */
55
- configure(config = {}) {
56
- if (config.workerUrl) this.workerUrl = config.workerUrl;
57
- if (config.dbProvider) this.provider = config.dbProvider;
58
-
59
- if (this.provider === 'supabase' && (config.supabaseUrl || config.supabaseKey)) {
60
- const url = config.supabaseUrl || this.supabase.supabaseUrl;
61
- const key = config.supabaseKey || this.supabase.supabaseKey;
62
- this.supabase = createClient(url, key);
63
- }
64
- console.log('[ByteX SDK] Configuration updated.');
65
32
  }
66
33
 
67
- // ─────────────────────────────────────────────
68
- // SECTION 1: AUTHENTICATION
69
- // ─────────────────────────────────────────────
70
-
71
34
  async login(email, password) {
72
- if (this.provider === 'firebase') {
73
- const res = await fetch(`https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=${this.fbApiKey}`, {
74
- method: 'POST',
75
- headers: { 'Content-Type': 'application/json' },
76
- body: JSON.stringify({ email, password, returnSecureToken: true })
77
- });
78
- const data = await res.json();
79
- if (data.error) throw new Error(`Login Failed: ${data.error.message}`);
80
- this.user = { id: data.localId, email: data.email };
81
- this.session = data.idToken;
82
- this._emit('login', this.user);
83
- return this.user;
84
- } else {
85
- const { data, error } = await this.supabase.auth.signInWithPassword({ email, password });
86
- if (error) throw new Error(`Login Failed: ${error.message}`);
87
- this.user = data.user;
88
- this._emit('login', this.user);
89
- return this.user;
90
- }
91
- }
92
-
93
- /**
94
- * Sign out and clear the current session.
95
- */
96
- async logout() {
97
- await this.supabase.auth.signOut();
98
- this.user = null;
99
- this.apiKey = null;
100
- this._emit('logout');
101
- }
102
-
103
- /**
104
- * Get the current user's profile from the database.
105
- * @returns {object|null} Profile data
106
- */
107
- async getProfile() {
108
- if (!this.user) throw new Error('You must login() first.');
109
- const { data, error } = await this.supabase.from('profiles').select('*').eq('id', this.user.id).single();
110
- if (error) throw error;
111
- return data;
112
- }
113
-
114
- // ─────────────────────────────────────────────
115
- // SECTION 2: API KEY MANAGEMENT
116
- // ─────────────────────────────────────────────
117
-
118
- /**
119
- * Set the active API key for storage operations.
120
- * @param {string} key - Your BTX-USER-XXXXXXXX key
121
- */
122
- setApiKey(key) {
123
- this.apiKey = key;
124
- }
125
-
126
- /**
127
- * List all API keys for the logged-in user.
128
- * @returns {Array<{label: string, key: string}>}
129
- */
130
- async getKeys() {
131
- if (!this.user) throw new Error('You must login() first.');
132
- if (this.provider === 'firebase') {
133
- const res = await fetch(`https://firestore.googleapis.com/v1/projects/${this.fbProjectId}/databases/(default)/documents/api_keys`);
134
- const data = await res.json();
135
- return (data.documents || []).map(d => ({
136
- label: d.fields.key_label?.stringValue,
137
- key: d.fields.api_key?.stringValue,
138
- createdAt: d.createTime
139
- }));
140
- } else {
141
- const { data, error } = await this.supabase.from('api_keys').select('*').eq('user_id', this.user.id);
142
- if (error) throw error;
143
- return data.map(k => ({ label: k.key_label, key: k.api_key, createdAt: k.created_at }));
144
- }
145
- }
146
-
147
- /**
148
- * Generate a new API key and auto-set it as active.
149
- * @param {string} label - A friendly project name
150
- * @returns {string} The new API key
151
- */
152
- async createKey(label) {
153
- if (!this.user) throw new Error('You must login() first.');
154
- const newKey = `BTX-USER-${_randomId()}`;
155
- if (this.provider === 'firebase') {
156
- await fetch(`https://firestore.googleapis.com/v1/projects/${this.fbProjectId}/databases/(default)/documents/api_keys?documentId=${newKey}`, {
157
- method: 'POST',
158
- headers: { 'Content-Type': 'application/json' },
159
- body: JSON.stringify({
160
- fields: {
161
- user_id: { stringValue: this.user.id },
162
- key_label: { stringValue: label },
163
- api_key: { stringValue: newKey }
164
- }
165
- })
166
- });
167
- } else {
168
- const { error } = await this.supabase.from('api_keys').insert([{
169
- user_id: this.user.id,
170
- key_label: label,
171
- api_key: newKey
172
- }]);
173
- if (error) throw error;
174
- }
175
- this.apiKey = newKey;
176
- return newKey;
177
- }
178
-
179
- /**
180
- * Delete a specific API key.
181
- * @param {string} apiKey - The key to revoke
182
- */
183
- async deleteKey(apiKey) {
184
- if (!this.user) throw new Error('You must login() first.');
185
- const { error } = await this.supabase.from('api_keys').delete().eq('user_id', this.user.id).eq('api_key', apiKey);
186
- if (error) throw error;
187
- if (this.apiKey === apiKey) this.apiKey = null;
188
- }
189
-
190
- /**
191
- * Delete a project: removes the API key AND all files uploaded under it.
192
- * @param {string} apiKey - The project key to destroy
193
- */
194
- async deleteProject(apiKey) {
195
- const files = await this.listFiles();
196
- for (const file of files) {
197
- try { await this._workerPost('delete', { fileName: file.file_name }); } catch (e) { /* skip */ }
198
- }
199
- await this.deleteKey(apiKey);
200
- }
201
-
202
- // ─────────────────────────────────────────────
203
- // SECTION 3: FILE UPLOAD
204
- // ─────────────────────────────────────────────
205
-
206
- /**
207
- * Set the project scope for isolation (e.g., 'development', 'staging', 'production')
208
- * @param {string} scope
209
- */
210
- setProjectScope(scope) {
211
- this._projectScope = scope;
212
- }
213
-
214
- /**
215
- * Register a middleware to intercept uploads and downloads.
216
- * @param {object} middleware - Object with beforeUpload / afterUpload methods.
217
- */
218
- use(middleware) {
219
- this._middlewares.push(middleware);
220
- }
221
-
222
- /**
223
- * Purge the global CDN cache for a specific file.
224
- * @param {string} fileName
225
- */
226
- async purgeCache(fileName) {
227
- this._requireKey();
228
- const res = await fetch(`${this.workerUrl}?action=purge&key=${this.apiKey}`, {
229
- method: 'POST',
230
- headers: { 'Content-Type': 'application/json' },
231
- body: JSON.stringify({ fileName })
232
- });
233
- if (!res.ok) throw new Error('Failed to purge CDN cache');
234
- return await res.json();
235
- }
236
-
237
- /**
238
- * Upload a file with default encryption (AES-CTR).
239
- * @param {string} name - Filename (e.g. 'video.mp4')
240
- * @param {Blob|ArrayBuffer|File} data - File content
241
- * @param {object} [options] - Optional: { thumbnail: Blob, title: string }
242
- * @returns {object} { success: boolean }
243
- */
244
- async upload(name, data, options = {}) {
245
- this._requireKey();
246
-
247
- // Execute beforeUpload Middlewares
248
- let payload = { name, data, options, scope: this._projectScope };
249
- for (const mw of this._middlewares) {
250
- if (typeof mw.beforeUpload === 'function') {
251
- payload = await mw.beforeUpload(payload) || payload;
252
- }
253
- }
254
-
255
- // Apply scope prefix if not production
256
- const finalName = payload.scope !== 'production' ? `[${payload.scope}]_${payload.name}` : payload.name;
257
-
258
- const fd = new FormData();
259
- const blob = payload.data instanceof Blob ? payload.data : new Blob([payload.data]);
260
- fd.append('file', blob, finalName);
261
- if (options.thumbnail) {
262
- const thumbBlob = options.thumbnail instanceof Blob ? options.thumbnail : new Blob([options.thumbnail]);
263
- fd.append('thumbnail', thumbBlob, `thumb_${name}`);
264
- }
265
- if (options.title) fd.append('title', options.title);
266
- if (options.password) fd.append('password', options.password);
267
-
268
- const res = await fetch(`${this.workerUrl}?action=upload&key=${this.apiKey}`, {
269
- method: 'POST',
270
- body: fd
271
- });
272
- if (!res.ok) {
273
- const errText = await res.text();
274
- this._emit('error', { action: 'upload', message: errText });
275
- throw new Error(`Upload failed: ${errText}`);
276
- }
277
- const result = await res.json();
278
- this._emit('upload', { name, size: blob.size });
279
- return result;
280
- }
281
-
282
- /**
283
- * Pro Feature: Chunked Upload for very large files.
284
- * Splits file into 5MB chunks for better reliability.
285
- * @param {string} name
286
- * @param {File|Blob} file
287
- * @param {object} [options]
288
- */
289
- async uploadLarge(name, file, options = {}) {
290
- this._requireKey();
291
- const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB
292
- const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
293
-
294
- console.log(`[ByteX] 🚀 Starting Chunked Upload for ${name} (${totalChunks} chunks)`);
295
-
296
- // For now, we perform a simplified sequential upload or a single multi-part
297
- // In a full TUS implementation, we would send chunks one by one.
298
- // For ByteX, we will use the standard upload but with optimized buffer handling.
299
- return this.upload(name, file, options);
35
+ const { data, error } = await this.supabase.auth.signInWithPassword({ email, password });
36
+ if (error) throw new Error(`Login Failed: ${error.message}`);
37
+ this.user = data.user;
38
+ this._emit('login', this.user);
39
+ return data.user;
300
40
  }
301
41
 
302
-
303
- /**
304
- * Upload a file WITHOUT encryption (raw/plain).
305
- * Note: This sends the file directly without AES-CTR processing.
306
- * The file will still be committed to your storage node as-is.
307
- * @param {string} name
308
- * @param {Blob|ArrayBuffer|File} data
309
- * @returns {object}
310
- */
311
- async uploadRaw(name, data) {
42
+ async upload(name, data) {
312
43
  this._requireKey();
313
44
  const fd = new FormData();
314
45
  const blob = data instanceof Blob ? data : new Blob([data]);
315
46
  fd.append('file', blob, name);
316
- fd.append('raw', 'true');
317
47
 
318
- const res = await fetch(`${this.workerUrl}?action=upload&key=${this.apiKey}&raw=true`, {
319
- method: 'POST',
320
- body: fd
321
- });
322
- if (!res.ok) throw new Error(`Raw upload failed: ${await res.text()}`);
323
- return await res.json();
324
- }
325
-
326
- /**
327
- * Upload a file with a CUSTOM encryption key (Bring Your Own Key).
328
- * @param {string} name
329
- * @param {Blob|ArrayBuffer|File} data
330
- * @param {string} encryptionKey - Your custom 32-char encryption key
331
- * @returns {object}
332
- */
333
- async uploadEncrypted(name, data, encryptionKey) {
334
- this._requireKey();
335
- const rawData = data instanceof Blob ? await data.arrayBuffer() : (data instanceof ArrayBuffer ? data : new Blob([data]).arrayBuffer());
336
- const actualData = rawData instanceof Promise ? await rawData : rawData;
337
- const encrypted = await this.encrypt(actualData, encryptionKey);
338
- const fd = new FormData();
339
- fd.append('file', new Blob([encrypted]), name);
340
- fd.append('custom_enc', 'true');
341
-
342
- const res = await fetch(`${this.workerUrl}?action=upload&key=${this.apiKey}&custom_enc=true`, {
48
+ const res = await fetch(`${this.workerUrl}?action=upload&key=${this.apiKey}`, {
343
49
  method: 'POST',
344
50
  body: fd
345
51
  });
346
- if (!res.ok) throw new Error(`Encrypted upload failed: ${await res.text()}`);
52
+ if (!res.ok) throw new Error(`Upload failed: ${await res.text()}`);
347
53
  return await res.json();
348
54
  }
349
55
 
350
- // ─────────────────────────────────────────────
351
- // SECTION 4: FILE RETRIEVAL & STREAMING
352
- // ─────────────────────────────────────────────
353
-
354
- /**
355
- * Download a file (auto-decrypts if encrypted).
356
- * @param {string} name - Filename (with or without .stream.btx)
357
- * @param {string} [password] - Password if the file is protected
358
- * @returns {Blob} Decrypted file data
359
- */
360
- async download(name, password) {
56
+ async downloadData(name) {
361
57
  this._requireKey();
362
- // ByteX Worker always expects .stream.btx for action=view
363
58
  const streamName = name.endsWith('.stream.btx') ? name : `${name}.stream.btx`;
364
- let url = `${this.workerUrl}?action=view&key=${this.apiKey}&file=${encodeURIComponent(streamName)}&_cb=${Date.now()}`;
365
- if (password) url += `&password=${encodeURIComponent(password)}`;
59
+ const url = `${this.workerUrl}?action=view&key=${this.apiKey}&file=${encodeURIComponent(streamName)}&_cb=${Date.now()}`;
366
60
 
367
61
  const { data: { session } } = await this.supabase.auth.getSession();
368
62
  const res = await fetch(url, {
369
63
  headers: {
370
64
  'Authorization': `Bearer ${session?.access_token || ''}`,
371
- 'Cache-Control': 'no-cache',
372
- 'Pragma': 'no-cache'
65
+ 'Cache-Control': 'no-cache'
373
66
  }
374
67
  });
375
68
  if (!res.ok) throw new Error(`Download failed: ${res.status}`);
376
- return await res.blob();
377
- }
378
-
379
- /**
380
- * Helper to download JSON or NDJSON data directly from cloud.
381
- * Compatible with React Native (uses FileReader).
382
- * @param {string} name
383
- * @returns {object|Array}
384
- */
385
- async downloadData(name) {
386
- const blob = await this.download(name);
387
-
388
- return new Promise((resolve, reject) => {
389
- const reader = new FileReader();
390
- reader.onload = () => {
391
- const text = reader.result;
392
- try {
393
- // Detect NDJSON (multiple lines of JSON)
394
- if (text.includes('\n') && text.trim().startsWith('{')) {
395
- const lines = text.trim().split('\n');
396
- resolve(lines.map(l => JSON.parse(l)));
397
- } else {
398
- resolve(JSON.parse(text));
399
- }
400
- } catch (e) {
401
- reject(new Error("Failed to parse Cloud Data (Invalid JSON/NDJSON)"));
402
- }
403
- };
404
- reader.onerror = () => reject(new Error("Failed to read Cloud Blob"));
405
- reader.readAsText(blob);
406
- });
69
+ const text = await res.text();
70
+ try {
71
+ return JSON.parse(text);
72
+ } catch (e) {
73
+ // Handle NDJSON
74
+ return text.split('\n').filter(Boolean).map(line => JSON.parse(line));
75
+ }
407
76
  }
408
77
 
409
- /**
410
- * Get the streaming URL for a media file (video/audio).
411
- * Supports precision seeking with range requests.
412
- * @param {string} name - Original filename (e.g. 'movie.mp4')
413
- * @param {string} [password] - Password if the file is protected
414
- * @returns {string} Full streaming URL
415
- */
416
- stream(name, password) {
78
+ async delete(name) {
417
79
  this._requireKey();
418
80
  const streamName = name.endsWith('.stream.btx') ? name : `${name}.stream.btx`;
419
- let url = `${this.workerUrl}?action=view&key=${this.apiKey}&file=${encodeURIComponent(streamName)}`;
420
- if (password) url += `&password=${encodeURIComponent(password)}`;
421
- return url;
81
+ const { data: { session } } = await this.supabase.auth.getSession();
82
+ const res = await fetch(`${this.workerUrl}?action=delete&key=${this.apiKey}&file=${encodeURIComponent(streamName)}`, {
83
+ method: 'DELETE',
84
+ headers: {
85
+ 'Authorization': `Bearer ${session?.access_token || ''}`
86
+ }
87
+ });
88
+ if (!res.ok) throw new Error(`Delete failed: ${res.status}`);
89
+ return { success: true };
422
90
  }
423
91
 
424
- // ─────────────────────────────────────────────
425
- // SECTION 5: FILE MANAGEMENT
426
- // ─────────────────────────────────────────────
427
-
428
- /**
429
- * List all uploaded files with metadata.
430
- * @returns {Array<{file_name, path, title, thumbnail, size, uploaded_at}>}
431
- */
432
92
  async listFiles() {
433
93
  this._requireKey();
434
94
  const res = await fetch(`${this.workerUrl}?action=list&key=${this.apiKey}&cb=${Date.now()}`);
435
95
  if (!res.ok) throw new Error(`List failed: ${res.status}`);
436
96
  const text = await res.text();
437
- if (!text || text.trim() === '' || text.trim() === '{}') return [];
438
- return text.trim().split('\n').filter(l => l.trim() !== '' && l.trim() !== '{}').map(line => {
439
- try { return JSON.parse(line); } catch (e) { return null; }
440
- }).filter(Boolean);
97
+ return text.split('\n').filter(Boolean).map(line => JSON.parse(line));
441
98
  }
442
99
 
443
100
  /**
444
- * Get detailed info/metadata for a specific file.
445
- * @param {string} name - The filename to look up
446
- * @returns {object|null} File metadata or null
101
+ * ByteX X-RAY: Full system diagnostic.
447
102
  */
448
- async getFileInfo(name) {
449
- const files = await this.listFiles();
450
- const baseName = name.replace(/\.stream\.btx$/i, '').replace(/\.btx$/i, '');
451
- return files.find(f => {
452
- const fBase = (f.file_name || f.path || '').replace(/\.stream\.btx$/i, '').replace(/\.btx$/i, '');
453
- return fBase === baseName || fBase === name;
454
- }) || null;
455
- }
456
-
457
- /**
458
- * Search files by name or title.
459
- * @param {string} query - Search query (case-insensitive)
460
- * @returns {Array} Matching files
461
- */
462
- async search(query) {
463
- const files = await this.listFiles();
464
- const q = query.toLowerCase();
465
- return files.filter(f =>
466
- (f.file_name || '').toLowerCase().includes(q) ||
467
- (f.title || '').toLowerCase().includes(q)
468
- );
469
- }
470
-
471
- /**
472
- * Delete a single file permanently.
473
- * @param {string} name - Filename to delete
474
- * @returns {object} { success: boolean }
475
- */
476
- async delete(name) {
477
- this._requireKey();
478
- const streamName = name.endsWith('.stream.btx') ? name : `${name}.stream.btx`;
479
- const result = await this._workerPost('delete', { fileName: streamName });
480
- this._emit('delete', { name });
481
- return result;
482
- }
483
-
484
- /**
485
- * Delete ALL files in your storage. Use with caution!
486
- * @returns {{ deleted: number, errors: number }}
487
- */
488
- async deleteAll() {
489
- const files = await this.listFiles();
490
- let deleted = 0, errors = 0;
491
- for (const file of files) {
492
- try {
493
- await this._workerPost('delete', { fileName: file.file_name || file.path });
494
- deleted++;
495
- } catch (e) { errors++; }
496
- }
497
- return { deleted, errors };
498
- }
499
-
500
- /**
501
- * Rename a file (delete old + re-upload metadata).
502
- * @param {string} oldName
503
- * @param {string} newName
504
- */
505
- async rename(oldName, newName) {
506
- this._requireKey();
507
- const fileData = await this.download(oldName);
508
- await this.delete(oldName);
509
- await this.upload(newName, fileData);
510
- }
511
-
512
- // ─────────────────────────────────────────────
513
- // SECTION 6: BULK & EXPORT OPERATIONS
514
- // ─────────────────────────────────────────────
515
-
516
- /**
517
- * Download ALL files and return as a map of { filename: Blob }.
518
- * For ZIP export in browser, use exportZip() instead.
519
- * @returns {Object<string, Blob>}
520
- */
521
- async downloadAll() {
522
- const files = await this.listFiles();
523
- const result = {};
524
- for (const file of files) {
525
- try {
526
- const blob = await this.download(file.file_name || file.path);
527
- const cleanName = (file.file_name || file.path || 'file').replace(/\.stream\.btx$/i, '');
528
- result[cleanName] = blob;
529
- } catch (e) { /* skip failed downloads */ }
530
- }
531
- return result;
532
- }
533
-
534
- /**
535
- * Export ALL files as a single ZIP download (browser only).
536
- * Requires JSZip to be available globally or passed in options.
537
- * @param {object} [options] - { JSZip: JSZipConstructor, zipName: 'bytex-export.zip' }
538
- * @returns {Blob} ZIP file blob
539
- */
540
- /**
541
- * Export files as a single ZIP download (browser only).
542
- * @param {object} [options] - { projectLabel, JSZip, zipName }
543
- * @returns {Blob} ZIP file blob
544
- */
545
- async exportZip(options = {}) {
546
- const JSZipLib = options.JSZip || (typeof globalThis !== 'undefined' && globalThis.JSZip);
547
- if (!JSZipLib) throw new Error('exportZip requires JSZip.');
548
-
549
- const zip = new JSZipLib();
550
- const files = await this.listFiles();
551
-
552
- // Filter by project label if provided
553
- const filteredFiles = options.projectLabel
554
- ? files.filter(f => f.key_label === options.projectLabel)
555
- : files;
103
+ async xray() {
104
+ console.log('[ByteX X-RAY] 🔍 Starting diagnostic scan...');
105
+ const report = {
106
+ timestamp: new Date().toISOString(),
107
+ sdk_version: '1.8.1',
108
+ checks: {}
109
+ };
556
110
 
557
- for (const file of filteredFiles) {
558
- try {
559
- const blob = await this.download(file.file_name || file.path);
560
- const name = (file.file_name || file.path || 'file').replace(/\.stream\.btx$/i, '');
561
- zip.file(name, blob);
562
- } catch (e) { console.warn(`Failed to include ${file.file_name} in ZIP`); }
563
- }
564
-
565
- const zipBlob = await zip.generateAsync({ type: 'blob' });
566
-
567
- if (typeof document !== 'undefined') {
568
- const a = document.createElement('a');
569
- a.href = URL.createObjectURL(zipBlob);
570
- a.download = options.zipName || (options.projectLabel ? `bytex-${options.projectLabel}.zip` : 'bytex-export.zip');
571
- a.click();
572
- URL.revokeObjectURL(a.href);
111
+ try {
112
+ this._requireKey();
113
+ report.checks.api_key = { status: 'OK', key: `${this.apiKey.substring(0, 8)}...` };
114
+ } catch (e) {
115
+ report.checks.api_key = { status: 'ERROR', message: e.message };
573
116
  }
574
- return zipBlob;
575
- }
576
-
577
- // ─────────────────────────────────────────────
578
- // SECTION 7: QUOTA & MONITORING
579
- // ─────────────────────────────────────────────
580
-
581
- /**
582
- * Get current storage usage and limits.
583
- * @returns {{ usedBytes, limitBytes, usedGB, limitGB, percentage }}
584
- */
585
- async getQuota() {
586
- this._requireKey();
587
- const res = await fetch(`${this.workerUrl}?action=quota`, {
588
- headers: { 'x-bytex-key': this.apiKey }
589
- });
590
- if (!res.ok) throw new Error(`Quota check failed: ${res.status}`);
591
- const data = await res.json();
592
- const usedGB = (data.usedBytes / (1024 ** 3)).toFixed(2);
593
- const limitGB = data.limitBytes > 0 ? (data.limitBytes / (1024 ** 3)).toFixed(0) : 'Unlimited';
594
- const percentage = data.limitBytes > 0 ? ((data.usedBytes / data.limitBytes) * 100).toFixed(1) : 0;
595
-
596
- if (percentage > 90) this._emit('quota-warn', { percentage, usedGB, limitGB });
597
-
598
- return { ...data, usedGB, limitGB, percentage };
599
- }
600
117
 
601
- /**
602
- * Get detailed analytics for the current API Key.
603
- * @returns {Promise<{download_count, bandwidth_used_gb}>}
604
- */
605
- async getAnalytics() {
606
- this._requireKey();
607
- const { data, error } = await this.supabase
608
- .from('api_keys')
609
- .select('download_count, bandwidth_used')
610
- .eq('api_key', this.apiKey)
611
- .single();
612
-
613
- if (error) throw error;
614
- return {
615
- downloadCount: data.download_count || 0,
616
- bandwidthUsedGB: ((data.bandwidth_used || 0) / (1024 ** 3)).toFixed(4)
118
+ const { data: { session } } = await this.supabase.auth.getSession();
119
+ report.checks.auth = {
120
+ status: session ? 'LOGGED_IN' : 'GUEST',
121
+ user: session?.user?.email || 'none'
617
122
  };
618
- }
619
-
620
-
621
- // ─────────────────────────────────────────────
622
- // SECTION 8: ENCRYPTION UTILITIES
623
- // ─────────────────────────────────────────────
624
123
 
625
- /**
626
- * Encrypt raw data using AES-CTR.
627
- * @param {ArrayBuffer} data - Raw data to encrypt
628
- * @param {string} [key] - 32-char encryption key (defaults to ByteX standard)
629
- * @returns {ArrayBuffer} Encrypted data
630
- */
631
- async encrypt(data, key) {
632
- const encoder = new TextEncoder();
633
- const kb = encoder.encode(key || DEFAULT_ENC_KEY).slice(0, 32);
634
- const iv = encoder.encode(DEFAULT_IV).slice(0, 16);
635
- const cryptoKey = await crypto.subtle.importKey('raw', kb, 'AES-CTR', false, ['encrypt']);
636
- return await crypto.subtle.encrypt({ name: 'AES-CTR', counter: iv, length: 64 }, cryptoKey, data);
637
- }
638
-
639
- /**
640
- * Decrypt AES-CTR encrypted data.
641
- * @param {ArrayBuffer} data - Encrypted data
642
- * @param {string} [key] - 32-char encryption key (defaults to ByteX standard)
643
- * @returns {ArrayBuffer} Decrypted data
644
- */
645
- async decrypt(data, key) {
646
- const encoder = new TextEncoder();
647
- const kb = encoder.encode(key || DEFAULT_ENC_KEY).slice(0, 32);
648
- const iv = encoder.encode(DEFAULT_IV).slice(0, 16);
649
- const cryptoKey = await crypto.subtle.importKey('raw', kb, 'AES-CTR', false, ['decrypt']);
650
- return await crypto.subtle.decrypt({ name: 'AES-CTR', counter: iv, length: 64 }, cryptoKey, data);
651
- }
652
-
653
- // ─────────────────────────────────────────────
654
- // SECTION 9: SHARING
655
- // ─────────────────────────────────────────────
656
-
657
- /**
658
- * Generate a public shareable URL for a file.
659
- * @param {string} name - Filename to share
660
- * @param {string} [password] - Optional password protection
661
- * @returns {string} Public URL
662
- */
663
- share(name, password) {
664
- return this.stream(name, password);
665
- }
124
+ const start = Date.now();
125
+ try {
126
+ const res = await fetch(`${this.workerUrl}?action=list&key=${this.apiKey}`);
127
+ report.checks.worker = {
128
+ status: res.ok ? 'ONLINE' : 'ERROR',
129
+ latency_ms: Date.now() - start
130
+ };
131
+ } catch (e) {
132
+ report.checks.worker = { status: 'OFFLINE', message: e.message };
133
+ }
666
134
 
667
- /**
668
- * Generate a time-limited signed URL.
669
- * @param {string} name - Filename to share
670
- * @param {string} duration - Duration string (e.g. '1h', '2d')
671
- * @param {string} [password] - Optional password
672
- * @returns {Promise<string>} Signed URL
673
- */
674
- async shareSigned(name, duration, password) {
675
- this._requireKey();
676
- const streamName = name.endsWith('.stream.btx') ? name : `${name}.stream.btx`;
677
-
678
- // Parse duration (very basic)
679
- let ms = 3600000; // default 1h
680
- if (duration.endsWith('h')) ms = parseInt(duration) * 3600000;
681
- else if (duration.endsWith('d')) ms = parseInt(duration) * 86400000;
682
- else if (duration.endsWith('m')) ms = parseInt(duration) * 60000;
135
+ const issues = Object.values(report.checks).filter(c => c.status === 'ERROR' || c.status === 'OFFLINE');
136
+ report.health_score = issues.length === 0 ? 'EXCELLENT' : 'CRITICAL';
683
137
 
684
- const expires = Date.now() + ms;
685
- const secret = "btx-link-signing-secret"; // Must match worker secret
686
-
687
- // Generate HMAC signature
688
- const encoder = new TextEncoder();
689
- const data = encoder.encode(`${streamName}:${expires}`);
690
- const key = await crypto.subtle.importKey("raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
691
- const signatureBuffer = await crypto.subtle.sign("HMAC", key, data);
692
- const signature = Array.from(new Uint8Array(signatureBuffer)).map(b => b.toString(16).padStart(2, "0")).join("");
693
-
694
- let url = `${this.workerUrl}?action=view&key=${this.apiKey}&file=${encodeURIComponent(streamName)}&expires=${expires}&signature=${signature}`;
695
- if (password) url += `&password=${encodeURIComponent(password)}`;
696
- return url;
138
+ return report;
697
139
  }
698
140
 
699
- // ─────────────────────────────────────────────
700
- // SECTION 10: EVENT SYSTEM
701
- // ─────────────────────────────────────────────
702
-
703
- /**
704
- * Listen to SDK events.
705
- * Supported events: 'login', 'logout', 'upload', 'delete', 'error', 'quota-warn'
706
- * @param {string} event
707
- * @param {Function} callback
708
- */
709
- on(event, callback) {
710
- if (!this._listeners[event]) this._listeners[event] = [];
711
- this._listeners[event].push(callback);
712
- }
713
-
714
- /**
715
- * Remove an event listener.
716
- * @param {string} event
717
- * @param {Function} callback
718
- */
719
- off(event, callback) {
720
- if (!this._listeners[event]) return;
721
- this._listeners[event] = this._listeners[event].filter(cb => cb !== callback);
722
- }
723
-
724
- // ─────────────────────────────────────────────
725
- // INTERNAL HELPERS
726
- // ─────────────────────────────────────────────
727
-
728
141
  _requireKey() {
729
- if (!this.apiKey) throw new Error('API Key required! Call setApiKey() or createKey() first.');
142
+ if (!this.apiKey) throw new Error('API Key required!');
730
143
  }
731
144
 
732
145
  _emit(event, data) {
733
146
  if (this._listeners[event]) {
734
- this._listeners[event].forEach(cb => { try { cb(data); } catch (e) {} });
735
- }
736
- }
737
-
738
- async _workerPost(action, body) {
739
- const { data: { session } } = await this.supabase.auth.getSession();
740
- const res = await fetch(`${this.workerUrl}?action=${action}&key=${this.apiKey}`, {
741
- method: 'POST',
742
- headers: {
743
- 'Content-Type': 'application/json',
744
- 'Authorization': `Bearer ${session?.access_token || ''}`
745
- },
746
- body: JSON.stringify(body)
747
- });
748
- if (!res.ok) {
749
- const errText = await res.text();
750
- this._emit('error', { action, message: errText });
751
- throw new Error(`${action} failed: ${errText}`);
147
+ this._listeners[event].forEach(cb => cb(data));
752
148
  }
753
- return await res.json();
754
- }
755
- }
756
-
757
- /**
758
- * BytexWeb — Manage and deploy websites to Cloudflare Pages.
759
- */
760
- export class BytexWeb {
761
- constructor(config = {}) {
762
- this.cfToken = config.cfToken || null;
763
- this.cfAccountId = config.cfAccountId || null;
764
149
  }
765
150
 
766
- /**
767
- * Deploy a static directory to Cloudflare Pages.
768
- * @param {string} projectName
769
- * @param {string} directory - Path to build files
770
- * @returns {Promise<object>} Deployment info
771
- */
772
- async deploy(projectName, directory) {
773
- if (!this.cfToken || !this.cfAccountId) throw new Error('Cloudflare credentials required.');
774
-
775
- // 1. Create project if not exists
776
- await fetch(`https://api.cloudflare.com/client/v4/accounts/${this.cfAccountId}/pages/projects`, {
777
- method: 'POST',
778
- headers: { 'Authorization': `Bearer ${this.cfToken}`, 'Content-Type': 'application/json' },
779
- body: JSON.stringify({ name: projectName, production_branch: 'main' })
780
- }).catch(() => {}); // ignore error if already exists
781
-
782
- // 2. Deployment via direct upload requires zipping or multipart
783
- // For the SDK (browser/node), we'll provide the instructions or use a helper.
784
- // In the CLI, we will use a more robust method.
785
- return { success: true, url: `https://${projectName}.pages.dev` };
786
- }
787
-
788
- /**
789
- * Bind a custom domain to a Pages project.
790
- * @param {string} projectName
791
- * @param {string} domain
792
- */
793
- async addDomain(projectName, domain) {
794
- const res = await fetch(`https://api.cloudflare.com/client/v4/accounts/${this.cfAccountId}/pages/projects/${projectName}/domains`, {
795
- method: 'POST',
796
- headers: { 'Authorization': `Bearer ${this.cfToken}`, 'Content-Type': 'application/json' },
797
- body: JSON.stringify({ name: domain })
798
- });
799
- return await res.json();
151
+ on(event, callback) {
152
+ if (!this._listeners[event]) this._listeners[event] = [];
153
+ this._listeners[event].push(callback);
800
154
  }
801
155
  }
802
-
803
- export const BytexMiddlewares = {
804
- /**
805
- * Middleware to automatically optimize images to WebP in the browser before upload.
806
- * @param {number} quality - WebP quality (0.0 to 1.0)
807
- */
808
- imageOptimizer: (quality = 0.8) => ({
809
- beforeUpload: async (payload) => {
810
- // Only run in browser environment and if the file is an image
811
- if (typeof document === 'undefined' || !payload.name.match(/\.(jpg|jpeg|png)$/i)) return payload;
812
-
813
- try {
814
- const file = payload.data;
815
- const bitmap = await createImageBitmap(file);
816
- const canvas = document.createElement('canvas');
817
- canvas.width = bitmap.width;
818
- canvas.height = bitmap.height;
819
- const ctx = canvas.getContext('2d');
820
- ctx.drawImage(bitmap, 0, 0);
821
-
822
- const optimizedBlob = await new Promise(resolve => canvas.toBlob(resolve, 'image/webp', quality));
823
- payload.data = optimizedBlob;
824
- payload.name = payload.name.replace(/\.(jpg|jpeg|png)$/i, '.webp');
825
- console.log(`[ByteX SDK] Optimized image to WebP: ${payload.name}`);
826
- } catch (e) {
827
- console.warn('[ByteX SDK] Image optimization failed, skipping:', e);
828
- }
829
- return payload;
830
- }
831
- })
832
- };
833
-
834
- function _randomId() {
835
- return Math.random().toString(36).substring(2, 10).toUpperCase();
836
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bytex-sdk",
3
- "version": "1.8.0",
3
+ "version": "1.8.1",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {