free-framework 4.4.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 (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +198 -0
  3. package/bin/free.js +118 -0
  4. package/cli/commands/build.js +124 -0
  5. package/cli/commands/deploy.js +143 -0
  6. package/cli/commands/devtools.js +210 -0
  7. package/cli/commands/doctor.js +72 -0
  8. package/cli/commands/install.js +28 -0
  9. package/cli/commands/make.js +74 -0
  10. package/cli/commands/migrate.js +67 -0
  11. package/cli/commands/new.js +54 -0
  12. package/cli/commands/serve.js +73 -0
  13. package/cli/commands/test.js +57 -0
  14. package/compiler/analyzer.js +102 -0
  15. package/compiler/generator.js +386 -0
  16. package/compiler/lexer.js +166 -0
  17. package/compiler/parser.js +410 -0
  18. package/database/model.js +6 -0
  19. package/database/orm.js +379 -0
  20. package/database/query-builder.js +179 -0
  21. package/index.js +51 -0
  22. package/package.json +80 -0
  23. package/plugins/auth.js +212 -0
  24. package/plugins/cache.js +85 -0
  25. package/plugins/chat.js +32 -0
  26. package/plugins/mail.js +53 -0
  27. package/plugins/metrics.js +126 -0
  28. package/plugins/payments.js +59 -0
  29. package/plugins/queue.js +139 -0
  30. package/plugins/search.js +51 -0
  31. package/plugins/storage.js +123 -0
  32. package/plugins/upload.js +62 -0
  33. package/router/router.js +57 -0
  34. package/runtime/app.js +14 -0
  35. package/runtime/client.js +254 -0
  36. package/runtime/cluster.js +35 -0
  37. package/runtime/edge.js +62 -0
  38. package/runtime/middleware/maintenance.js +54 -0
  39. package/runtime/middleware/security.js +30 -0
  40. package/runtime/server.js +130 -0
  41. package/runtime/validator.js +102 -0
  42. package/runtime/views/error.free +104 -0
  43. package/runtime/views/maintenance.free +0 -0
  44. package/template-engine/renderer.js +24 -0
  45. package/templates/app-template/.env +23 -0
  46. package/templates/app-template/app/Exceptions/Handler.js +65 -0
  47. package/templates/app-template/app/Http/Controllers/AuthController.free +91 -0
  48. package/templates/app-template/app/Http/Middleware/AuthGuard.js +46 -0
  49. package/templates/app-template/app/Services/Storage.js +75 -0
  50. package/templates/app-template/app/Services/Validator.js +91 -0
  51. package/templates/app-template/app/controllers/AuthController.free +42 -0
  52. package/templates/app-template/app/middleware/auth.js +25 -0
  53. package/templates/app-template/app/models/User.free +32 -0
  54. package/templates/app-template/app/routes.free +12 -0
  55. package/templates/app-template/app/styles.css +23 -0
  56. package/templates/app-template/app/views/counter.free +23 -0
  57. package/templates/app-template/app/views/header.free +13 -0
  58. package/templates/app-template/config/app.js +32 -0
  59. package/templates/app-template/config/auth.js +39 -0
  60. package/templates/app-template/config/database.js +54 -0
  61. package/templates/app-template/package.json +28 -0
  62. package/templates/app-template/resources/css/app.css +11 -0
  63. package/templates/app-template/resources/views/dashboard.free +25 -0
  64. package/templates/app-template/resources/views/home.free +26 -0
  65. package/templates/app-template/routes/api.free +22 -0
  66. package/templates/app-template/routes/web.free +25 -0
  67. package/templates/app-template/tailwind.config.js +21 -0
  68. package/templates/app-template/views/about.ejs +47 -0
  69. package/templates/app-template/views/home.ejs +52 -0
  70. package/templates/auth/login.html +144 -0
  71. package/templates/auth/register.html +146 -0
  72. package/utils/logger.js +20 -0
@@ -0,0 +1,212 @@
1
+ /**
2
+ * plugins/auth.js
3
+ * Official Authentication System for Free Framework.
4
+ * Features: JWT auth, session cookies, password reset tokens, email verification.
5
+ */
6
+ const jwt = require('jsonwebtoken');
7
+ const bcrypt = require('bcrypt');
8
+ const crypto = require('crypto');
9
+
10
+ // In-memory token store — in production, persist to Redis/DB
11
+ const resetTokens = new Map(); // token → { email, expiresAt }
12
+ const verifyTokens = new Map(); // token → { email, expiresAt }
13
+
14
+ function AuthPlugin(config = {}) {
15
+ return {
16
+ install(server) {
17
+ const secret = process.env.JWT_SECRET || 'free-ultra-secret-key';
18
+ const loginPath = config.login || '/login';
19
+ const resetExpiry = config.resetExpiry || 60 * 60 * 1000; // 1 hour
20
+
21
+ // ── Global Middleware: Token Verification ───────────────────────
22
+ server.middleware((req, res, next) => {
23
+ // Support both cookie-based and Bearer token auth
24
+ let token = (req.cookies && req.cookies.free_token);
25
+ if (!token) {
26
+ const authHeader = req.headers['authorization'] || '';
27
+ token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : null;
28
+ }
29
+
30
+ if (token) {
31
+ try {
32
+ const decoded = jwt.verify(token, secret);
33
+ res.context = res.context || {};
34
+ res.context.user = decoded;
35
+ } catch (e) {
36
+ // Invalid token? Just ignore, contextual user remains null
37
+ }
38
+ }
39
+
40
+ // Auth helper methods
41
+ res.login = (userObj, expiresIn = '7d') => {
42
+ const token = jwt.sign(userObj, secret, { expiresIn });
43
+ const isProd = process.env.NODE_ENV === 'production';
44
+ res.cookie('free_token', token, {
45
+ httpOnly: true,
46
+ secure: isProd,
47
+ sameSite: isProd ? 'none' : 'lax', // 'none' requires Secure
48
+ maxAge: 7 * 24 * 60 * 60 * 1000,
49
+ });
50
+ return token;
51
+ };
52
+ res.logout = () => res.clearCookie('free_token');
53
+
54
+ // Password helpers
55
+ req.hashPassword = (pw) => bcrypt.hash(pw, 12);
56
+ req.verifyPassword = (pw, hash) => bcrypt.compare(pw, hash);
57
+
58
+ next();
59
+ });
60
+
61
+ // ── Named 'auth' middleware: Enforcement only ───────────────────
62
+ const authMW = (req, res, next) => {
63
+ if (res.context && res.context.user) {
64
+ return next();
65
+ }
66
+ res.redirect(loginPath);
67
+ };
68
+ server.registerMiddleware('auth', authMW);
69
+ server.registerMiddleware('AuthGuard', authMW);
70
+
71
+ // ── Built-in Auth API Endpoints ──────────────────────────────────
72
+ server.app.post('/_free/auth/register', async (req, res) => {
73
+ try {
74
+ console.log("📝 Auth: Registration request received");
75
+ const body = await req.json().catch(() => ({}));
76
+ const { name, email, password } = body;
77
+ if (!email || !password) return res.status(400).json({ success: false, error: 'Email and password are required' });
78
+
79
+ // Use server-attached modelsRegistry to avoid circular require
80
+ const User = server.modelsRegistry && server.modelsRegistry['User'];
81
+ if (!User) {
82
+ console.error("❌ Auth Error: User model not found in registry.");
83
+ return res.status(500).json({ success: false, error: 'User model not found.' });
84
+ }
85
+
86
+ console.log(`🔍 Auth: Checking if user ${email} exists...`);
87
+ const existing = await User.where('email', email).first();
88
+ if (existing) {
89
+ console.log(`⚠️ Auth: Email ${email} already registered.`);
90
+ return res.status(409).json({ success: false, error: 'Email already registered' });
91
+ }
92
+
93
+ console.log("🔐 Auth: Hashing password...");
94
+ // Use bcryptjs safely or native bcrypt
95
+ const hashed = await bcrypt.hash(password, 12);
96
+
97
+ console.log("💾 Auth: Creating user record...");
98
+ const user = await User.create({ name: name || email.split('@')[0], email, password: hashed });
99
+
100
+ console.log(`✅ Auth: User ${user.id} registered successfully.`);
101
+ res.login({ id: user.id, email, name: user.name });
102
+ res.json({ success: true, user: { id: user.id, email, name: user.name } });
103
+ } catch (e) {
104
+ console.error("❌ Auth Registration Exception:", e);
105
+ res.status(500).json({ success: false, error: e.message });
106
+ }
107
+ });
108
+
109
+ server.app.post('/_free/auth/login', async (req, res) => {
110
+ try {
111
+ const body = await req.json().catch(() => ({}));
112
+ const { email, password } = body;
113
+ if (!email || !password) return res.status(400).json({ success: false, error: 'Email and password are required' });
114
+
115
+ const User = server.modelsRegistry && server.modelsRegistry['User'];
116
+ if (!User) return res.status(500).json({ success: false, error: 'User model not found.' });
117
+
118
+ const user = await User.where('email', email).first();
119
+ if (!user) return res.status(401).json({ success: false, error: 'Invalid credentials' });
120
+
121
+ const match = await bcrypt.compare(password, user.password);
122
+ if (!match) return res.status(401).json({ success: false, error: 'Invalid credentials' });
123
+
124
+ res.login({ id: user.id, email: user.email, name: user.name });
125
+ res.json({ success: true, user: { id: user.id, email: user.email, name: user.name } });
126
+ } catch (e) {
127
+ res.status(500).json({ success: false, error: e.message });
128
+ }
129
+ });
130
+
131
+ server.app.post('/_free/auth/logout', (req, res) => {
132
+ res.clearCookie('free_token');
133
+ res.json({ success: true });
134
+ });
135
+
136
+ // ── Password Reset ───────────────────────────────────────────────
137
+
138
+ /**
139
+ * Generate a password reset token for an email.
140
+ * @returns {string} reset token — send this via email
141
+ */
142
+ server.auth = server.auth || {};
143
+
144
+ server.auth.createResetToken = async (email) => {
145
+ const token = crypto.randomBytes(32).toString('hex');
146
+ if (server.cache) {
147
+ await server.cache.set(`reset_token:${token}`, email, resetExpiry / 1000);
148
+ } else {
149
+ resetTokens.set(token, { email, expiresAt: Date.now() + resetExpiry });
150
+ }
151
+ return token;
152
+ };
153
+
154
+ server.auth.verifyResetToken = async (token) => {
155
+ if (server.cache) {
156
+ return await server.cache.get(`reset_token:${token}`);
157
+ }
158
+ const entry = resetTokens.get(token);
159
+ if (!entry) return null;
160
+ if (Date.now() > entry.expiresAt) {
161
+ resetTokens.delete(token);
162
+ return null;
163
+ }
164
+ return entry.email;
165
+ };
166
+
167
+ server.auth.consumeResetToken = async (token) => {
168
+ const email = await server.auth.verifyResetToken(token);
169
+ if (email) {
170
+ if (server.cache) await server.cache.forget(`reset_token:${token}`);
171
+ else resetTokens.delete(token);
172
+ }
173
+ return email;
174
+ };
175
+
176
+ // ── Email Verification ───────────────────────────────────────────
177
+
178
+ server.auth.createVerifyToken = async (email) => {
179
+ const token = crypto.randomBytes(32).toString('hex');
180
+ if (server.cache) {
181
+ await server.cache.set(`verify_token:${token}`, email, resetExpiry / 1000);
182
+ } else {
183
+ verifyTokens.set(token, { email, expiresAt: Date.now() + resetExpiry });
184
+ }
185
+ return token;
186
+ };
187
+
188
+ server.auth.consumeVerifyToken = async (token) => {
189
+ let email = null;
190
+ if (server.cache) {
191
+ email = await server.cache.get(`verify_token:${token}`);
192
+ if (email) await server.cache.forget(`verify_token:${token}`);
193
+ } else {
194
+ const entry = verifyTokens.get(token);
195
+ if (entry && Date.now() <= entry.expiresAt) {
196
+ email = entry.email;
197
+ }
198
+ verifyTokens.delete(token);
199
+ }
200
+ return email;
201
+ };
202
+
203
+ // ── JWT token generator (for API use) ────────────────────────────
204
+ server.auth.sign = (payload, expiresIn = '7d') => jwt.sign(payload, secret, { expiresIn });
205
+ server.auth.verify = (token) => jwt.verify(token, secret);
206
+
207
+ console.log("🔒 Auth Plugin installed. JWT + sessions + password reset + email verify.");
208
+ }
209
+ };
210
+ }
211
+
212
+ module.exports = AuthPlugin;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * plugins/cache.js
3
+ * Application Cache Plugin for Free Framework.
4
+ * Drivers: memory (LRU), redis.
5
+ *
6
+ * Usage:
7
+ * server.cache.set("posts", data, 60); // 60 seconds TTL
8
+ * server.cache.get("posts"); // returns data or null
9
+ * server.cache.forget("posts"); // invalidate
10
+ * server.cache.flush(); // clear all
11
+ * server.cache.stats(); // hit/miss/size info
12
+ */
13
+
14
+ const { LRUCache } = require('lru-cache');
15
+
16
+ function CachePlugin(config = {}) {
17
+ const driver = config.driver || 'memory'; // 'memory' | 'redis'
18
+
19
+ return {
20
+ install(server) {
21
+ let backend;
22
+
23
+ if (driver === 'redis') {
24
+ // Redis driver — requires: npm install ioredis
25
+ const Redis = (() => {
26
+ try { return require('ioredis'); }
27
+ catch { throw new Error('CachePlugin redis driver: run npm install ioredis'); }
28
+ })();
29
+ const redis = new Redis(config.redis || process.env.REDIS_URL || 'redis://127.0.0.1:6379');
30
+ let hits = 0, misses = 0;
31
+
32
+ backend = {
33
+ async set(key, value, ttlSeconds = 300) {
34
+ await redis.set(key, JSON.stringify(value), 'EX', ttlSeconds);
35
+ },
36
+ async get(key) {
37
+ const raw = await redis.get(key);
38
+ if (raw === null) { misses++; return null; }
39
+ hits++;
40
+ return JSON.parse(raw);
41
+ },
42
+ async forget(key) { await redis.del(key); },
43
+ async flush() { await redis.flushdb(); },
44
+ stats() { return { driver: 'redis', hits, misses }; },
45
+ };
46
+ } else {
47
+ // Memory driver (LRU)
48
+ const cache = new LRUCache({
49
+ max: config.max || 1000,
50
+ ttl: (config.defaultTtl || 300) * 1000, // ms
51
+ });
52
+ let hits = 0, misses = 0;
53
+
54
+ backend = {
55
+ async set(key, value, ttlSeconds) {
56
+ cache.set(key, value, ttlSeconds ? { ttl: ttlSeconds * 1000 } : undefined);
57
+ },
58
+ async get(key) {
59
+ const val = cache.get(key);
60
+ if (val === undefined) { misses++; return null; }
61
+ hits++;
62
+ return val;
63
+ },
64
+ async forget(key) { cache.delete(key); },
65
+ async flush() { cache.clear(); },
66
+ stats() {
67
+ return {
68
+ driver: 'memory',
69
+ size: cache.size,
70
+ maxSize: cache.max,
71
+ hits,
72
+ misses,
73
+ hitRate: hits + misses > 0 ? ((hits / (hits + misses)) * 100).toFixed(1) + '%' : 'N/A',
74
+ };
75
+ },
76
+ };
77
+ }
78
+
79
+ server.cache = backend;
80
+ console.log(`🗄️ Cache Plugin installed. Driver: ${driver}`);
81
+ }
82
+ };
83
+ }
84
+
85
+ module.exports = CachePlugin;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * plugins/chat.js
3
+ * Real-Time Chat Plugin for Free Framework.
4
+ * Exposes WebSocket Server over HyperExpress using native pub/sub capabilities.
5
+ */
6
+
7
+ function ChatPlugin() {
8
+ return {
9
+ install(server) {
10
+ server.app.ws('/ws/chat', {
11
+ idle_timeout: 120,
12
+ open: (ws) => {
13
+ ws.subscribe('global_chat');
14
+ // Notify room
15
+ server.app.publish('global_chat', '👋 A new user has joined the chat.');
16
+ },
17
+ message: (ws, message, isBinary) => {
18
+ const text = Buffer.from(message).toString();
19
+ // Echo message to everyone in the room
20
+ server.app.publish('global_chat', `💬 User: ${text}`);
21
+ },
22
+ close: (ws, code, message) => {
23
+ server.app.publish('global_chat', '🚪 A user left the chat.');
24
+ }
25
+ });
26
+
27
+ console.log("🟢 Chat Plugin installed. WebSocket active on ws://localhost/ws/chat");
28
+ }
29
+ };
30
+ }
31
+
32
+ module.exports = ChatPlugin;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * plugins/mail.js
3
+ * Official Mail Plugin for Free Framework.
4
+ * Powered by Nodemailer with support for SMTP and JSON-based templates.
5
+ */
6
+
7
+ const nodemailer = require('nodemailer');
8
+
9
+ function MailPlugin(config = {}) {
10
+ let transporter = nodemailer.createTransport(config.transport || {
11
+ host: process.env.MAIL_HOST || 'localhost',
12
+ port: process.env.MAIL_PORT || 1025,
13
+ auth: {
14
+ user: process.env.MAIL_USER,
15
+ pass: process.env.MAIL_PASS
16
+ }
17
+ });
18
+
19
+ return {
20
+ install(server) {
21
+ server.mail = {
22
+ /**
23
+ * Send a basic email.
24
+ */
25
+ async send(options) {
26
+ return await transporter.sendMail({
27
+ from: options.from || config.from || '"Free Framework" <no-reply@freejs.dev>',
28
+ to: options.to,
29
+ subject: options.subject,
30
+ text: options.text,
31
+ html: options.html
32
+ });
33
+ },
34
+
35
+ /**
36
+ * Send using a template (Simple placeholder replacement).
37
+ */
38
+ async sendTemplate(to, subject, templateHtml, data = {}) {
39
+ let html = templateHtml;
40
+ Object.keys(data).forEach(key => {
41
+ const regex = new RegExp(`{{${key}}}`, 'g');
42
+ html = html.replace(regex, data[key]);
43
+ });
44
+ return this.send({ to, subject, html });
45
+ }
46
+ };
47
+
48
+ console.log('📧 Mail Plugin installed (Ready to send)');
49
+ }
50
+ };
51
+ }
52
+
53
+ module.exports = MailPlugin;
@@ -0,0 +1,126 @@
1
+ /**
2
+ * plugins/metrics.js
3
+ * Observability Plugin for Free Framework.
4
+ * Exposes:
5
+ * GET /metrics — Prometheus-compatible metrics
6
+ * GET /health — Health check JSON
7
+ * Live request tracing (configurable via LOG_REQUESTS=true)
8
+ */
9
+
10
+ function MetricsPlugin(config = {}) {
11
+ return {
12
+ install(server) {
13
+ const startTime = Date.now();
14
+ const counters = {
15
+ requests_total: 0,
16
+ requests_success: 0,
17
+ requests_error: 0,
18
+ };
19
+ const durations = []; // last 1000 request durations (ms)
20
+ const routeCounts = {};
21
+
22
+ // ── Request tracing middleware ───────────────────────────────────
23
+ server.app.use((req, res, next) => {
24
+ const start = Date.now();
25
+ counters.requests_total++;
26
+ const routeKey = `${req.method} ${req.path}`;
27
+ routeCounts[routeKey] = (routeCounts[routeKey] || 0) + 1;
28
+
29
+ const originalSend = res.send.bind(res);
30
+ res.send = (body) => {
31
+ const duration = Date.now() - start;
32
+ durations.push(duration);
33
+ if (durations.length > 1000) durations.shift(); // rolling window
34
+
35
+ if ((res.statusCode || 200) >= 400) {
36
+ counters.requests_error++;
37
+ } else {
38
+ counters.requests_success++;
39
+ }
40
+
41
+ if (process.env.LOG_REQUESTS === 'true') {
42
+ const status = res.statusCode || 200;
43
+ const color = status >= 500 ? '\x1b[31m' : status >= 400 ? '\x1b[33m' : '\x1b[32m';
44
+ console.log(`${color}${req.method}\x1b[0m ${req.path} \x1b[90m${status} ${duration}ms\x1b[0m`);
45
+ }
46
+
47
+ return originalSend(body);
48
+ };
49
+
50
+ next();
51
+ });
52
+
53
+ // ── Helper computations ──────────────────────────────────────────
54
+ function getPercentile(arr, p) {
55
+ if (arr.length === 0) return 0;
56
+ const sorted = [...arr].sort((a, b) => a - b);
57
+ const idx = Math.floor((p / 100) * sorted.length);
58
+ return sorted[idx] || 0;
59
+ }
60
+
61
+ function getMemory() {
62
+ const m = process.memoryUsage();
63
+ return {
64
+ heap_used_mb: (m.heapUsed / 1024 / 1024).toFixed(2),
65
+ heap_total_mb: (m.heapTotal / 1024 / 1024).toFixed(2),
66
+ rss_mb: (m.rss / 1024 / 1024).toFixed(2),
67
+ };
68
+ }
69
+
70
+ // ── Health endpoint ──────────────────────────────────────────────
71
+ server.app.get('/health', (req, res) => {
72
+ const mem = getMemory();
73
+ res.json({
74
+ status: 'ok',
75
+ uptime: `${((Date.now() - startTime) / 1000).toFixed(0)}s`,
76
+ memory: mem,
77
+ requests: {
78
+ total: counters.requests_total,
79
+ success: counters.requests_success,
80
+ error: counters.requests_error,
81
+ },
82
+ });
83
+ });
84
+
85
+ // ── Prometheus-compatible /metrics ───────────────────────────────
86
+ server.app.get('/metrics', (req, res) => {
87
+ const mem = getMemory();
88
+ const avg = durations.length > 0 ? (durations.reduce((a, b) => a + b, 0) / durations.length).toFixed(1) : 0;
89
+ const p95 = getPercentile(durations, 95);
90
+ const p99 = getPercentile(durations, 99);
91
+ const uptime = ((Date.now() - startTime) / 1000).toFixed(0);
92
+
93
+ let out = '';
94
+ out += `# HELP free_requests_total Total HTTP requests\n`;
95
+ out += `# TYPE free_requests_total counter\n`;
96
+ out += `free_requests_total ${counters.requests_total}\n\n`;
97
+ out += `free_requests_success ${counters.requests_success}\n`;
98
+ out += `free_requests_error ${counters.requests_error}\n\n`;
99
+
100
+ out += `# HELP free_request_duration_ms Request duration percentiles\n`;
101
+ out += `free_request_duration_avg ${avg}\n`;
102
+ out += `free_request_duration_p95 ${p95}\n`;
103
+ out += `free_request_duration_p99 ${p99}\n\n`;
104
+
105
+ out += `# HELP free_memory_heap_used_bytes Heap memory used\n`;
106
+ out += `free_memory_heap_used_mb ${mem.heap_used_mb}\n`;
107
+ out += `free_memory_rss_mb ${mem.rss_mb}\n\n`;
108
+
109
+ out += `free_uptime_seconds ${uptime}\n\n`;
110
+
111
+ out += `# HELP free_route_requests_total Requests per route\n`;
112
+ for (const [route, count] of Object.entries(routeCounts)) {
113
+ const safeLabel = route.replace(/"/g, '\\"');
114
+ out += `free_route_requests_total{route="${safeLabel}"} ${count}\n`;
115
+ }
116
+
117
+ res.setHeader('Content-Type', 'text/plain; version=0.0.4');
118
+ res.send(out);
119
+ });
120
+
121
+ console.log('📊 Metrics Plugin installed. Endpoints: GET /health GET /metrics');
122
+ }
123
+ };
124
+ }
125
+
126
+ module.exports = MetricsPlugin;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * plugins/payments.js
3
+ * Payments Plugin for Free Framework.
4
+ * Native integration with Stripe and unified checkout flow.
5
+ */
6
+
7
+ function PaymentsPlugin(config = {}) {
8
+ const stripeKey = config.secretKey || process.env.STRIPE_SECRET_KEY;
9
+ let stripe = null;
10
+
11
+ return {
12
+ install(server) {
13
+ if (stripeKey) {
14
+ try {
15
+ const Stripe = require('stripe');
16
+ stripe = new Stripe(stripeKey);
17
+ } catch (e) {
18
+ console.warn('PaymentsPlugin: Stripe module not found. Run npm install stripe');
19
+ }
20
+ }
21
+
22
+ server.payments = {
23
+ /**
24
+ * Create a Stripe Checkout session.
25
+ */
26
+ async createCheckout(items, successUrl, cancelUrl) {
27
+ if (!stripe) throw new Error('Stripe not initialized. Check STRIPE_SECRET_KEY');
28
+
29
+ return await stripe.checkout.sessions.create({
30
+ payment_method_types: ['card'],
31
+ line_items: items.map(item => ({
32
+ price_data: {
33
+ currency: config.currency || 'usd',
34
+ product_data: { name: item.name },
35
+ unit_amount: item.amount, // in cents
36
+ },
37
+ quantity: item.quantity || 1,
38
+ })),
39
+ mode: 'payment',
40
+ success_url: successUrl,
41
+ cancel_url: cancelUrl,
42
+ });
43
+ },
44
+
45
+ /**
46
+ * Handle webhook events (e.g. payment success notification)
47
+ */
48
+ async verifyWebhook(body, sig, secret) {
49
+ if (!stripe) return null;
50
+ return stripe.webhooks.constructEvent(body, sig, secret);
51
+ }
52
+ };
53
+
54
+ console.log('💳 Payments Plugin installed (Stripe Ready)');
55
+ }
56
+ };
57
+ }
58
+
59
+ module.exports = PaymentsPlugin;