voltjs-framework 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1265 -0
  3. package/bin/volt.js +139 -0
  4. package/package.json +56 -0
  5. package/src/api/graphql.js +399 -0
  6. package/src/api/rest.js +204 -0
  7. package/src/api/websocket.js +285 -0
  8. package/src/cli/build.js +111 -0
  9. package/src/cli/create.js +371 -0
  10. package/src/cli/db.js +106 -0
  11. package/src/cli/dev.js +114 -0
  12. package/src/cli/generate.js +278 -0
  13. package/src/cli/lint.js +172 -0
  14. package/src/cli/routes.js +118 -0
  15. package/src/cli/start.js +42 -0
  16. package/src/cli/test.js +138 -0
  17. package/src/core/app.js +701 -0
  18. package/src/core/config.js +232 -0
  19. package/src/core/middleware.js +133 -0
  20. package/src/core/plugins.js +88 -0
  21. package/src/core/react-renderer.js +244 -0
  22. package/src/core/renderer.js +337 -0
  23. package/src/core/router.js +183 -0
  24. package/src/database/index.js +461 -0
  25. package/src/database/migration.js +192 -0
  26. package/src/database/model.js +285 -0
  27. package/src/database/query.js +394 -0
  28. package/src/database/seeder.js +89 -0
  29. package/src/index.js +156 -0
  30. package/src/security/auth.js +425 -0
  31. package/src/security/cors.js +80 -0
  32. package/src/security/csrf.js +125 -0
  33. package/src/security/encryption.js +110 -0
  34. package/src/security/helmet.js +103 -0
  35. package/src/security/index.js +75 -0
  36. package/src/security/rateLimit.js +119 -0
  37. package/src/security/sanitizer.js +113 -0
  38. package/src/security/xss.js +110 -0
  39. package/src/ui/component.js +224 -0
  40. package/src/ui/reactive.js +503 -0
  41. package/src/ui/template.js +448 -0
  42. package/src/utils/cache.js +216 -0
  43. package/src/utils/collection.js +772 -0
  44. package/src/utils/cron.js +213 -0
  45. package/src/utils/date.js +223 -0
  46. package/src/utils/events.js +181 -0
  47. package/src/utils/excel.js +482 -0
  48. package/src/utils/form.js +547 -0
  49. package/src/utils/hash.js +121 -0
  50. package/src/utils/http.js +461 -0
  51. package/src/utils/logger.js +186 -0
  52. package/src/utils/mail.js +347 -0
  53. package/src/utils/paginator.js +179 -0
  54. package/src/utils/pdf.js +417 -0
  55. package/src/utils/queue.js +199 -0
  56. package/src/utils/schema.js +985 -0
  57. package/src/utils/sms.js +243 -0
  58. package/src/utils/storage.js +348 -0
  59. package/src/utils/string.js +236 -0
  60. package/src/utils/validation.js +318 -0
