bytex-sdk 5.4.0 → 5.6.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/README.md +30 -41
- package/package.json +17 -12
- package/src/index.js +165 -0
- package/index.js +0 -820
- package/postinstall.js +0 -26
package/README.md
CHANGED
|
@@ -1,56 +1,45 @@
|
|
|
1
|
-
# ByteX SDK
|
|
2
|
-
> The official SDK for the ByteX Cloud Ecosystem.
|
|
1
|
+
# ByteX Cloud SDK 🚀
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
The official SDK for the ByteX ecosystem, enabling developers to build professional "Super SaaS" products with 100% cloud sovereignty (BYOC).
|
|
5
4
|
|
|
6
|
-
##
|
|
7
|
-
- **
|
|
8
|
-
- **
|
|
9
|
-
- **
|
|
10
|
-
- **
|
|
11
|
-
- **Middleware System**: Intercept and optimize uploads (e.g., auto-convert to WebP).
|
|
12
|
-
- **Bulk Operations**: Download or export entire projects as ZIP.
|
|
5
|
+
## Features
|
|
6
|
+
- 🔐 **Secure Authentication**: Integrated with Supabase.
|
|
7
|
+
- 📦 **Data Persistence**: Upload and download streams directly to your own Cloud (HuggingFace).
|
|
8
|
+
- 🕵️♂️ **X-Ray Diagnostics**: Built-in network and infrastructure health checks.
|
|
9
|
+
- ⚡ **Lightweight**: Zero-config needed for most use cases.
|
|
13
10
|
|
|
14
|
-
##
|
|
11
|
+
## Installation
|
|
15
12
|
```bash
|
|
16
|
-
npm install bytex-sdk
|
|
13
|
+
npm install @bytex/cloud-sdk
|
|
17
14
|
```
|
|
18
15
|
|
|
19
|
-
##
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
### Initialize
|
|
20
19
|
```javascript
|
|
21
|
-
import { BytexCloud } from 'bytex-sdk';
|
|
20
|
+
import { BytexCloud } from '@bytex/cloud-sdk';
|
|
21
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
const bytex = new BytexCloud({
|
|
24
|
+
apiKey: 'YOUR_API_KEY',
|
|
25
|
+
workerUrl: 'https://api.bytex.work/',
|
|
26
|
+
storage: AsyncStorage // for React Native
|
|
27
27
|
});
|
|
28
|
-
|
|
29
|
-
// Upload a file
|
|
30
|
-
await bytex.upload('hello.txt', 'Hello World');
|
|
31
|
-
|
|
32
|
-
// Get a streaming URL
|
|
33
|
-
const streamUrl = bytex.stream('video.mp4');
|
|
34
|
-
|
|
35
|
-
// List files
|
|
36
|
-
const files = await bytex.listFiles();
|
|
37
|
-
console.log(files);
|
|
38
28
|
```
|
|
39
29
|
|
|
40
|
-
|
|
30
|
+
### Upload Data
|
|
41
31
|
```javascript
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
// Automatically optimize images to WebP before upload
|
|
47
|
-
bytex.use(BytexMiddlewares.imageOptimizer(0.8));
|
|
48
|
-
|
|
49
|
-
await bytex.upload('photo.jpg', fileData); // Uploads as photo.webp
|
|
32
|
+
await bytex.uploadData('my_project', {
|
|
33
|
+
status: 'active',
|
|
34
|
+
users: 1000
|
|
35
|
+
});
|
|
50
36
|
```
|
|
51
37
|
|
|
52
|
-
|
|
53
|
-
|
|
38
|
+
### Infrastructure Health Check (X-Ray)
|
|
39
|
+
```javascript
|
|
40
|
+
const report = await bytex.xray();
|
|
41
|
+
console.log(report.checks);
|
|
42
|
+
```
|
|
54
43
|
|
|
55
|
-
##
|
|
56
|
-
|
|
44
|
+
## License
|
|
45
|
+
MIT © ByteX Guru
|
package/package.json
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bytex-sdk",
|
|
3
|
-
"version": "5.
|
|
4
|
-
"description": "
|
|
5
|
-
"main": "index.js",
|
|
3
|
+
"version": "5.6.0",
|
|
4
|
+
"description": "The official ByteX Cloud SDK for BYOC (Bring Your Own Cloud) infrastructure management.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
-
"postinstall": "node postinstall.js"
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
9
8
|
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"bytex",
|
|
11
|
+
"cloud",
|
|
12
|
+
"saas",
|
|
13
|
+
"byoc",
|
|
14
|
+
"supabase",
|
|
15
|
+
"huggingface"
|
|
16
|
+
],
|
|
17
|
+
"author": "ByteX Guru",
|
|
18
|
+
"license": "MIT",
|
|
10
19
|
"dependencies": {
|
|
11
|
-
"@supabase/supabase-js": "^2.
|
|
12
|
-
"
|
|
13
|
-
}
|
|
14
|
-
"keywords": ["bytex", "cloud", "storage", "ai", "byom", "byoc"],
|
|
15
|
-
"author": "ByteX Team",
|
|
16
|
-
"license": "ISC",
|
|
17
|
-
"type": "module"
|
|
20
|
+
"@supabase/supabase-js": "^2.39.0",
|
|
21
|
+
"react-native-url-polyfill": "^2.0.0"
|
|
22
|
+
}
|
|
18
23
|
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { createClient } from '@supabase/supabase-js';
|
|
2
|
+
|
|
3
|
+
const SUPABASE_URL = 'https://vkiddclfbwmslaiyyftl.supabase.co';
|
|
4
|
+
const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InZraWRkY2xmYndtc2xhaXl5ZnRsIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzgzMDYyNjMsImV4cCI6MjA5Mzg4MjI2M30.VMtzB_I_b6WptKoxCko-vRQ5yPIBliOxNaxcRmqezSs';
|
|
5
|
+
const WORKER_URL = 'https://api.bytex.work/';
|
|
6
|
+
|
|
7
|
+
export class BytexCloud {
|
|
8
|
+
constructor(config = {}) {
|
|
9
|
+
this.config = config;
|
|
10
|
+
this.apiKey = config.apiKey || null;
|
|
11
|
+
this.workerUrl = config.workerUrl || WORKER_URL;
|
|
12
|
+
|
|
13
|
+
const supabaseConfig = {
|
|
14
|
+
auth: {
|
|
15
|
+
persistSession: true,
|
|
16
|
+
autoRefreshToken: true,
|
|
17
|
+
detectSessionInUrl: false
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
if (config.storage) {
|
|
21
|
+
supabaseConfig.auth.storage = config.storage;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
this.supabase = createClient(
|
|
25
|
+
config.supabaseUrl || SUPABASE_URL,
|
|
26
|
+
config.supabaseKey || SUPABASE_ANON_KEY,
|
|
27
|
+
supabaseConfig
|
|
28
|
+
);
|
|
29
|
+
this._listeners = {};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async _execute(fn, action) {
|
|
33
|
+
try {
|
|
34
|
+
return await fn();
|
|
35
|
+
} catch (e) {
|
|
36
|
+
const diag = await this.xray();
|
|
37
|
+
const advice = diag.advice.length > 0 ? `\n[X-RAY Advice]: ${diag.advice.join(' ')}` : '';
|
|
38
|
+
throw new Error(`${action} failed: ${e.message}${advice}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async login(email, password) {
|
|
43
|
+
return this._execute(async () => {
|
|
44
|
+
const { data, error } = await this.supabase.auth.signInWithPassword({ email, password });
|
|
45
|
+
if (error) throw error;
|
|
46
|
+
return data.user;
|
|
47
|
+
}, 'Login');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async register(email, password) {
|
|
51
|
+
return this._execute(async () => {
|
|
52
|
+
const { data, error } = await this.supabase.auth.signUp({ email, password });
|
|
53
|
+
if (error) throw error;
|
|
54
|
+
return data.user;
|
|
55
|
+
}, 'Register');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async logout() {
|
|
59
|
+
await this.supabase.auth.signOut();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async getUser() {
|
|
63
|
+
const { data: { session } } = await this.supabase.auth.getSession();
|
|
64
|
+
return session?.user || null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async upload(name, data) {
|
|
68
|
+
return this._execute(async () => {
|
|
69
|
+
this._requireKey();
|
|
70
|
+
const fd = new FormData();
|
|
71
|
+
if (data && typeof data === 'object' && data.uri) {
|
|
72
|
+
// React Native specific FormData file upload
|
|
73
|
+
const rnFile = {
|
|
74
|
+
uri: data.uri,
|
|
75
|
+
name: name, // Enforce full folder path
|
|
76
|
+
type: data.type || 'audio/mpeg'
|
|
77
|
+
};
|
|
78
|
+
fd.append('file', rnFile);
|
|
79
|
+
} else {
|
|
80
|
+
fd.append('file', data instanceof Blob ? data : new Blob([data]), name);
|
|
81
|
+
}
|
|
82
|
+
const { data: { session } } = await this.supabase.auth.getSession();
|
|
83
|
+
const res = await fetch(`${this.workerUrl}?action=upload&key=${this.apiKey}&file=${encodeURIComponent(name)}`, {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
headers: { 'Authorization': `Bearer ${session?.access_token || ''}` },
|
|
86
|
+
body: fd
|
|
87
|
+
});
|
|
88
|
+
if (!res.ok) throw new Error(await res.text() || res.statusText);
|
|
89
|
+
return await res.json();
|
|
90
|
+
}, 'Upload');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async uploadData(name, data) {
|
|
94
|
+
return this._execute(async () => {
|
|
95
|
+
this._requireKey();
|
|
96
|
+
const streamName = name.endsWith('.btx') ? name : `${name}.stream.btx`;
|
|
97
|
+
const url = `${this.workerUrl}?action=upload&key=${this.apiKey}&file=${encodeURIComponent(streamName)}`;
|
|
98
|
+
const { data: { session } } = await this.supabase.auth.getSession();
|
|
99
|
+
|
|
100
|
+
const payload = typeof data === 'string' ? data : JSON.stringify(data);
|
|
101
|
+
|
|
102
|
+
const res = await fetch(url, {
|
|
103
|
+
method: 'POST',
|
|
104
|
+
headers: {
|
|
105
|
+
'Authorization': `Bearer ${session?.access_token || ''}`,
|
|
106
|
+
'Content-Type': 'application/json'
|
|
107
|
+
},
|
|
108
|
+
body: payload
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (!res.ok) throw new Error(`Upload failed with status ${res.status}`);
|
|
112
|
+
return await res.json();
|
|
113
|
+
}, 'Upload');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async downloadData(name) {
|
|
117
|
+
return this._execute(async () => {
|
|
118
|
+
this._requireKey();
|
|
119
|
+
const streamName = name.endsWith('.btx') ? name : `${name}.stream.btx`;
|
|
120
|
+
const url = `${this.workerUrl}?action=view&key=${this.apiKey}&file=${encodeURIComponent(streamName)}&_cb=${Date.now()}`;
|
|
121
|
+
const { data: { session } } = await this.supabase.auth.getSession();
|
|
122
|
+
const res = await fetch(url, {
|
|
123
|
+
headers: { 'Authorization': `Bearer ${session?.access_token || ''}` }
|
|
124
|
+
});
|
|
125
|
+
if (!res.ok) throw new Error(`Status ${res.status}`);
|
|
126
|
+
const text = await res.text();
|
|
127
|
+
try { return JSON.parse(text); } catch (e) { return text.split('\n').filter(Boolean).map(line => JSON.parse(line)); }
|
|
128
|
+
}, 'Download');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async delete(name) {
|
|
132
|
+
return this._execute(async () => {
|
|
133
|
+
this._requireKey();
|
|
134
|
+
const streamName = name.endsWith('.btx') ? name : `${name}.stream.btx`;
|
|
135
|
+
const { data: { session } } = await this.supabase.auth.getSession();
|
|
136
|
+
const res = await fetch(`${this.workerUrl}?action=delete&key=${this.apiKey}&file=${encodeURIComponent(streamName)}`, {
|
|
137
|
+
method: 'DELETE',
|
|
138
|
+
headers: { 'Authorization': `Bearer ${session?.access_token || ''}` }
|
|
139
|
+
});
|
|
140
|
+
if (!res.ok) throw new Error(`Status ${res.status}`);
|
|
141
|
+
return true;
|
|
142
|
+
}, 'Delete');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async xray() {
|
|
146
|
+
const report = { timestamp: new Date().toISOString(), checks: {}, advice: [] };
|
|
147
|
+
const { data: { session } } = await this.supabase.auth.getSession();
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
const res = await fetch(`${this.workerUrl}?action=list&key=${this.apiKey}`);
|
|
151
|
+
report.checks.connectivity = res.ok ? 'OK' : 'ERROR';
|
|
152
|
+
} catch (e) {
|
|
153
|
+
report.checks.connectivity = 'OFFLINE';
|
|
154
|
+
report.advice.push('ByteX Cloud unreachable. Check internet connection.');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!this.apiKey) report.advice.push('API Key is missing.');
|
|
158
|
+
if (!session) report.advice.push('User is not logged in.');
|
|
159
|
+
if (!this.config.storage) report.advice.push('AsyncStorage engine is missing.');
|
|
160
|
+
|
|
161
|
+
return report;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
_requireKey() { if (!this.apiKey) throw new Error('API Key required!'); }
|
|
165
|
+
}
|
package/index.js
DELETED
|
@@ -1,820 +0,0 @@
|
|
|
1
|
-
import { createClient } from '@supabase/supabase-js';
|
|
2
|
-
|
|
3
|
-
const SUPABASE_URL = 'https://vkiddclfbwmslaiyyftl.supabase.co';
|
|
4
|
-
const SUPABASE_ANON_KEY = 'sb_publishable_hXMlH9OmJG1_n1s-3lbXKg_6V-88Lj9';
|
|
5
|
-
const WORKER_URL = 'https://api.bytex.work/';
|
|
6
|
-
|
|
7
|
-
export class BytexCloud {
|
|
8
|
-
constructor(config = {}) {
|
|
9
|
-
this.config = config;
|
|
10
|
-
this.apiKey = config.apiKey || null;
|
|
11
|
-
this.workerUrl = config.workerUrl || WORKER_URL;
|
|
12
|
-
|
|
13
|
-
const supabaseConfig = {
|
|
14
|
-
auth: {
|
|
15
|
-
persistSession: true,
|
|
16
|
-
autoRefreshToken: true,
|
|
17
|
-
detectSessionInUrl: false
|
|
18
|
-
}
|
|
19
|
-
};
|
|
20
|
-
if (config.storage) {
|
|
21
|
-
supabaseConfig.auth.storage = config.storage;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
this.supabase = createClient(config.supabaseUrl || SUPABASE_URL, config.supabaseKey || SUPABASE_ANON_KEY, supabaseConfig);
|
|
25
|
-
this._listeners = {};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// --- AUTO X-RAY WRAPPER ---
|
|
29
|
-
async _execute(fn, actionName) {
|
|
30
|
-
try {
|
|
31
|
-
return await fn();
|
|
32
|
-
} catch (e) {
|
|
33
|
-
console.warn(`[ByteX SDK] Error in ${actionName}. Running Auto X-RAY...`);
|
|
34
|
-
const diagnostic = await this.xray();
|
|
35
|
-
const advice = diagnostic.advice.length > 0 ? `\n[X-RAY Advice]: ${diagnostic.advice.join(' ')}` : '';
|
|
36
|
-
throw new Error(`${actionName} failed: ${e.message}${advice}`);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// --- CORE METHODS (ENHANCED WITH AUTO X-RAY) ---
|
|
41
|
-
async login(email, password) {
|
|
42
|
-
return this._execute(async () => {
|
|
43
|
-
const { data, error } = await this.supabase.auth.signInWithPassword({ email, password });
|
|
44
|
-
if (error) throw error;
|
|
45
|
-
return data.user;
|
|
46
|
-
}, 'Login');
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async upload(name, data) {
|
|
50
|
-
return this._execute(async () => {
|
|
51
|
-
this._requireKey();
|
|
52
|
-
const fd = new FormData();
|
|
53
|
-
fd.append('file', data instanceof Blob ? data : new Blob([data]), name);
|
|
54
|
-
const res = await fetch(`${this.workerUrl}?action=upload&key=${this.apiKey}`, { method: 'POST', body: fd });
|
|
55
|
-
if (!res.ok) throw new Error(await res.text() || res.statusText);
|
|
56
|
-
return await res.json();
|
|
57
|
-
}, 'Upload');
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async downloadData(name) {
|
|
61
|
-
return this._execute(async () => {
|
|
62
|
-
this._requireKey();
|
|
63
|
-
const streamName = name.endsWith('.btx') ? name : `${name}.stream.btx`;
|
|
64
|
-
const url = `${this.workerUrl}?action=view&key=${this.apiKey}&file=${encodeURIComponent(streamName)}&_cb=${Date.now()}`;
|
|
65
|
-
const { data: { session } } = await this.supabase.auth.getSession();
|
|
66
|
-
const res = await fetch(url, { headers: { 'Authorization': `Bearer ${session?.access_token || ''}` } });
|
|
67
|
-
if (!res.ok) throw new Error(`Status ${res.status}`);
|
|
68
|
-
const text = await res.text();
|
|
69
|
-
try { return JSON.parse(text); } catch (e) { return text.split('\n').filter(Boolean).map(line => JSON.parse(line)); }
|
|
70
|
-
}, 'Download');
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async delete(name) {
|
|
74
|
-
return this._execute(async () => {
|
|
75
|
-
this._requireKey();
|
|
76
|
-
const streamName = name.endsWith('.btx') ? name : `${name}.stream.btx`;
|
|
77
|
-
const { data: { session } } = await this.supabase.auth.getSession();
|
|
78
|
-
const res = await fetch(`${this.workerUrl}?action=delete&key=${this.apiKey}&file=${encodeURIComponent(streamName)}`, {
|
|
79
|
-
method: 'DELETE',
|
|
80
|
-
headers: { 'Authorization': `Bearer ${session?.access_token || ''}` }
|
|
81
|
-
});
|
|
82
|
-
if (!res.ok) throw new Error(`Status ${res.status}`);
|
|
83
|
-
return true;
|
|
84
|
-
}, 'Delete');
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* ByteX X-RAY v2.5 (Self-Healing Diagnostic)
|
|
89
|
-
*/
|
|
90
|
-
async xray() {
|
|
91
|
-
const report = { timestamp: new Date().toISOString(), sdk_version: '5.2.0', checks: {}, advice: [] };
|
|
92
|
-
const { data: { session } } = await this.supabase.auth.getSession();
|
|
93
|
-
|
|
94
|
-
// 1. Connectivity Check
|
|
95
|
-
const start = Date.now();
|
|
96
|
-
try {
|
|
97
|
-
const res = await fetch(`${this.workerUrl}?action=list&key=${this.apiKey}`);
|
|
98
|
-
report.checks.connectivity = res.ok ? 'OK' : 'ERROR';
|
|
99
|
-
report.latency = Date.now() - start;
|
|
100
|
-
} catch (e) {
|
|
101
|
-
report.checks.connectivity = 'OFFLINE';
|
|
102
|
-
report.advice.push('ByteX Cloud is unreachable. Check your internet.');
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// 2. API Key Check
|
|
106
|
-
if (!this.apiKey) {
|
|
107
|
-
report.checks.api_key = 'MISSING';
|
|
108
|
-
report.advice.push('No API Key detected. Please provide an apiKey in the constructor.');
|
|
109
|
-
} else { report.checks.api_key = 'OK'; }
|
|
110
|
-
|
|
111
|
-
// 3. Auth Check
|
|
112
|
-
report.checks.auth = session ? 'AUTHENTICATED' : 'GUEST';
|
|
113
|
-
if (!session) report.advice.push('User is not logged in. Some operations may fail.');
|
|
114
|
-
|
|
115
|
-
// 4. Platform Audit (Mobile/Web)
|
|
116
|
-
if (!this.config.storage && typeof navigator !== 'undefined') {
|
|
117
|
-
report.advice.push('Storage engine (AsyncStorage) is missing. Sessions will not persist.');
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return report;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
_requireKey() { if (!this.apiKey) throw new Error('API Key required!'); }
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Copy a file within the same project under a new name.
|
|
127
|
-
*/
|
|
128
|
-
async copy(sourceName, destName) {
|
|
129
|
-
return this._execute(async () => {
|
|
130
|
-
this._requireKey();
|
|
131
|
-
const src = sourceName.endsWith('.btx') ? sourceName : `${sourceName}.stream.btx`;
|
|
132
|
-
const { data: { session } } = await this.supabase.auth.getSession();
|
|
133
|
-
const res = await fetch(`${this.workerUrl}?action=view&key=${this.apiKey}&file=${encodeURIComponent(src)}&_cb=${Date.now()}`, {
|
|
134
|
-
headers: { 'Authorization': `Bearer ${session?.access_token || ''}` }
|
|
135
|
-
});
|
|
136
|
-
if (!res.ok) throw new Error(`File not found: ${sourceName}`);
|
|
137
|
-
const data = await res.arrayBuffer();
|
|
138
|
-
const fd = new FormData();
|
|
139
|
-
fd.append('file', new Blob([data]), destName);
|
|
140
|
-
const upRes = await fetch(`${this.workerUrl}?action=upload&key=${this.apiKey}`, { method: 'POST', body: fd });
|
|
141
|
-
if (!upRes.ok) throw new Error(await upRes.text());
|
|
142
|
-
return { source: sourceName, dest: destName };
|
|
143
|
-
}, 'Copy');
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Move a file to a different project (API key).
|
|
148
|
-
* @param {string} name - File name to move
|
|
149
|
-
* @param {string} destApiKey - Destination project's API key
|
|
150
|
-
*/
|
|
151
|
-
async move(name, destApiKey) {
|
|
152
|
-
return this._execute(async () => {
|
|
153
|
-
this._requireKey();
|
|
154
|
-
const src = name.endsWith('.btx') ? name : `${name}.stream.btx`;
|
|
155
|
-
const { data: { session } } = await this.supabase.auth.getSession();
|
|
156
|
-
// Download from source
|
|
157
|
-
const res = await fetch(`${this.workerUrl}?action=view&key=${this.apiKey}&file=${encodeURIComponent(src)}&_cb=${Date.now()}`, {
|
|
158
|
-
headers: { 'Authorization': `Bearer ${session?.access_token || ''}` }
|
|
159
|
-
});
|
|
160
|
-
if (!res.ok) throw new Error(`File not found: ${name}`);
|
|
161
|
-
const data = await res.arrayBuffer();
|
|
162
|
-
// Upload to destination
|
|
163
|
-
const fd = new FormData();
|
|
164
|
-
fd.append('file', new Blob([data]), name);
|
|
165
|
-
await fetch(`${this.workerUrl}?action=upload&key=${destApiKey}`, { method: 'POST', body: fd });
|
|
166
|
-
// Delete from source
|
|
167
|
-
await fetch(`${this.workerUrl}?action=delete&key=${this.apiKey}&file=${encodeURIComponent(src)}`, {
|
|
168
|
-
method: 'DELETE', headers: { 'Authorization': `Bearer ${session?.access_token || ''}` }
|
|
169
|
-
});
|
|
170
|
-
return { moved: name, to: destApiKey };
|
|
171
|
-
}, 'Move');
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Tag a file with one or more labels.
|
|
176
|
-
* @param {string} fileName - The file to tag
|
|
177
|
-
* @param {string[]} tags - Array of tag strings
|
|
178
|
-
*/
|
|
179
|
-
async tag(fileName, tags) {
|
|
180
|
-
return this._execute(async () => {
|
|
181
|
-
this._requireKey();
|
|
182
|
-
const existing = await this._getMeta('tags', fileName) || [];
|
|
183
|
-
const merged = [...new Set([...existing, ...tags])];
|
|
184
|
-
await this._setMeta('tags', fileName, merged);
|
|
185
|
-
return { fileName, tags: merged };
|
|
186
|
-
}, 'Tag');
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
async getTags(fileName) {
|
|
190
|
-
return this._execute(async () => {
|
|
191
|
-
this._requireKey();
|
|
192
|
-
return await this._getMeta('tags', fileName) || [];
|
|
193
|
-
}, 'GetTags');
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
async removeTag(fileName, tag) {
|
|
197
|
-
return this._execute(async () => {
|
|
198
|
-
this._requireKey();
|
|
199
|
-
const existing = await this._getMeta('tags', fileName) || [];
|
|
200
|
-
const updated = existing.filter(t => t !== tag);
|
|
201
|
-
await this._setMeta('tags', fileName, updated);
|
|
202
|
-
return { fileName, tags: updated };
|
|
203
|
-
}, 'RemoveTag');
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Pin a file to protect it from bulk operations.
|
|
208
|
-
*/
|
|
209
|
-
async pin(fileName) {
|
|
210
|
-
return this._execute(async () => {
|
|
211
|
-
this._requireKey();
|
|
212
|
-
const existing = await this._getMeta('pins', '__global') || [];
|
|
213
|
-
if (!existing.includes(fileName)) {
|
|
214
|
-
await this._setMeta('pins', '__global', [...existing, fileName]);
|
|
215
|
-
}
|
|
216
|
-
return { pinned: fileName };
|
|
217
|
-
}, 'Pin');
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
async unpin(fileName) {
|
|
221
|
-
return this._execute(async () => {
|
|
222
|
-
this._requireKey();
|
|
223
|
-
const existing = await this._getMeta('pins', '__global') || [];
|
|
224
|
-
await this._setMeta('pins', '__global', existing.filter(f => f !== fileName));
|
|
225
|
-
return { unpinned: fileName };
|
|
226
|
-
}, 'Unpin');
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
async getPins() {
|
|
230
|
-
return this._execute(async () => {
|
|
231
|
-
this._requireKey();
|
|
232
|
-
return await this._getMeta('pins', '__global') || [];
|
|
233
|
-
}, 'GetPins');
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Save a versioned snapshot of a file.
|
|
238
|
-
*/
|
|
239
|
-
async saveVersion(name) {
|
|
240
|
-
return this._execute(async () => {
|
|
241
|
-
this._requireKey();
|
|
242
|
-
const ts = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
|
|
243
|
-
return await this.copy(name, `__version__${ts}__${name}`);
|
|
244
|
-
}, 'SaveVersion');
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* List all saved versions of a file.
|
|
249
|
-
*/
|
|
250
|
-
async listVersions(name) {
|
|
251
|
-
return this._execute(async () => {
|
|
252
|
-
this._requireKey();
|
|
253
|
-
const { data: { session } } = await this.supabase.auth.getSession();
|
|
254
|
-
const res = await fetch(`${this.workerUrl}?action=list&key=${this.apiKey}`, {
|
|
255
|
-
headers: { 'Authorization': `Bearer ${session?.access_token || ''}` }
|
|
256
|
-
});
|
|
257
|
-
const text = await res.text();
|
|
258
|
-
const files = text.trim().split('\n').filter(Boolean).map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
|
|
259
|
-
return files.filter(f => (f.file_name || '').startsWith('__version__') && (f.file_name || '').endsWith(name));
|
|
260
|
-
}, 'ListVersions');
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* Register a webhook URL to receive event notifications.
|
|
265
|
-
* Note: webhooks are stored locally/in Supabase. Your server must listen at the endpoint.
|
|
266
|
-
*/
|
|
267
|
-
async addWebhook(url, events = ['upload', 'delete']) {
|
|
268
|
-
return this._execute(async () => {
|
|
269
|
-
this._requireKey();
|
|
270
|
-
const existing = await this._getMeta('webhooks', '__global') || [];
|
|
271
|
-
const updated = [...existing.filter(w => w.url !== url), { url, events, createdAt: new Date().toISOString() }];
|
|
272
|
-
await this._setMeta('webhooks', '__global', updated);
|
|
273
|
-
return { url, events };
|
|
274
|
-
}, 'AddWebhook');
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
async getWebhooks() {
|
|
278
|
-
return this._execute(async () => {
|
|
279
|
-
this._requireKey();
|
|
280
|
-
return await this._getMeta('webhooks', '__global') || [];
|
|
281
|
-
}, 'GetWebhooks');
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
async removeWebhook(url) {
|
|
285
|
-
return this._execute(async () => {
|
|
286
|
-
this._requireKey();
|
|
287
|
-
const existing = await this._getMeta('webhooks', '__global') || [];
|
|
288
|
-
await this._setMeta('webhooks', '__global', existing.filter(w => w.url !== url));
|
|
289
|
-
return { removed: url };
|
|
290
|
-
}, 'RemoveWebhook');
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// Internal metadata helper using Supabase
|
|
294
|
-
async _getMeta(type, key) {
|
|
295
|
-
try {
|
|
296
|
-
const { data } = await this.supabase.from('bytex_metadata').select('value').eq('api_key', this.apiKey).eq('meta_type', type).eq('meta_key', key).single();
|
|
297
|
-
return data?.value || null;
|
|
298
|
-
} catch { return null; }
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
async _setMeta(type, key, value) {
|
|
302
|
-
await this.supabase.from('bytex_metadata').upsert({ api_key: this.apiKey, meta_type: type, meta_key: key, value }, { onConflict: 'api_key,meta_type,meta_key' });
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* ByteX AI (BYOM) v1.1
|
|
308
|
-
* Bring Your Own Model — connect your own AI API keys
|
|
309
|
-
*
|
|
310
|
-
* Usage:
|
|
311
|
-
* const ai = new BytexAI({
|
|
312
|
-
* openai: 'sk-...',
|
|
313
|
-
* custom: { key: '...', endpoint: 'https://api.groq.com/openai/v1' }
|
|
314
|
-
* });
|
|
315
|
-
*/
|
|
316
|
-
export class BytexAI {
|
|
317
|
-
constructor(keys = {}) {
|
|
318
|
-
this.keys = keys;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/**
|
|
322
|
-
* Universal chat method.
|
|
323
|
-
* @param {'openai'|'claude'|'gemini'|'custom'} provider - The AI provider
|
|
324
|
-
* @param {string} model - The model name
|
|
325
|
-
* @param {Array<{role: string, content: string}>} messages - Chat messages
|
|
326
|
-
* @param {object} options - Optional params
|
|
327
|
-
* @returns {Promise<string>} - The AI reply text
|
|
328
|
-
*/
|
|
329
|
-
async chat(provider, model, messages, options = {}) {
|
|
330
|
-
switch (provider) {
|
|
331
|
-
case 'openai': return this._openai(model, messages, options);
|
|
332
|
-
case 'claude': return this._claude(model, messages, options);
|
|
333
|
-
case 'gemini': return this._gemini(model, messages, options);
|
|
334
|
-
case 'custom': return this._custom(model, messages, options);
|
|
335
|
-
default: throw new Error(`[BytexAI] Unknown provider: "${provider}". Use 'openai', 'claude', 'gemini', or 'custom'.`);
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
async _openai(model, messages, options) {
|
|
340
|
-
return this._openaiCompatible('https://api.openai.com/v1', this.keys.openai, model || 'gpt-4o-mini', messages, options);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
async _custom(model, messages, options) {
|
|
344
|
-
const config = this.keys.custom || {};
|
|
345
|
-
if (!config.key || !config.endpoint) {
|
|
346
|
-
throw new Error('[BytexAI] Custom provider requires both "key" and "endpoint" (OpenAI-compatible).');
|
|
347
|
-
}
|
|
348
|
-
return this._openaiCompatible(config.endpoint, config.key, model, messages, options);
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
async _openaiCompatible(baseURL, key, model, messages, options) {
|
|
352
|
-
if (!key) throw new Error(`[BytexAI] API key for ${baseURL} is missing.`);
|
|
353
|
-
const url = baseURL.endsWith('/') ? `${baseURL}chat/completions` : `${baseURL}/chat/completions`;
|
|
354
|
-
const res = await fetch(url, {
|
|
355
|
-
method: 'POST',
|
|
356
|
-
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${key}` },
|
|
357
|
-
body: JSON.stringify({
|
|
358
|
-
model: model,
|
|
359
|
-
messages,
|
|
360
|
-
temperature: options.temperature ?? 0.7,
|
|
361
|
-
max_tokens: options.maxTokens ?? 1024,
|
|
362
|
-
})
|
|
363
|
-
});
|
|
364
|
-
if (!res.ok) throw new Error(`[BytexAI] API error (${baseURL}): ${await res.text()}`);
|
|
365
|
-
const data = await res.json();
|
|
366
|
-
return data.choices[0].message.content;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
async _claude(model, messages, options) {
|
|
370
|
-
if (!this.keys.claude) throw new Error('[BytexAI] Claude API key is missing. Pass it via: new BytexAI({ claude: "sk-ant-..." })');
|
|
371
|
-
const system = messages.find(m => m.role === 'system')?.content;
|
|
372
|
-
const chatMessages = messages.filter(m => m.role !== 'system');
|
|
373
|
-
const res = await fetch('https://api.anthropic.com/v1/messages', {
|
|
374
|
-
method: 'POST',
|
|
375
|
-
headers: {
|
|
376
|
-
'Content-Type': 'application/json',
|
|
377
|
-
'x-api-key': this.keys.claude,
|
|
378
|
-
'anthropic-version': '2023-06-01'
|
|
379
|
-
},
|
|
380
|
-
body: JSON.stringify({
|
|
381
|
-
model: model || 'claude-3-5-haiku-20241022',
|
|
382
|
-
max_tokens: options.maxTokens ?? 1024,
|
|
383
|
-
...(system && { system }),
|
|
384
|
-
messages: chatMessages
|
|
385
|
-
})
|
|
386
|
-
});
|
|
387
|
-
if (!res.ok) throw new Error(`[BytexAI] Claude error: ${await res.text()}`);
|
|
388
|
-
const data = await res.json();
|
|
389
|
-
return data.content[0].text;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
async _gemini(model, messages, options) {
|
|
393
|
-
if (!this.keys.gemini) throw new Error('[BytexAI] Gemini API key is missing. Pass it via: new BytexAI({ gemini: "AIza..." })');
|
|
394
|
-
const contents = messages
|
|
395
|
-
.filter(m => m.role !== 'system')
|
|
396
|
-
.map(m => ({ role: m.role === 'assistant' ? 'model' : 'user', parts: [{ text: m.content }] }));
|
|
397
|
-
const endpoint = `https://generativelanguage.googleapis.com/v1beta/models/${model || 'gemini-1.5-flash'}:generateContent?key=${this.keys.gemini}`;
|
|
398
|
-
const res = await fetch(endpoint, {
|
|
399
|
-
method: 'POST',
|
|
400
|
-
headers: { 'Content-Type': 'application/json' },
|
|
401
|
-
body: JSON.stringify({ contents, generationConfig: { temperature: options.temperature ?? 0.7, maxOutputTokens: options.maxTokens ?? 1024 } })
|
|
402
|
-
});
|
|
403
|
-
if (!res.ok) throw new Error(`[BytexAI] Gemini error: ${await res.text()}`);
|
|
404
|
-
const data = await res.json();
|
|
405
|
-
return data.candidates[0].content.parts[0].text;
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// ════════════════════════════════════════════════════════════════
|
|
410
|
-
// ByteX REALTIME — Database Realtime via Supabase
|
|
411
|
-
// Strategy: Uses ByteX's Supabase OR user's own (BYOC)
|
|
412
|
-
// Free tier: 200 concurrent connections per Supabase project
|
|
413
|
-
// ════════════════════════════════════════════════════════════════
|
|
414
|
-
/**
|
|
415
|
-
* BytexRealtime — Subscribe to database changes in real-time.
|
|
416
|
-
*
|
|
417
|
-
* Usage:
|
|
418
|
-
* const rt = new BytexRealtime(bytex.supabase);
|
|
419
|
-
* rt.on('api_keys', (payload) => console.log(payload));
|
|
420
|
-
*/
|
|
421
|
-
export class BytexRealtime {
|
|
422
|
-
constructor(supabaseClient) {
|
|
423
|
-
if (!supabaseClient) throw new Error('[BytexRealtime] Pass a Supabase client. Use: new BytexRealtime(bytex.supabase)');
|
|
424
|
-
this.client = supabaseClient;
|
|
425
|
-
this._channels = new Map();
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
/**
|
|
429
|
-
* Subscribe to realtime changes on a table.
|
|
430
|
-
* @param {string} table - Table name (e.g. 'api_keys', 'files')
|
|
431
|
-
* @param {function} callback - Receives { event, new, old }
|
|
432
|
-
* @param {{ event?: 'INSERT'|'UPDATE'|'DELETE'|'*', filter?: string }} options
|
|
433
|
-
*/
|
|
434
|
-
on(table, callback, options = {}) {
|
|
435
|
-
const event = options.event || '*';
|
|
436
|
-
const channelName = `bytex-rt-${table}-${Date.now()}`;
|
|
437
|
-
const channel = this.client
|
|
438
|
-
.channel(channelName)
|
|
439
|
-
.on('postgres_changes', {
|
|
440
|
-
event,
|
|
441
|
-
schema: 'public',
|
|
442
|
-
table,
|
|
443
|
-
...(options.filter && { filter: options.filter })
|
|
444
|
-
}, (payload) => callback(payload))
|
|
445
|
-
.subscribe();
|
|
446
|
-
this._channels.set(channelName, channel);
|
|
447
|
-
return channelName;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
/**
|
|
451
|
-
* Subscribe to a broadcast channel (WebSocket replacement).
|
|
452
|
-
* @param {string} channelName - Unique room/channel name
|
|
453
|
-
* @param {function} callback - Receives { event, payload }
|
|
454
|
-
*/
|
|
455
|
-
join(channelName, callback) {
|
|
456
|
-
const channel = this.client.channel(channelName);
|
|
457
|
-
channel.on('broadcast', { event: '*' }, (msg) => callback(msg)).subscribe();
|
|
458
|
-
this._channels.set(channelName, channel);
|
|
459
|
-
return {
|
|
460
|
-
send: (event, payload) => channel.send({ type: 'broadcast', event, payload }),
|
|
461
|
-
leave: () => this.off(channelName)
|
|
462
|
-
};
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
/**
|
|
466
|
-
* Unsubscribe from a channel.
|
|
467
|
-
* @param {string} channelName
|
|
468
|
-
*/
|
|
469
|
-
off(channelName) {
|
|
470
|
-
const ch = this._channels.get(channelName);
|
|
471
|
-
if (ch) { this.client.removeChannel(ch); this._channels.delete(channelName); }
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
/** Remove all active subscriptions. */
|
|
475
|
-
destroy() {
|
|
476
|
-
this._channels.forEach((ch) => this.client.removeChannel(ch));
|
|
477
|
-
this._channels.clear();
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
// ════════════════════════════════════════════════════════════════
|
|
482
|
-
// ByteX CRON — Scheduled Jobs via Cloudflare Workers Cron Triggers
|
|
483
|
-
// Strategy: BYOC — user provides their Cloudflare API token
|
|
484
|
-
// Free tier: 3 Cron Triggers per Cloudflare account (FREE)
|
|
485
|
-
// ════════════════════════════════════════════════════════════════
|
|
486
|
-
/**
|
|
487
|
-
* BytexCron — Create and manage Cloudflare Workers Cron Triggers.
|
|
488
|
-
*
|
|
489
|
-
* Requires BYOC (user's Cloudflare account):
|
|
490
|
-
* const cron = new BytexCron({ cfToken: '...', cfAccountId: '...', workerName: 'my-worker' });
|
|
491
|
-
*/
|
|
492
|
-
export class BytexCron {
|
|
493
|
-
constructor({ cfToken, cfAccountId, workerName } = {}) {
|
|
494
|
-
if (!cfToken || !cfAccountId || !workerName)
|
|
495
|
-
throw new Error('[BytexCron] Required: { cfToken, cfAccountId, workerName } — Use BYOC config from bytex dashboard.');
|
|
496
|
-
this.token = cfToken;
|
|
497
|
-
this.accountId = cfAccountId;
|
|
498
|
-
this.workerName = workerName;
|
|
499
|
-
this.base = `https://api.cloudflare.com/client/v4/accounts/${cfAccountId}`;
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
/** List all existing cron triggers for the worker. */
|
|
503
|
-
async list() {
|
|
504
|
-
const res = await fetch(`${this.base}/workers/scripts/${this.workerName}/schedules`, {
|
|
505
|
-
headers: { 'Authorization': `Bearer ${this.token}` }
|
|
506
|
-
});
|
|
507
|
-
const data = await res.json();
|
|
508
|
-
if (!data.success) throw new Error(`[BytexCron] ${JSON.stringify(data.errors)}`);
|
|
509
|
-
return data.result?.schedules || [];
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
/**
|
|
513
|
-
* Add a new cron trigger (max 3 in free tier).
|
|
514
|
-
* @param {string} cron - Cron expression (e.g. '0 0 * * *' = daily at midnight)
|
|
515
|
-
*/
|
|
516
|
-
async add(cron) {
|
|
517
|
-
const existing = await this.list();
|
|
518
|
-
const schedules = [...existing.map(s => ({ cron: s.cron })), { cron }];
|
|
519
|
-
const res = await fetch(`${this.base}/workers/scripts/${this.workerName}/schedules`, {
|
|
520
|
-
method: 'PUT',
|
|
521
|
-
headers: { 'Authorization': `Bearer ${this.token}`, 'Content-Type': 'application/json' },
|
|
522
|
-
body: JSON.stringify(schedules)
|
|
523
|
-
});
|
|
524
|
-
const data = await res.json();
|
|
525
|
-
if (!data.success) throw new Error(`[BytexCron] ${JSON.stringify(data.errors)}`);
|
|
526
|
-
return data.result;
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
/**
|
|
530
|
-
* Remove a cron trigger.
|
|
531
|
-
* @param {string} cron - The exact cron string to remove
|
|
532
|
-
*/
|
|
533
|
-
async remove(cron) {
|
|
534
|
-
const existing = await this.list();
|
|
535
|
-
const schedules = existing.filter(s => s.cron !== cron).map(s => ({ cron: s.cron }));
|
|
536
|
-
const res = await fetch(`${this.base}/workers/scripts/${this.workerName}/schedules`, {
|
|
537
|
-
method: 'PUT',
|
|
538
|
-
headers: { 'Authorization': `Bearer ${this.token}`, 'Content-Type': 'application/json' },
|
|
539
|
-
body: JSON.stringify(schedules)
|
|
540
|
-
});
|
|
541
|
-
const data = await res.json();
|
|
542
|
-
if (!data.success) throw new Error(`[BytexCron] ${JSON.stringify(data.errors)}`);
|
|
543
|
-
return data.result;
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
// ════════════════════════════════════════════════════════════════
|
|
548
|
-
// ByteX QUEUE — Message Queue via Cloudflare Queues
|
|
549
|
-
// Strategy: BYOC — user provides their Cloudflare API token
|
|
550
|
-
// Free tier: 1,000,000 messages/month per account (FREE)
|
|
551
|
-
// ════════════════════════════════════════════════════════════════
|
|
552
|
-
/**
|
|
553
|
-
* BytexQueue — Create and publish to Cloudflare Queues.
|
|
554
|
-
*
|
|
555
|
-
* Requires BYOC (user's Cloudflare account):
|
|
556
|
-
* const queue = new BytexQueue({ cfToken: '...', cfAccountId: '...' });
|
|
557
|
-
*/
|
|
558
|
-
export class BytexQueue {
|
|
559
|
-
constructor({ cfToken, cfAccountId } = {}) {
|
|
560
|
-
if (!cfToken || !cfAccountId)
|
|
561
|
-
throw new Error('[BytexQueue] Required: { cfToken, cfAccountId } — Use BYOC config from bytex dashboard.');
|
|
562
|
-
this.token = cfToken;
|
|
563
|
-
this.accountId = cfAccountId;
|
|
564
|
-
this.base = `https://api.cloudflare.com/client/v4/accounts/${cfAccountId}/queues`;
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
/** List all existing queues. */
|
|
568
|
-
async list() {
|
|
569
|
-
const res = await fetch(this.base, { headers: { 'Authorization': `Bearer ${this.token}` } });
|
|
570
|
-
const data = await res.json();
|
|
571
|
-
if (!data.success) throw new Error(`[BytexQueue] ${JSON.stringify(data.errors)}`);
|
|
572
|
-
return data.result || [];
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
/**
|
|
576
|
-
* Create a new queue.
|
|
577
|
-
* @param {string} name - Queue name (lowercase, hyphens only)
|
|
578
|
-
*/
|
|
579
|
-
async create(name) {
|
|
580
|
-
const res = await fetch(this.base, {
|
|
581
|
-
method: 'POST',
|
|
582
|
-
headers: { 'Authorization': `Bearer ${this.token}`, 'Content-Type': 'application/json' },
|
|
583
|
-
body: JSON.stringify({ queue_name: name })
|
|
584
|
-
});
|
|
585
|
-
const data = await res.json();
|
|
586
|
-
if (!data.success) throw new Error(`[BytexQueue] ${JSON.stringify(data.errors)}`);
|
|
587
|
-
return data.result;
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
/**
|
|
591
|
-
* Delete a queue.
|
|
592
|
-
* @param {string} name - Queue name
|
|
593
|
-
*/
|
|
594
|
-
async delete(name) {
|
|
595
|
-
const res = await fetch(`${this.base}/${name}`, {
|
|
596
|
-
method: 'DELETE',
|
|
597
|
-
headers: { 'Authorization': `Bearer ${this.token}` }
|
|
598
|
-
});
|
|
599
|
-
const data = await res.json();
|
|
600
|
-
if (!data.success) throw new Error(`[BytexQueue] ${JSON.stringify(data.errors)}`);
|
|
601
|
-
return true;
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
/**
|
|
605
|
-
* Publish a message to a queue.
|
|
606
|
-
* @param {string} queueName - Target queue
|
|
607
|
-
* @param {object|string} body - Message content
|
|
608
|
-
* @param {object} options - { delaySeconds? }
|
|
609
|
-
*/
|
|
610
|
-
async publish(queueName, body, options = {}) {
|
|
611
|
-
const message = { body: typeof body === 'string' ? body : JSON.stringify(body) };
|
|
612
|
-
if (options.delaySeconds) message.delay_seconds = options.delaySeconds;
|
|
613
|
-
const res = await fetch(`${this.base}/${queueName}/messages`, {
|
|
614
|
-
method: 'POST',
|
|
615
|
-
headers: { 'Authorization': `Bearer ${this.token}`, 'Content-Type': 'application/json' },
|
|
616
|
-
body: JSON.stringify({ messages: [message] })
|
|
617
|
-
});
|
|
618
|
-
const data = await res.json();
|
|
619
|
-
if (!data.success) throw new Error(`[BytexQueue] ${JSON.stringify(data.errors)}`);
|
|
620
|
-
return data.result;
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
/**
|
|
624
|
-
* Publish multiple messages in a single batch (max 100).
|
|
625
|
-
* @param {string} queueName - Target queue
|
|
626
|
-
* @param {Array<object|string>} items - Array of message bodies
|
|
627
|
-
*/
|
|
628
|
-
async publishBatch(queueName, items) {
|
|
629
|
-
const messages = items.map(body => ({ body: typeof body === 'string' ? body : JSON.stringify(body) }));
|
|
630
|
-
const res = await fetch(`${this.base}/${queueName}/messages`, {
|
|
631
|
-
method: 'POST',
|
|
632
|
-
headers: { 'Authorization': `Bearer ${this.token}`, 'Content-Type': 'application/json' },
|
|
633
|
-
body: JSON.stringify({ messages })
|
|
634
|
-
});
|
|
635
|
-
const data = await res.json();
|
|
636
|
-
if (!data.success) throw new Error(`[BytexQueue] ${JSON.stringify(data.errors)}`);
|
|
637
|
-
return data.result;
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
// ════════════════════════════════════════════════════════════════
|
|
642
|
-
// ByteX PROXY — Reverse Proxy via Cloudflare Workers
|
|
643
|
-
// Strategy: Uses ByteX's Cloudflare (no BYOC needed for basic use)
|
|
644
|
-
// Free tier: 100,000 requests/day (FREE)
|
|
645
|
-
// ════════════════════════════════════════════════════════════════
|
|
646
|
-
/**
|
|
647
|
-
* BytexProxy — Deploy and manage Cloudflare Worker reverse proxies.
|
|
648
|
-
*
|
|
649
|
-
* Usage:
|
|
650
|
-
* // Use ByteX's proxy endpoint (no config needed):
|
|
651
|
-
* const url = BytexProxy.buildUrl('https://api.target.com/endpoint', { headers: {...} });
|
|
652
|
-
*
|
|
653
|
-
* // Or deploy your own proxy Worker (BYOC):
|
|
654
|
-
* const proxy = new BytexProxy({ cfToken: '...', cfAccountId: '...' });
|
|
655
|
-
* await proxy.deploy('my-proxy', 'https://api.target.com');
|
|
656
|
-
*/
|
|
657
|
-
export class BytexProxy {
|
|
658
|
-
constructor({ cfToken, cfAccountId } = {}) {
|
|
659
|
-
this.token = cfToken;
|
|
660
|
-
this.accountId = cfAccountId;
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
/**
|
|
664
|
-
* Build a proxied URL via ByteX's own proxy layer.
|
|
665
|
-
* Useful for CORS bypass and header injection.
|
|
666
|
-
* @param {string} targetUrl - The destination URL to proxy
|
|
667
|
-
* @param {{ headers?: object }} options
|
|
668
|
-
*/
|
|
669
|
-
static buildUrl(targetUrl, options = {}) {
|
|
670
|
-
const encoded = encodeURIComponent(targetUrl);
|
|
671
|
-
const base = 'https://api.bytex.work/?action=proxy&target=';
|
|
672
|
-
return `${base}${encoded}`;
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
/**
|
|
676
|
-
* Deploy a Cloudflare Worker that proxies to a target origin (BYOC).
|
|
677
|
-
* @param {string} workerName - Unique name for this proxy worker
|
|
678
|
-
* @param {string} targetOrigin - Base URL of the backend (e.g. 'https://my-api.com')
|
|
679
|
-
* @param {{ injectHeaders?: object, stripHeaders?: string[] }} options
|
|
680
|
-
*/
|
|
681
|
-
async deploy(workerName, targetOrigin, options = {}) {
|
|
682
|
-
if (!this.token || !this.accountId)
|
|
683
|
-
throw new Error('[BytexProxy] BYOC required for deploy. Pass { cfToken, cfAccountId }.');
|
|
684
|
-
|
|
685
|
-
const injectHeaders = options.injectHeaders
|
|
686
|
-
? Object.entries(options.injectHeaders).map(([k, v]) => `req.headers.set('${k}', '${v}');`).join('\n ')
|
|
687
|
-
: '';
|
|
688
|
-
const stripHeaders = (options.stripHeaders || [])
|
|
689
|
-
.map(h => `req.headers.delete('${h}');`).join('\n ');
|
|
690
|
-
|
|
691
|
-
const workerScript = `
|
|
692
|
-
export default {
|
|
693
|
-
async fetch(request) {
|
|
694
|
-
const url = new URL(request.url);
|
|
695
|
-
const target = new URL('${targetOrigin}' + url.pathname + url.search);
|
|
696
|
-
const req = new Request(target, request);
|
|
697
|
-
${injectHeaders}
|
|
698
|
-
${stripHeaders}
|
|
699
|
-
const response = await fetch(req);
|
|
700
|
-
const res = new Response(response.body, response);
|
|
701
|
-
res.headers.set('Access-Control-Allow-Origin', '*');
|
|
702
|
-
return res;
|
|
703
|
-
}
|
|
704
|
-
}`;
|
|
705
|
-
|
|
706
|
-
const fd = new FormData();
|
|
707
|
-
fd.append('metadata', JSON.stringify({ main_module: 'worker.js', compatibility_date: '2024-01-01' }));
|
|
708
|
-
fd.append('worker.js', new Blob([workerScript], { type: 'application/javascript+module' }), 'worker.js');
|
|
709
|
-
|
|
710
|
-
const res = await fetch(`https://api.cloudflare.com/client/v4/accounts/${this.accountId}/workers/scripts/${workerName}`, {
|
|
711
|
-
method: 'PUT',
|
|
712
|
-
headers: { 'Authorization': `Bearer ${this.token}` },
|
|
713
|
-
body: fd
|
|
714
|
-
});
|
|
715
|
-
const data = await res.json();
|
|
716
|
-
if (!data.success) throw new Error(`[BytexProxy] ${JSON.stringify(data.errors)}`);
|
|
717
|
-
return { workerName, url: `https://${workerName}.workers.dev`, target: targetOrigin };
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
// ════════════════════════════════════════════════════════════════
|
|
722
|
-
// ByteX MQTT — IoT Messaging via user's MQTT Broker
|
|
723
|
-
// Strategy: BYOC — user provides their broker (HiveMQ/EMQX/any)
|
|
724
|
-
// Free tier: HiveMQ Cloud free (10 devices), EMQX Serverless (1M msg/mo)
|
|
725
|
-
// ════════════════════════════════════════════════════════════════
|
|
726
|
-
/**
|
|
727
|
-
* BytexMQTT — Lightweight MQTT client wrapper (BYOC).
|
|
728
|
-
*
|
|
729
|
-
* Requires the 'mqtt' package: npm install mqtt
|
|
730
|
-
* Requires user's own MQTT broker (HiveMQ, EMQX, Mosquitto, etc.)
|
|
731
|
-
*
|
|
732
|
-
* Usage:
|
|
733
|
-
* const mq = new BytexMQTT({
|
|
734
|
-
* brokerUrl: 'mqtts://your-cluster.hivemq.cloud:8883',
|
|
735
|
-
* username: 'user', password: 'pass'
|
|
736
|
-
* });
|
|
737
|
-
* await mq.connect();
|
|
738
|
-
* mq.subscribe('bytex/files/#', (topic, msg) => console.log(topic, msg));
|
|
739
|
-
* mq.publish('bytex/files/new', { name: 'photo.jpg' });
|
|
740
|
-
*/
|
|
741
|
-
export class BytexMQTT {
|
|
742
|
-
constructor({ brokerUrl, username, password, clientId } = {}) {
|
|
743
|
-
if (!brokerUrl)
|
|
744
|
-
throw new Error('[BytexMQTT] Required: { brokerUrl } — Get free broker at hivemq.com or emqx.com (BYOC).');
|
|
745
|
-
this.brokerUrl = brokerUrl;
|
|
746
|
-
this.options = {
|
|
747
|
-
username, password,
|
|
748
|
-
clientId: clientId || `bytex-${Math.random().toString(36).substring(2, 9)}`,
|
|
749
|
-
clean: true,
|
|
750
|
-
reconnectPeriod: 3000,
|
|
751
|
-
connectTimeout: 10000,
|
|
752
|
-
};
|
|
753
|
-
this.client = null;
|
|
754
|
-
this._handlers = new Map();
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
/**
|
|
758
|
-
* Connect to the MQTT broker.
|
|
759
|
-
* @returns {Promise<void>}
|
|
760
|
-
*/
|
|
761
|
-
async connect() {
|
|
762
|
-
let mqtt;
|
|
763
|
-
try { mqtt = (await import('mqtt')).default; }
|
|
764
|
-
catch { throw new Error('[BytexMQTT] Install the mqtt package first: npm install mqtt'); }
|
|
765
|
-
|
|
766
|
-
return new Promise((resolve, reject) => {
|
|
767
|
-
this.client = mqtt.connect(this.brokerUrl, this.options);
|
|
768
|
-
this.client.on('connect', () => resolve());
|
|
769
|
-
this.client.on('error', (err) => reject(new Error(`[BytexMQTT] Connection failed: ${err.message}`)));
|
|
770
|
-
this.client.on('message', (topic, buf) => {
|
|
771
|
-
const msg = buf.toString();
|
|
772
|
-
let parsed; try { parsed = JSON.parse(msg); } catch { parsed = msg; }
|
|
773
|
-
this._handlers.forEach((fn, pattern) => {
|
|
774
|
-
if (this._topicMatch(pattern, topic)) fn(topic, parsed);
|
|
775
|
-
});
|
|
776
|
-
});
|
|
777
|
-
});
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
/**
|
|
781
|
-
* Publish a message to a topic.
|
|
782
|
-
* @param {string} topic
|
|
783
|
-
* @param {object|string} payload
|
|
784
|
-
* @param {{ qos?: 0|1|2, retain?: boolean }} options
|
|
785
|
-
*/
|
|
786
|
-
publish(topic, payload, options = {}) {
|
|
787
|
-
if (!this.client) throw new Error('[BytexMQTT] Not connected. Call connect() first.');
|
|
788
|
-
const msg = typeof payload === 'string' ? payload : JSON.stringify(payload);
|
|
789
|
-
this.client.publish(topic, msg, { qos: options.qos ?? 1, retain: options.retain ?? false });
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
/**
|
|
793
|
-
* Subscribe to a topic or wildcard pattern.
|
|
794
|
-
* @param {string} topic - Supports MQTT wildcards: '#' (multi-level), '+' (single-level)
|
|
795
|
-
* @param {function} callback - (topic, parsedPayload) => void
|
|
796
|
-
* @param {{ qos?: 0|1|2 }} options
|
|
797
|
-
*/
|
|
798
|
-
subscribe(topic, callback, options = {}) {
|
|
799
|
-
if (!this.client) throw new Error('[BytexMQTT] Not connected. Call connect() first.');
|
|
800
|
-
this.client.subscribe(topic, { qos: options.qos ?? 1 });
|
|
801
|
-
this._handlers.set(topic, callback);
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
/** Unsubscribe from a topic. */
|
|
805
|
-
unsubscribe(topic) {
|
|
806
|
-
if (this.client) this.client.unsubscribe(topic);
|
|
807
|
-
this._handlers.delete(topic);
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
/** Gracefully disconnect from the broker. */
|
|
811
|
-
disconnect() {
|
|
812
|
-
if (this.client) { this.client.end(); this.client = null; }
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
_topicMatch(pattern, topic) {
|
|
816
|
-
const p = pattern.replace(/\+/g, '[^/]+').replace(/#$/, '.*');
|
|
817
|
-
return new RegExp(`^${p}$`).test(topic);
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
|
package/postinstall.js
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
|
|
3
|
-
console.log('');
|
|
4
|
-
console.log(chalk.blue.bold('========================================='));
|
|
5
|
-
console.log(chalk.cyan.bold(' 🚀 Successfully installed ByteX SDK!'));
|
|
6
|
-
console.log(chalk.blue.bold('========================================='));
|
|
7
|
-
console.log('');
|
|
8
|
-
console.log(chalk.white('Get started by importing the SDK into your project:'));
|
|
9
|
-
console.log('');
|
|
10
|
-
console.log(chalk.gray(' // 1. Initialize ByteX'));
|
|
11
|
-
console.log(chalk.green(" import { BytexCloud } from 'bytex-sdk';"));
|
|
12
|
-
console.log(chalk.green(" const bytex = new BytexCloud();"));
|
|
13
|
-
console.log('');
|
|
14
|
-
console.log(chalk.gray(' // 2. Login & Generate/Select API Key directly in code'));
|
|
15
|
-
console.log(chalk.green(" await bytex.login('your@email.com', 'password');"));
|
|
16
|
-
console.log(chalk.green(" const myKey = await bytex.createKey('My Web App');"));
|
|
17
|
-
console.log(chalk.gray(' // (The active API Key is now set automatically!)'));
|
|
18
|
-
console.log('');
|
|
19
|
-
console.log(chalk.gray(' // 3. Upload a file'));
|
|
20
|
-
console.log(chalk.green(" await bytex.upload('document.pdf', fileData);"));
|
|
21
|
-
console.log('');
|
|
22
|
-
console.log(chalk.white('Want to do this via terminal instead? Try the CLI:'));
|
|
23
|
-
console.log(chalk.cyan(' npx bytex-cli init'));
|
|
24
|
-
console.log('');
|
|
25
|
-
console.log(chalk.gray('For full SDK documentation, visit: https://bytex.work/docs'));
|
|
26
|
-
console.log('');
|