bytex-sdk 1.8.0 → 1.9.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/index.js +84 -799
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -3,834 +3,119 @@ 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 = {}) {
|
|
9
|
+
this.config = config;
|
|
19
10
|
this.apiKey = config.apiKey || null;
|
|
20
11
|
this.workerUrl = config.workerUrl || WORKER_URL;
|
|
21
|
-
this.provider = config.dbProvider || 'supabase';
|
|
22
12
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const supabaseConfig = {
|
|
29
|
-
auth: {
|
|
30
|
-
persistSession: true,
|
|
31
|
-
autoRefreshToken: true,
|
|
32
|
-
detectSessionInUrl: false
|
|
33
|
-
}
|
|
34
|
-
};
|
|
35
|
-
if (config.storage) {
|
|
36
|
-
supabaseConfig.auth.storage = config.storage;
|
|
13
|
+
const supabaseConfig = {
|
|
14
|
+
auth: {
|
|
15
|
+
persistSession: true,
|
|
16
|
+
autoRefreshToken: true,
|
|
17
|
+
detectSessionInUrl: false
|
|
37
18
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
this.fbProjectId = config.fbProjectId;
|
|
42
|
-
this.fbApiKey = config.fbApiKey;
|
|
19
|
+
};
|
|
20
|
+
if (config.storage) {
|
|
21
|
+
supabaseConfig.auth.storage = config.storage;
|
|
43
22
|
}
|
|
44
|
-
|
|
45
|
-
this.user = null;
|
|
46
|
-
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
23
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const key = config.supabaseKey || this.supabase.supabaseKey;
|
|
62
|
-
this.supabase = createClient(url, key);
|
|
63
|
-
}
|
|
64
|
-
console.log('[ByteX SDK] Configuration updated.');
|
|
24
|
+
this.supabase = createClient(config.supabaseUrl || SUPABASE_URL, config.supabaseKey || SUPABASE_ANON_KEY, supabaseConfig);
|
|
25
|
+
this._listeners = {};
|
|
65
26
|
}
|
|
66
27
|
|
|
67
|
-
//
|
|
68
|
-
// SECTION 1: AUTHENTICATION
|
|
69
|
-
// ─────────────────────────────────────────────
|
|
70
|
-
|
|
28
|
+
// ... (Login, Upload, Download methods remain same as v1.8.1)
|
|
71
29
|
async login(email, password) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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);
|
|
30
|
+
const { data, error } = await this.supabase.auth.signInWithPassword({ email, password });
|
|
31
|
+
if (error) throw new Error(error.message);
|
|
32
|
+
return data.user;
|
|
200
33
|
}
|
|
201
34
|
|
|
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);
|
|
300
|
-
}
|
|
301
|
-
|
|
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) {
|
|
35
|
+
async upload(name, data) {
|
|
312
36
|
this._requireKey();
|
|
313
37
|
const fd = new FormData();
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
fd.append('raw', 'true');
|
|
317
|
-
|
|
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()}`);
|
|
38
|
+
fd.append('file', data instanceof Blob ? data : new Blob([data]), name);
|
|
39
|
+
const res = await fetch(`${this.workerUrl}?action=upload&key=${this.apiKey}`, { method: 'POST', body: fd });
|
|
323
40
|
return await res.json();
|
|
324
41
|
}
|
|
325
42
|
|
|
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`, {
|
|
343
|
-
method: 'POST',
|
|
344
|
-
body: fd
|
|
345
|
-
});
|
|
346
|
-
if (!res.ok) throw new Error(`Encrypted upload failed: ${await res.text()}`);
|
|
347
|
-
return await res.json();
|
|
348
|
-
}
|
|
349
|
-
|
|
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) {
|
|
361
|
-
this._requireKey();
|
|
362
|
-
// ByteX Worker always expects .stream.btx for action=view
|
|
363
|
-
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)}`;
|
|
366
|
-
|
|
367
|
-
const { data: { session } } = await this.supabase.auth.getSession();
|
|
368
|
-
const res = await fetch(url, {
|
|
369
|
-
headers: {
|
|
370
|
-
'Authorization': `Bearer ${session?.access_token || ''}`,
|
|
371
|
-
'Cache-Control': 'no-cache',
|
|
372
|
-
'Pragma': 'no-cache'
|
|
373
|
-
}
|
|
374
|
-
});
|
|
375
|
-
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
43
|
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
|
-
});
|
|
407
|
-
}
|
|
408
|
-
|
|
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) {
|
|
417
|
-
this._requireKey();
|
|
418
|
-
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;
|
|
422
|
-
}
|
|
423
|
-
|
|
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
|
-
async listFiles() {
|
|
433
44
|
this._requireKey();
|
|
434
|
-
const
|
|
435
|
-
|
|
45
|
+
const url = `${this.workerUrl}?action=view&key=${this.apiKey}&file=${encodeURIComponent(name.endsWith('.btx') ? name : name + '.stream.btx')}&_cb=${Date.now()}`;
|
|
46
|
+
const { data: { session } } = await this.supabase.auth.getSession();
|
|
47
|
+
const res = await fetch(url, { headers: { 'Authorization': `Bearer ${session?.access_token || ''}` } });
|
|
436
48
|
const text = await res.text();
|
|
437
|
-
|
|
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);
|
|
49
|
+
try { return JSON.parse(text); } catch (e) { return text.split('\n').filter(Boolean).map(line => JSON.parse(line)); }
|
|
441
50
|
}
|
|
442
51
|
|
|
443
52
|
/**
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
* @returns {object|null} File metadata or null
|
|
53
|
+
* ByteX X-RAY v2.0 (The Surgeon)
|
|
54
|
+
* Advanced diagnostics & code implementation audit.
|
|
447
55
|
*/
|
|
448
|
-
async
|
|
449
|
-
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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;
|
|
556
|
-
|
|
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);
|
|
573
|
-
}
|
|
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
|
-
|
|
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)
|
|
56
|
+
async xray() {
|
|
57
|
+
console.log('[ByteX X-RAY] 🔍 Initiating Deep Code Surgery...');
|
|
58
|
+
const report = {
|
|
59
|
+
timestamp: new Date().toISOString(),
|
|
60
|
+
sdk_version: '1.9.0',
|
|
61
|
+
health_score: 100,
|
|
62
|
+
diagnostics: {},
|
|
63
|
+
advice: []
|
|
617
64
|
};
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
// ─────────────────────────────────────────────
|
|
622
|
-
// SECTION 8: ENCRYPTION UTILITIES
|
|
623
|
-
// ─────────────────────────────────────────────
|
|
624
|
-
|
|
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
|
-
}
|
|
666
|
-
|
|
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;
|
|
683
|
-
|
|
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
65
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
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}`);
|
|
752
|
-
}
|
|
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
|
-
}
|
|
765
|
-
|
|
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
|
-
}
|
|
66
|
+
// 1. Implementation Audit (Code Surgery)
|
|
67
|
+
report.diagnostics.implementation = { status: 'OK' };
|
|
68
|
+
if (!this.config.storage && typeof navigator !== 'undefined' && /React|Expo/i.test(navigator.userAgent || '')) {
|
|
69
|
+
report.diagnostics.implementation = { status: 'WARN', issue: 'Missing Storage Engine' };
|
|
70
|
+
report.advice.push('React Native detected but no storage engine (AsyncStorage) provided. Sessions will not persist.');
|
|
71
|
+
report.health_score -= 20;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 2. API Key Diagnostic
|
|
75
|
+
try {
|
|
76
|
+
this._requireKey();
|
|
77
|
+
report.diagnostics.api_key = { status: 'OK', prefix: this.apiKey.substring(0, 8) };
|
|
78
|
+
} catch (e) {
|
|
79
|
+
report.diagnostics.api_key = { status: 'CRITICAL', issue: 'Missing Key' };
|
|
80
|
+
report.advice.push('No API Key found. Call setApiKey() or pass it in constructor.');
|
|
81
|
+
report.health_score -= 40;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 3. Worker Connectivity & Speed Probe
|
|
85
|
+
const start = Date.now();
|
|
86
|
+
try {
|
|
87
|
+
const res = await fetch(`${this.workerUrl}?action=list&key=${this.apiKey}`);
|
|
88
|
+
const latency = Date.now() - start;
|
|
89
|
+
report.diagnostics.connectivity = {
|
|
90
|
+
status: res.ok ? 'ONLINE' : 'DEGRADED',
|
|
91
|
+
latency_ms: latency
|
|
92
|
+
};
|
|
93
|
+
if (latency > 500) {
|
|
94
|
+
report.advice.push('High latency detected. This may cause delays in audio/video streaming.');
|
|
95
|
+
}
|
|
96
|
+
} catch (e) {
|
|
97
|
+
report.diagnostics.connectivity = { status: 'OFFLINE', issue: 'Unreachable' };
|
|
98
|
+
report.advice.push('ByteX Cloud is unreachable. Check your internet or firewall settings.');
|
|
99
|
+
report.health_score -= 30;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 4. Quota Check
|
|
103
|
+
try {
|
|
104
|
+
const qRes = await fetch(`${this.workerUrl}?action=quota&key=${this.apiKey}`);
|
|
105
|
+
if (qRes.ok) {
|
|
106
|
+
const quota = await qRes.json();
|
|
107
|
+
report.diagnostics.storage = quota;
|
|
108
|
+
if (quota.percentage > 90) {
|
|
109
|
+
report.advice.push('Storage is almost full. Delete unused files to avoid upload errors.');
|
|
110
|
+
report.health_score -= 10;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
} catch (e) { /* skip if quota fails */ }
|
|
787
114
|
|
|
788
|
-
|
|
789
|
-
|
|
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();
|
|
115
|
+
report.final_status = report.health_score > 80 ? 'HEALTHY' : (report.health_score > 50 ? 'WARNING' : 'CRITICAL');
|
|
116
|
+
return report;
|
|
800
117
|
}
|
|
801
|
-
}
|
|
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
118
|
|
|
834
|
-
|
|
835
|
-
|
|
119
|
+
_requireKey() { if (!this.apiKey) throw new Error('API Key required!'); }
|
|
120
|
+
_emit(event, data) { if (this._listeners[event]) this._listeners[event].forEach(cb => cb(data)); }
|
|
836
121
|
}
|