@@ -0,0 +1,243 @@
1
+ /**
2
+ * VoltJS SMS Module
3
+ *
4
+ * Send SMS messages via various providers (Twilio, Vonage, custom API).
5
+ * Uses native HTTPS for API calls — zero dependencies.
6
+ *
7
+ * @example
8
+ * const { SMS } = require('voltjs');
9
+ *
10
+ * const sms = new SMS({
11
+ * provider: 'twilio',
12
+ * accountSid: 'your_sid',
13
+ * authToken: 'your_token',
14
+ * from: '+1234567890',
15
+ * });
16
+ *
17
+ * await sms.send('+0987654321', 'Hello from VoltJS!');
18
+ */
19
+
20
+ 'use strict';
21
+
22
+ const https = require('https');
23
+ const http = require('http');
24
+
25
+ class SMS {
26
+ constructor(config = {}) {
27
+ this._config = {
28
+ provider: config.provider || 'custom',
29
+ from: config.from || process.env.SMS_FROM || '',
30
+ // Twilio
31
+ accountSid: config.accountSid || process.env.TWILIO_ACCOUNT_SID || '',
32
+ authToken: config.authToken || process.env.TWILIO_AUTH_TOKEN || '',
33
+ // Vonage/Nexmo
34
+ apiKey: config.apiKey || process.env.VONAGE_API_KEY || '',
35
+ apiSecret: config.apiSecret || process.env.VONAGE_API_SECRET || '',
36
+ // Custom API
37
+ apiUrl: config.apiUrl || process.env.SMS_API_URL || '',
38
+ apiHeaders: config.apiHeaders || {},
39
+ apiBodyTemplate: config.apiBodyTemplate || null,
40
+ ...config,
41
+ };
42
+ this._templates = new Map();
43
+ this._log = [];
44
+ }
45
+
46
+ /** Send an SMS */
47
+ async send(to, message, options = {}) {
48
+ const formattedTo = this._formatPhone(to);
49
+ const formattedMessage = options.template
50
+ ? this._renderTemplate(options.template, options.data || {})
51
+ : message;
52
+
53
+ let result;
54
+ switch (this._config.provider.toLowerCase()) {
55
+ case 'twilio':
56
+ result = await this._sendTwilio(formattedTo, formattedMessage);
57
+ break;
58
+ case 'vonage':
59
+ case 'nexmo':
60
+ result = await this._sendVonage(formattedTo, formattedMessage);
61
+ break;
62
+ case 'custom':
63
+ result = await this._sendCustom(formattedTo, formattedMessage);
64
+ break;
65
+ case 'console':
66
+ case 'log':
67
+ result = this._sendConsole(formattedTo, formattedMessage);
68
+ break;
69
+ default:
70
+ throw new Error(`Unknown SMS provider: ${this._config.provider}`);
71
+ }
72
+
73
+ this._log.push({
74
+ to: formattedTo,
75
+ message: formattedMessage.substring(0, 50),
76
+ provider: this._config.provider,
77
+ timestamp: new Date(),
78
+ ...result,
79
+ });
80
+
81
+ return result;
82
+ }
83
+
84
+ /** Send SMS to multiple recipients */
85
+ async sendBulk(recipients, message, options = {}) {
86
+ const results = [];
87
+ for (const to of recipients) {
88
+ try {
89
+ const result = await this.send(to, message, options);
90
+ results.push({ to, status: 'sent', ...result });
91
+ } catch (err) {
92
+ results.push({ to, status: 'failed', error: err.message });
93
+ }
94
+ }
95
+ return results;
96
+ }
97
+
98
+ /** Register an SMS template */
99
+ template(name, template) {
100
+ this._templates.set(name, template);
101
+ return this;
102
+ }
103
+
104
+ /** Send OTP SMS */
105
+ async sendOTP(to, code, options = {}) {
106
+ const message = options.message || `Your verification code is: ${code}. Valid for ${options.validFor || '10'} minutes.`;
107
+ return this.send(to, message);
108
+ }
109
+
110
+ /** Get send log */
111
+ getLog() {
112
+ return [...this._log];
113
+ }
114
+
115
+ // ===== PROVIDERS =====
116
+
117
+ async _sendTwilio(to, body) {
118
+ const { accountSid, authToken, from } = this._config;
119
+
120
+ const postData = new URLSearchParams({
121
+ To: to,
122
+ From: from,
123
+ Body: body,
124
+ }).toString();
125
+
126
+ const response = await this._httpRequest({
127
+ hostname: 'api.twilio.com',
128
+ path: `/2010-04-01/Accounts/${accountSid}/Messages.json`,
129
+ method: 'POST',
130
+ headers: {
131
+ 'Content-Type': 'application/x-www-form-urlencoded',
132
+ 'Authorization': 'Basic ' + Buffer.from(`${accountSid}:${authToken}`).toString('base64'),
133
+ 'Content-Length': Buffer.byteLength(postData),
134
+ },
135
+ }, postData);
136
+
137
+ return { success: true, provider: 'twilio', sid: response.sid };
138
+ }
139
+
140
+ async _sendVonage(to, text) {
141
+ const { apiKey, apiSecret, from } = this._config;
142
+
143
+ const postData = JSON.stringify({
144
+ api_key: apiKey,
145
+ api_secret: apiSecret,
146
+ to: to.replace(/\+/g, ''),
147
+ from,
148
+ text,
149
+ });
150
+
151
+ const response = await this._httpRequest({
152
+ hostname: 'rest.nexmo.com',
153
+ path: '/sms/json',
154
+ method: 'POST',
155
+ headers: {
156
+ 'Content-Type': 'application/json',
157
+ 'Content-Length': Buffer.byteLength(postData),
158
+ },
159
+ }, postData);
160
+
161
+ return { success: true, provider: 'vonage', response };
162
+ }
163
+
164
+ async _sendCustom(to, message) {
165
+ const { apiUrl, apiHeaders, apiBodyTemplate } = this._config;
166
+ if (!apiUrl) throw new Error('Custom SMS provider requires apiUrl');
167
+
168
+ const urlObj = new URL(apiUrl);
169
+ let body;
170
+
171
+ if (apiBodyTemplate) {
172
+ body = JSON.stringify(apiBodyTemplate)
173
+ .replace(/\{\{to\}\}/g, to)
174
+ .replace(/\{\{message\}\}/g, message)
175
+ .replace(/\{\{from\}\}/g, this._config.from);
176
+ } else {
177
+ body = JSON.stringify({ to, message, from: this._config.from });
178
+ }
179
+
180
+ const response = await this._httpRequest({
181
+ hostname: urlObj.hostname,
182
+ port: urlObj.port,
183
+ path: urlObj.pathname + urlObj.search,
184
+ method: 'POST',
185
+ headers: {
186
+ 'Content-Type': 'application/json',
187
+ 'Content-Length': Buffer.byteLength(body),
188
+ ...apiHeaders,
189
+ },
190
+ }, body);
191
+
192
+ return { success: true, provider: 'custom', response };
193
+ }
194
+
195
+ _sendConsole(to, message) {
196
+ console.log(`\x1b[36m 📱 SMS [${this._config.from} → ${to}]: ${message}\x1b[0m`);
197
+ return { success: true, provider: 'console' };
198
+ }
199
+
200
+ // ===== HELPERS =====
201
+
202
+ _formatPhone(phone) {
203
+ let cleaned = String(phone).replace(/[^0-9+]/g, '');
204
+ if (!cleaned.startsWith('+')) {
205
+ cleaned = '+' + cleaned;
206
+ }
207
+ return cleaned;
208
+ }
209
+
210
+ _renderTemplate(name, data) {
211
+ const template = this._templates.get(name);
212
+ if (!template) throw new Error(`SMS template "${name}" not found`);
213
+ return template.replace(/\{\{\s*(.+?)\s*\}\}/g, (_, key) => {
214
+ return data[key] !== undefined ? String(data[key]) : '';
215
+ });
216
+ }
217
+
218
+ _httpRequest(options, postData) {
219
+ return new Promise((resolve, reject) => {
220
+ const protocol = options.port === 80 ? http : https;
221
+ const req = protocol.request(options, (res) => {
222
+ let data = '';
223
+ res.on('data', chunk => data += chunk);
224
+ res.on('end', () => {
225
+ try {
226
+ resolve(JSON.parse(data));
227
+ } catch {
228
+ resolve(data);
229
+ }
230
+ });
231
+ });
232
+ req.on('error', reject);
233
+ req.setTimeout(10000, () => {
234
+ req.destroy();
235
+ reject(new Error('SMS API request timed out'));
236
+ });
237
+ if (postData) req.write(postData);
238
+ req.end();
239
+ });
240
+ }
241
+ }
242
+
243
+ module.exports = { SMS };
@@ -0,0 +1,348 @@
1
+ /**
2
+ * VoltJS Storage
3
+ *
4
+ * File storage abstraction with local filesystem and S3-compatible support.
5
+ *
6
+ * @example
7
+ * const { Storage } = require('voltjs');
8
+ *
9
+ * const storage = new Storage({ root: './uploads' });
10
+ * await storage.put('avatars/user1.png', buffer);
11
+ * const file = await storage.get('avatars/user1.png');
12
+ * const url = storage.url('avatars/user1.png');
13
+ *
14
+ * // Upload middleware
15
+ * app.post('/upload', Storage.upload({ dest: 'uploads', maxSize: 5 * 1024 * 1024 }), handler);
16
+ */
17
+
18
+ 'use strict';
19
+
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+ const crypto = require('crypto');
23
+ const https = require('https');
24
+ const http = require('http');
25
+
26
+ class Storage {
27
+ constructor(options = {}) {
28
+ this.driver = options.driver || 'local';
29
+ this.root = options.root || './storage';
30
+ this.baseURL = options.baseURL || '/storage';
31
+
32
+ // S3 config
33
+ this.s3 = {
34
+ bucket: options.bucket || process.env.S3_BUCKET,
35
+ region: options.region || process.env.S3_REGION || 'us-east-1',
36
+ accessKey: options.accessKey || process.env.S3_ACCESS_KEY,
37
+ secretKey: options.secretKey || process.env.S3_SECRET_KEY,
38
+ endpoint: options.endpoint || process.env.S3_ENDPOINT,
39
+ };
40
+
41
+ if (this.driver === 'local') {
42
+ if (!fs.existsSync(this.root)) fs.mkdirSync(this.root, { recursive: true });
43
+ }
44
+ }
45
+
46
+ /** Store a file */
47
+ async put(filePath, content, options = {}) {
48
+ if (this.driver === 's3') return this._s3Put(filePath, content, options);
49
+
50
+ const fullPath = path.join(this.root, filePath);
51
+ const dir = path.dirname(fullPath);
52
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
53
+
54
+ const data = typeof content === 'string' ? Buffer.from(content) : content;
55
+ fs.writeFileSync(fullPath, data);
56
+
57
+ return {
58
+ path: filePath,
59
+ size: data.length,
60
+ url: this.url(filePath),
61
+ };
62
+ }
63
+
64
+ /** Get file contents */
65
+ async get(filePath) {
66
+ if (this.driver === 's3') return this._s3Get(filePath);
67
+
68
+ const fullPath = path.join(this.root, filePath);
69
+ if (!fs.existsSync(fullPath)) return null;
70
+ return fs.readFileSync(fullPath);
71
+ }
72
+
73
+ /** Get file as string */
74
+ async getText(filePath) {
75
+ const buf = await this.get(filePath);
76
+ return buf ? buf.toString('utf-8') : null;
77
+ }
78
+
79
+ /** Check if file exists */
80
+ async exists(filePath) {
81
+ if (this.driver === 's3') return this._s3Exists(filePath);
82
+ return fs.existsSync(path.join(this.root, filePath));
83
+ }
84
+
85
+ /** Delete a file */
86
+ async delete(filePath) {
87
+ if (this.driver === 's3') return this._s3Delete(filePath);
88
+
89
+ const fullPath = path.join(this.root, filePath);
90
+ if (fs.existsSync(fullPath)) {
91
+ fs.unlinkSync(fullPath);
92
+ return true;
93
+ }
94
+ return false;
95
+ }
96
+
97
+ /** Copy a file */
98
+ async copy(from, to) {
99
+ const content = await this.get(from);
100
+ if (!content) throw new Error(`File not found: ${from}`);
101
+ return this.put(to, content);
102
+ }
103
+
104
+ /** Move a file */
105
+ async move(from, to) {
106
+ await this.copy(from, to);
107
+ await this.delete(from);
108
+ return { path: to, url: this.url(to) };
109
+ }
110
+
111
+ /** Get file metadata */
112
+ async stat(filePath) {
113
+ if (this.driver === 's3') return this._s3Head(filePath);
114
+
115
+ const fullPath = path.join(this.root, filePath);
116
+ if (!fs.existsSync(fullPath)) return null;
117
+ const stat = fs.statSync(fullPath);
118
+ return {
119
+ size: stat.size,
120
+ created: stat.birthtime,
121
+ modified: stat.mtime,
122
+ isFile: stat.isFile(),
123
+ isDirectory: stat.isDirectory(),
124
+ };
125
+ }
126
+
127
+ /** List files in directory */
128
+ async list(directory = '', options = {}) {
129
+ if (this.driver === 's3') return this._s3List(directory);
130
+
131
+ const fullPath = path.join(this.root, directory);
132
+ if (!fs.existsSync(fullPath)) return [];
133
+
134
+ const items = fs.readdirSync(fullPath, { withFileTypes: true });
135
+ return items.map(item => ({
136
+ name: item.name,
137
+ path: path.join(directory, item.name).replace(/\\/g, '/'),
138
+ isFile: item.isFile(),
139
+ isDirectory: item.isDirectory(),
140
+ }));
141
+ }
142
+
143
+ /** Get public URL for file */
144
+ url(filePath) {
145
+ if (this.driver === 's3') {
146
+ const endpoint = this.s3.endpoint || `https://${this.s3.bucket}.s3.${this.s3.region}.amazonaws.com`;
147
+ return `${endpoint}/${filePath}`;
148
+ }
149
+ return `${this.baseURL}/${filePath}`;
150
+ }
151
+
152
+ /** Create a hash of file contents */
153
+ async hash(filePath, algorithm = 'md5') {
154
+ const content = await this.get(filePath);
155
+ if (!content) return null;
156
+ return crypto.createHash(algorithm).update(content).digest('hex');
157
+ }
158
+
159
+ /** Generate a unique filename */
160
+ static uniqueName(originalName) {
161
+ const ext = path.extname(originalName);
162
+ const timestamp = Date.now().toString(36);
163
+ const random = crypto.randomBytes(6).toString('hex');
164
+ return `${timestamp}-${random}${ext}`;
165
+ }
166
+
167
+ /** Upload middleware for Volt/Express apps */
168
+ static upload(options = {}) {
169
+ const dest = options.dest || 'uploads';
170
+ const maxSize = options.maxSize || 10 * 1024 * 1024; // 10MB
171
+ const allowedTypes = options.allowedTypes || null; // ['image/png', 'image/jpeg']
172
+ const storage = new Storage({ root: dest });
173
+
174
+ return async (req, res) => {
175
+ if (!req.files && !req.body?._files) return;
176
+
177
+ const files = req.files || req.body?._files || [];
178
+ const uploaded = [];
179
+
180
+ for (const file of (Array.isArray(files) ? files : [files])) {
181
+ // Size check
182
+ if (file.data && file.data.length > maxSize) {
183
+ res.json({ error: `File too large. Maximum: ${(maxSize / 1024 / 1024).toFixed(0)}MB` }, 413);
184
+ return false;
185
+ }
186
+
187
+ // Type check
188
+ if (allowedTypes && !allowedTypes.includes(file.type)) {
189
+ res.json({ error: `File type not allowed: ${file.type}` }, 415);
190
+ return false;
191
+ }
192
+
193
+ const filename = Storage.uniqueName(file.name || 'upload');
194
+ const result = await storage.put(filename, file.data);
195
+ uploaded.push({ ...result, originalName: file.name, type: file.type });
196
+ }
197
+
198
+ req.uploadedFiles = uploaded;
199
+ };
200
+ }
201
+
202
+ /** Serve static files middleware */
203
+ static serve(root, options = {}) {
204
+ const MIME_TYPES = {
205
+ '.html': 'text/html', '.css': 'text/css', '.js': 'application/javascript',
206
+ '.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg',
207
+ '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.svg': 'image/svg+xml',
208
+ '.ico': 'image/x-icon', '.woff': 'font/woff', '.woff2': 'font/woff2',
209
+ '.ttf': 'font/ttf', '.pdf': 'application/pdf', '.mp4': 'video/mp4',
210
+ '.webp': 'image/webp', '.webm': 'video/webm', '.mp3': 'audio/mpeg',
211
+ '.wav': 'audio/wav', '.txt': 'text/plain', '.xml': 'application/xml',
212
+ };
213
+
214
+ const prefix = options.prefix || '';
215
+
216
+ return (req, res) => {
217
+ if (req.method !== 'GET') return;
218
+
219
+ let urlPath = req.url.split('?')[0];
220
+ if (prefix && !urlPath.startsWith(prefix)) return;
221
+ if (prefix) urlPath = urlPath.slice(prefix.length);
222
+
223
+ const filePath = path.join(root, urlPath);
224
+
225
+ // Security: prevent directory traversal
226
+ if (!filePath.startsWith(path.resolve(root))) return;
227
+
228
+ if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) return;
229
+
230
+ const ext = path.extname(filePath);
231
+ const contentType = MIME_TYPES[ext] || 'application/octet-stream';
232
+
233
+ // Cache headers
234
+ if (options.maxAge) {
235
+ res.setHeader('Cache-Control', `public, max-age=${options.maxAge}`);
236
+ }
237
+
238
+ res.setHeader('Content-Type', contentType);
239
+ const stream = fs.createReadStream(filePath);
240
+ stream.pipe(res);
241
+ return false; // Stop middleware chain
242
+ };
243
+ }
244
+
245
+ // ===== S3 METHODS =====
246
+
247
+ async _s3Put(key, content, options = {}) {
248
+ const data = typeof content === 'string' ? Buffer.from(content) : content;
249
+ const contentType = options.contentType || 'application/octet-stream';
250
+
251
+ await this._s3Request('PUT', `/${key}`, data, {
252
+ 'Content-Type': contentType,
253
+ 'Content-Length': data.length,
254
+ });
255
+
256
+ return { path: key, size: data.length, url: this.url(key) };
257
+ }
258
+
259
+ async _s3Get(key) {
260
+ try {
261
+ return await this._s3Request('GET', `/${key}`);
262
+ } catch {
263
+ return null;
264
+ }
265
+ }
266
+
267
+ async _s3Delete(key) {
268
+ try {
269
+ await this._s3Request('DELETE', `/${key}`);
270
+ return true;
271
+ } catch {
272
+ return false;
273
+ }
274
+ }
275
+
276
+ async _s3Exists(key) {
277
+ try {
278
+ await this._s3Request('HEAD', `/${key}`);
279
+ return true;
280
+ } catch {
281
+ return false;
282
+ }
283
+ }
284
+
285
+ async _s3Head(key) {
286
+ try {
287
+ const res = await this._s3Request('HEAD', `/${key}`);
288
+ return { size: parseInt(res.headers?.['content-length'] || 0) };
289
+ } catch {
290
+ return null;
291
+ }
292
+ }
293
+
294
+ async _s3List(prefix = '') {
295
+ const params = prefix ? `?prefix=${encodeURIComponent(prefix)}` : '';
296
+ const body = await this._s3Request('GET', `/${params}`);
297
+ return body; // Raw XML — consumer should parse
298
+ }
299
+
300
+ _s3Request(method, path, body, extraHeaders = {}) {
301
+ return new Promise((resolve, reject) => {
302
+ const date = new Date().toUTCString();
303
+ const host = this.s3.endpoint
304
+ ? new URL(this.s3.endpoint).host
305
+ : `${this.s3.bucket}.s3.${this.s3.region}.amazonaws.com`;
306
+
307
+ const headers = {
308
+ Host: host,
309
+ Date: date,
310
+ ...extraHeaders,
311
+ };
312
+
313
+ // Simple AWS v2 signature
314
+ if (this.s3.accessKey && this.s3.secretKey) {
315
+ const stringToSign = `${method}\n\n${headers['Content-Type'] || ''}\n${date}\n/${this.s3.bucket}${path}`;
316
+ const signature = crypto.createHmac('sha1', this.s3.secretKey).update(stringToSign).digest('base64');
317
+ headers.Authorization = `AWS ${this.s3.accessKey}:${signature}`;
318
+ }
319
+
320
+ const options = {
321
+ hostname: host,
322
+ port: 443,
323
+ path,
324
+ method,
325
+ headers,
326
+ };
327
+
328
+ const req = https.request(options, (res) => {
329
+ const chunks = [];
330
+ res.on('data', c => chunks.push(c));
331
+ res.on('end', () => {
332
+ const buf = Buffer.concat(chunks);
333
+ if (res.statusCode >= 400) {
334
+ reject(new Error(`S3 ${method} ${path} failed: ${res.statusCode}`));
335
+ } else {
336
+ resolve(method === 'HEAD' ? { headers: res.headers } : buf);
337
+ }
338
+ });
339
+ });
340
+
341
+ req.on('error', reject);
342
+ if (body) req.write(body);
343
+ req.end();
344
+ });
345
+ }
346
+ }
347
+
348
+ module.exports = { Storage };