@intranefr/superbackend 1.4.3 → 1.4.4

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/.env.example CHANGED
@@ -43,5 +43,10 @@ MAX_FILE_SIZE_HARD_CAP=10485760
43
43
  # S3_REGION=us-east-1
44
44
  # S3_ACCESS_KEY_ID=minioadmin
45
45
  # S3_SECRET_ACCESS_KEY=minioadmin
46
- # S3_BUCKET=saasbackend
46
+ # S3_BUCKET=superbackend
47
+ # Legacy fallback: S3_BUCKET=saasbackend
47
48
  # S3_FORCE_PATH_STYLE=true
49
+
50
+ # Encryption key for encrypted settings (new preferred name)
51
+ # SUPERBACKEND_ENCRYPTION_KEY=your-32-byte-encryption-key
52
+ # Legacy fallback: SAASBACKEND_ENCRYPTION_KEY=your-32-byte-encryption-key
package/README.md CHANGED
@@ -29,13 +29,13 @@ Node.js middleware that gives your project backend superpowers. Handles authenti
29
29
  ## Installation
30
30
 
31
31
  ```bash
32
- npm install superbackend
32
+ npm install @intranefr/superbackend
33
33
  ```
34
34
 
35
35
  or
36
36
 
37
37
  ```bash
38
- yarn add superbackend
38
+ yarn add @intranefr/superbackend
39
39
  ```
40
40
 
41
41
  ---
@@ -45,7 +45,7 @@ yarn add superbackend
45
45
  ```javascript
46
46
  require('dotenv').config();
47
47
  const express = require('express');
48
- const { middleware } = require('saasbackend');
48
+ const { middleware } = require('@intranefr/superbackend');
49
49
 
50
50
  const app = express();
51
51
 
@@ -101,8 +101,8 @@ Please read the [CONTRIBUTING.md](#) for guidelines.
101
101
  <img src="https://img.shields.io/badge/Intrane-intranefr-blue?style=flat-square" alt="Intrane"/>
102
102
  </a>
103
103
  &nbsp;
104
- <a href="https://www.npmjs.com/package/superbackend" target="_blank">
105
- <img src="https://img.shields.io/npm/v/superbackend?style=flat-square" alt="npm"/>
104
+ <a href="https://www.npmjs.com/package/@intranefr/superbackend" target="_blank">
105
+ <img src="https://img.shields.io/npm/v/@intranefr%2Fsuperbackend?style=flat-square" alt="npm"/>
106
106
  </a>
107
107
 
108
108
  ## License
package/index.js CHANGED
@@ -2,7 +2,7 @@ require("dotenv").config({ path: process.env.ENV_FILE || ".env" });
2
2
  const express = require("express");
3
3
 
4
4
  /**
5
- * Creates the SaaS backend as Express middleware
5
+ * Creates the SuperBackend as Express middleware
6
6
  * @param {Object} options - Configuration options
7
7
  * @param {string} options.mongodbUri - MongoDB connection string
8
8
  * @param {string} options.corsOrigin - CORS origin(s)
@@ -13,7 +13,7 @@ const express = require("express");
13
13
  const middleware = require("./src/middleware");
14
14
 
15
15
  /**
16
- * Creates and starts a standalone SaaS backend server
16
+ * Creates and starts a standalone SuperBackend server
17
17
  * @param {Object} options - Configuration options
18
18
  * @param {number} options.port - Port to listen on
19
19
  * @param {string} options.mongodbUri - MongoDB connection string
@@ -28,7 +28,7 @@ function startServer(options = {}) {
28
28
 
29
29
  // Start server
30
30
  const server = app.listen(PORT, () => {
31
- console.log(`🚀 SaaSBackend standalone server running on http://localhost:${PORT}`);
31
+ console.log(`🚀 SuperBackend standalone server running on http://localhost:${PORT}`);
32
32
  });
33
33
 
34
34
  return { app, server };
@@ -36,8 +36,11 @@ function startServer(options = {}) {
36
36
 
37
37
  const saasbackend = {
38
38
  server: startServer,
39
+ consoleOverride: require("./src/services/consoleOverride.service"),
39
40
  middleware: (options = {}) => {
40
- globalThis.saasbackend = saasbackend;
41
+ // Set both registries for backward compatibility
42
+ globalThis.superbackend = saasbackend;
43
+ globalThis.saasbackend = saasbackend; // Legacy support
41
44
  return middleware(options);
42
45
  },
43
46
  services: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intranefr/superbackend",
3
- "version": "1.4.3",
3
+ "version": "1.4.4",
4
4
  "description": "Node.js middleware that gives your project backend superpowers",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -1,6 +1,7 @@
1
1
  {
2
- "name": "@saasbackend/error-tracking-browser-sdk",
3
- "version": "1.0.0",
2
+ "name": "@intranefr/superbackend-error-tracking-browser-sdk",
3
+ "version": "2.0.0",
4
+ "description": "Error tracking SDK for SuperBackend browser applications.",
4
5
  "type": "module",
5
6
  "exports": {
6
7
  ".": {
@@ -8,7 +9,7 @@
8
9
  }
9
10
  },
10
11
  "scripts": {
11
- "build": "esbuild src/embed.js --bundle --format=iife --global-name=saasbackendErrorTrackingEmbed --outfile=dist/embed.iife.js --minify"
12
+ "build": "esbuild src/embed.js --bundle --format=iife --global-name=superbackendErrorTrackingEmbed --outfile=dist/embed.iife.js --minify"
12
13
  },
13
14
  "devDependencies": {
14
15
  "esbuild": "^0.27.2"
@@ -1,11 +1,39 @@
1
1
  import { createErrorTrackingClient } from './core.js';
2
2
 
3
+ function attachToSuperbackendGlobal() {
4
+ const root = (typeof window !== 'undefined' ? window : undefined);
5
+ if (!root) return;
6
+
7
+ if (root.saasbackendErrorTrackingEmbed && !root.superbackendErrorTrackingEmbed) {
8
+ root.superbackendErrorTrackingEmbed = root.saasbackendErrorTrackingEmbed;
9
+ }
10
+
11
+ root.superbackend = root.superbackend || {};
12
+
13
+ if (!root.superbackend.errorTracking) {
14
+ root.superbackend.errorTracking = createErrorTrackingClient();
15
+ }
16
+
17
+ if (root.superbackend.errorTracking && typeof root.superbackend.errorTracking.init === 'function') {
18
+ root.superbackend.errorTracking.init();
19
+ }
20
+ }
21
+
3
22
  function attachToSaasbackendGlobal() {
4
23
  const root = (typeof window !== 'undefined' ? window : undefined);
5
24
  if (!root) return;
6
25
 
26
+ // Show deprecation warning in console
27
+ if (console.warn) {
28
+ console.warn('DEPRECATION: Global "window.saasbackend" is deprecated. Use "window.superbackend" instead.');
29
+ }
30
+
7
31
  root.saasbackend = root.saasbackend || {};
8
32
 
33
+ if (root.superbackendErrorTrackingEmbed && !root.saasbackendErrorTrackingEmbed) {
34
+ root.saasbackendErrorTrackingEmbed = root.superbackendErrorTrackingEmbed;
35
+ }
36
+
9
37
  if (!root.saasbackend.errorTracking) {
10
38
  root.saasbackend.errorTracking = createErrorTrackingClient();
11
39
  }
@@ -15,4 +43,5 @@ function attachToSaasbackendGlobal() {
15
43
  }
16
44
  }
17
45
 
46
+ attachToSuperbackendGlobal();
18
47
  attachToSaasbackendGlobal();
@@ -6,6 +6,7 @@ const path = require('path');
6
6
  const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
7
7
  const { generateAccessToken, generateRefreshToken } = require('../utils/jwt');
8
8
  const { retryFailedWebhooks, processWebhookEvent } = require('../utils/webhookRetry');
9
+ const { auditMiddleware } = require('../services/auditLogger');
9
10
 
10
11
  // Get all users
11
12
  const getUsers = asyncHandler(async (req, res) => {
@@ -17,6 +18,53 @@ const getUsers = asyncHandler(async (req, res) => {
17
18
  res.json(users.map(u => u.toJSON()));
18
19
  });
19
20
 
21
+ // Register new user (admin only)
22
+ const registerUser = asyncHandler(async (req, res) => {
23
+ const { email, password, name, role = 'user' } = req.body;
24
+
25
+ // Validation
26
+ if (!email || !password) {
27
+ return res.status(400).json({ error: 'Email and password are required' });
28
+ }
29
+
30
+ if (password.length < 6) {
31
+ return res.status(400).json({ error: 'Password must be at least 6 characters' });
32
+ }
33
+
34
+ if (!['user', 'admin'].includes(role)) {
35
+ return res.status(400).json({ error: 'Role must be either "user" or "admin"' });
36
+ }
37
+
38
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
39
+ if (!emailRegex.test(email)) {
40
+ return res.status(400).json({ error: 'Invalid email format' });
41
+ }
42
+
43
+ // Check if user already exists
44
+ const existingUser = await User.findOne({ email: email.toLowerCase() });
45
+ if (existingUser) {
46
+ return res.status(409).json({ error: 'Email already registered' });
47
+ }
48
+
49
+ // Create new user
50
+ const user = new User({
51
+ email: email.toLowerCase(),
52
+ passwordHash: password, // Will be hashed by pre-save hook
53
+ name: name || '',
54
+ role: role
55
+ });
56
+
57
+ await user.save();
58
+
59
+ // Log the admin action
60
+ console.log(`Admin registered new user: ${user.email} with role: ${user.role}`);
61
+
62
+ res.status(201).json({
63
+ success: true,
64
+ user: user.toJSON()
65
+ });
66
+ });
67
+
20
68
  // Get single user
21
69
  const getUser = asyncHandler(async (req, res) => {
22
70
  const user = await User.findById(req.params.id);
@@ -290,7 +338,7 @@ const provisionCoolifyDeploy = asyncHandler(async (req, res) => {
290
338
  });
291
339
  }
292
340
 
293
- // In ref-saasbackend, manage.sh already exists in the root of the repository
341
+ // In ref-superbackend, manage.sh already exists in the root of the repository
294
342
  // If it didn't, we would write it here. For this case, we'll just success.
295
343
  res.json({
296
344
  success: true,
@@ -307,6 +355,7 @@ const provisionCoolifyDeploy = asyncHandler(async (req, res) => {
307
355
 
308
356
  module.exports = {
309
357
  getUsers,
358
+ registerUser,
310
359
  getUser,
311
360
  updateUserSubscription,
312
361
  updateUserPassword,
@@ -1,7 +1,11 @@
1
1
  const migrationService = require('../services/migration.service');
2
2
 
3
3
  function getModelRegistry() {
4
- return globalThis?.saasbackend?.models || null;
4
+ // Try new registry first, then fallback to old registry for backward compatibility
5
+ if (globalThis?.saasbackend?.models && !globalThis?.superbackend?.models) {
6
+ console.warn('Deprecation: globalThis.saasbackend is deprecated. Use globalThis.superbackend instead.');
7
+ }
8
+ return globalThis?.superbackend?.models || globalThis?.saasbackend?.models || null;
5
9
  }
6
10
 
7
11
  function getModelByName(modelName) {
package/src/middleware.js CHANGED
@@ -7,6 +7,7 @@ const ejs = require("ejs");
7
7
  const { basicAuth } = require("./middleware/auth");
8
8
  const endpointRegistry = require("./admin/endpointRegistry");
9
9
  const { createFeatureFlagsEjsMiddleware } = require("./services/featureFlags.service");
10
+ const consoleOverride = require("./services/consoleOverride.service");
10
11
  const {
11
12
  hookConsoleError,
12
13
  setupProcessHandlers,
@@ -30,6 +31,9 @@ function createMiddleware(options = {}) {
30
31
  const router = express.Router();
31
32
  const adminPath = options.adminPath || "/admin";
32
33
 
34
+ // Initialize console override service early to capture all logs
35
+ consoleOverride.init();
36
+
33
37
  if (!errorCaptureInitialized) {
34
38
  errorCaptureInitialized = true;
35
39
  hookConsoleError();
@@ -7,6 +7,7 @@ const { basicAuth } = require('../middleware/auth');
7
7
  router.use(basicAuth);
8
8
 
9
9
  router.get('/users', adminController.getUsers);
10
+ router.post('/users/register', adminController.registerUser);
10
11
  router.get('/users/:id', adminController.getUser);
11
12
  router.put('/users/:id/subscription', adminController.updateUserSubscription);
12
13
  router.patch('/users/:id', adminController.updateUserPassword);
@@ -0,0 +1,291 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ let originalConsole = null;
5
+ let logFileStream = null;
6
+ let isActive = false;
7
+ let isWriting = false;
8
+ let memoryInterval = null;
9
+ let logLines = [];
10
+ const MAX_LOG_LINES = 2000;
11
+
12
+ // Store the truly original console methods at module load time
13
+ const TRULY_ORIGINAL_CONSOLE = {
14
+ log: console.log,
15
+ error: console.error,
16
+ warn: console.warn,
17
+ info: console.info,
18
+ debug: console.debug
19
+ };
20
+
21
+ /**
22
+ * Console Override Service
23
+ * Provides dual logging to stdout and file in non-production environments
24
+ */
25
+ const consoleOverride = {
26
+ /**
27
+ * Initialize console override
28
+ * @param {Object} options - Configuration options
29
+ * @param {string} options.logFile - Log file path (default: 'stdout.log')
30
+ * @param {boolean} options.forceEnable - Force enable regardless of NODE_ENV
31
+ */
32
+ init(options = {}) {
33
+ if (isActive) {
34
+ return; // Already initialized
35
+ }
36
+
37
+ const nodeEnv = process.env.NODE_ENV || 'development';
38
+ const forceEnabled = options.forceEnable || process.env.CONSOLE_OVERRIDE_ENABLED === 'true';
39
+ const forceDisabled = process.env.CONSOLE_OVERRIDE_ENABLED === 'false';
40
+
41
+ // Skip if production and not force enabled, or if force disabled
42
+ if ((nodeEnv === 'production' && !forceEnabled) || forceDisabled) {
43
+ return;
44
+ }
45
+
46
+ const logFile = options.logFile || process.env.CONSOLE_LOG_FILE || 'stdout.log';
47
+ const logPath = path.resolve(process.cwd(), logFile);
48
+
49
+ try {
50
+ // Close any existing stream before truncating
51
+ if (logFileStream && !logFileStream.destroyed) {
52
+ logFileStream.end();
53
+ logFileStream = null;
54
+ }
55
+
56
+ // Wait a bit for stream to fully close, then truncate
57
+ setTimeout(() => {
58
+ // Truncate log file on initialization (start with empty file)
59
+ if (fs.existsSync(logPath)) {
60
+ fs.truncateSync(logPath, 0);
61
+ }
62
+
63
+ // Create file stream for appending with error handling
64
+ logFileStream = fs.createWriteStream(logPath, { flags: 'a' });
65
+
66
+ // Handle stream errors
67
+ logFileStream.on('error', (error) => {
68
+ if (originalConsole && originalConsole.error && !isWriting) {
69
+ originalConsole.error('❌ Log stream error:', error.message);
70
+ }
71
+ isActive = false;
72
+ });
73
+
74
+ // Store original console
75
+ originalConsole = { ...console };
76
+
77
+ // Override console methods
78
+ this._overrideConsoleMethods();
79
+
80
+ // Start memory management interval (1 minute)
81
+ this._startMemoryManagement();
82
+
83
+ isActive = true;
84
+
85
+ // Log initialization using original console to avoid recursion
86
+ const initMsg = `📝 Console override initialized - logging to ${logPath}`;
87
+ originalConsole.log(initMsg);
88
+ this._writeToFile(initMsg);
89
+ }, 10);
90
+
91
+ } catch (error) {
92
+ // Fallback to console-only logging
93
+ originalConsole = originalConsole || console;
94
+ originalConsole.error('❌ Console override failed:', error.message);
95
+ isActive = false;
96
+ }
97
+ },
98
+
99
+ /**
100
+ * Override individual console methods
101
+ * @private
102
+ */
103
+ _overrideConsoleMethods() {
104
+ const methods = ['log', 'error', 'warn', 'info', 'debug'];
105
+
106
+ methods.forEach(method => {
107
+ console[method] = (...args) => {
108
+ // Call the truly original console method
109
+ TRULY_ORIGINAL_CONSOLE[method](...args);
110
+
111
+ // Write to file if stream is available and not already writing
112
+ if (logFileStream && !logFileStream.destroyed && !isWriting) {
113
+ this._writeToFile(args);
114
+ }
115
+ };
116
+ });
117
+ },
118
+
119
+ /**
120
+ * Write message to file
121
+ * @param {string|Array} message - Message to write
122
+ * @private
123
+ */
124
+ _writeToFile(message) {
125
+ if (!logFileStream || logFileStream.destroyed) {
126
+ return;
127
+ }
128
+
129
+ isWriting = true;
130
+ try {
131
+ const messageStr = Array.isArray(message)
132
+ ? message.map(arg => typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)).join(' ')
133
+ : String(message);
134
+
135
+ // Add to memory buffer
136
+ logLines.push(messageStr);
137
+
138
+ // Only write if stream is available and not in the middle of a rewrite
139
+ if (logFileStream && !logFileStream.destroyed) {
140
+ logFileStream.write(messageStr + '\n');
141
+ }
142
+ } catch (writeError) {
143
+ // Prevent infinite recursion - use original console for errors
144
+ if (originalConsole && originalConsole.error && !isWriting) {
145
+ originalConsole.error('❌ Log write error:', writeError.message);
146
+ }
147
+ } finally {
148
+ isWriting = false;
149
+ }
150
+ },
151
+
152
+ /**
153
+ * Start memory management interval
154
+ * @private
155
+ */
156
+ _startMemoryManagement() {
157
+ // Clear any existing interval
158
+ if (memoryInterval) {
159
+ clearInterval(memoryInterval);
160
+ }
161
+
162
+ // Set up 1-minute interval to manage log lines
163
+ memoryInterval = setInterval(() => {
164
+ this._manageLogMemory();
165
+ }, 60000); // 1 minute
166
+ },
167
+
168
+ /**
169
+ * Manage log memory by keeping only last MAX_LOG_LINES
170
+ * @private
171
+ */
172
+ _manageLogMemory() {
173
+ if (logLines.length > MAX_LOG_LINES) {
174
+ // Keep only the last MAX_LOG_LINES
175
+ const excessLines = logLines.length - MAX_LOG_LINES;
176
+ logLines = logLines.slice(excessLines);
177
+
178
+ // Rewrite the log file with only the recent lines
179
+ this._rewriteLogFile();
180
+ }
181
+ },
182
+
183
+ /**
184
+ * Rewrite log file with current memory buffer
185
+ * @private
186
+ */
187
+ _rewriteLogFile() {
188
+ if (!logFileStream || !logFileStream.path) {
189
+ return;
190
+ }
191
+
192
+ try {
193
+ const logPath = logFileStream.path;
194
+
195
+ // Write recent lines to file (this truncates the file)
196
+ const fileContent = logLines.join('\n') + '\n';
197
+ fs.writeFileSync(logPath, fileContent, { flag: 'w' });
198
+
199
+ // Reopen stream for appending
200
+ logFileStream = fs.createWriteStream(logPath, { flags: 'a' });
201
+
202
+ // Reattach error handler
203
+ logFileStream.on('error', (error) => {
204
+ if (originalConsole && originalConsole.error && !isWriting) {
205
+ originalConsole.error('❌ Log stream error:', error.message);
206
+ }
207
+ });
208
+
209
+ } catch (error) {
210
+ if (originalConsole && originalConsole.error) {
211
+ originalConsole.error('❌ Log file rewrite error:', error.message);
212
+ }
213
+ }
214
+ },
215
+
216
+ /**
217
+ * Restore original console
218
+ */
219
+ restore() {
220
+ if (!isActive && !originalConsole) {
221
+ return;
222
+ }
223
+
224
+ // Clear memory management interval
225
+ if (memoryInterval) {
226
+ clearInterval(memoryInterval);
227
+ memoryInterval = null;
228
+ }
229
+
230
+ // Restore original console methods using the truly original ones
231
+ ['log', 'error', 'warn', 'info', 'debug'].forEach(method => {
232
+ console[method] = TRULY_ORIGINAL_CONSOLE[method];
233
+ });
234
+
235
+ // Close file stream
236
+ if (logFileStream && !logFileStream.destroyed) {
237
+ logFileStream.end();
238
+ }
239
+
240
+ // Reset state
241
+ isActive = false;
242
+ originalConsole = null;
243
+ logFileStream = null;
244
+ logLines = [];
245
+ isWriting = false;
246
+ },
247
+
248
+ /**
249
+ * Check if override is active
250
+ * @returns {boolean}
251
+ */
252
+ isActive() {
253
+ return isActive;
254
+ },
255
+
256
+ /**
257
+ * Get current memory lines count (for testing/debugging)
258
+ * @returns {number}
259
+ */
260
+ getMemoryLinesCount() {
261
+ return logLines.length;
262
+ },
263
+
264
+ /**
265
+ * Get current log path
266
+ * @returns {string|null}
267
+ */
268
+ getLogPath() {
269
+ if (!logFileStream) {
270
+ return null;
271
+ }
272
+ return logFileStream.path;
273
+ }
274
+ };
275
+
276
+ // Cleanup on process exit
277
+ process.on('exit', () => {
278
+ consoleOverride.restore();
279
+ });
280
+
281
+ process.on('SIGINT', () => {
282
+ consoleOverride.restore();
283
+ process.exit(0);
284
+ });
285
+
286
+ process.on('SIGTERM', () => {
287
+ consoleOverride.restore();
288
+ process.exit(0);
289
+ });
290
+
291
+ module.exports = consoleOverride;
@@ -74,7 +74,7 @@ const sendEmail = async ({
74
74
  from ||
75
75
  (await getSetting(
76
76
  "EMAIL_FROM",
77
- process.env.EMAIL_FROM || "SaaSBackend <no-reply@resend.dev>",
77
+ process.env.EMAIL_FROM || "SuperBackend <no-reply@resend.dev>",
78
78
  ));
79
79
  const toArray = Array.isArray(to) ? to : [to];
80
80
 
@@ -338,6 +338,20 @@ const replaceTemplateVariables = (template, variables) => {
338
338
  return result;
339
339
  };
340
340
 
341
+ // Helper to clear cache (for testing)
342
+ const clearCache = () => {
343
+ settingsCache.clear();
344
+ resendClient = null;
345
+ };
346
+
347
+ // Helper to clear cache and reinitialize (for testing)
348
+ const clearCacheAndReinitialize = async () => {
349
+ settingsCache.clear();
350
+ resendClient = null;
351
+ // Reinitialize with current env vars
352
+ await initResend();
353
+ };
354
+
341
355
  module.exports = {
342
356
  sendEmail,
343
357
  sendPasswordResetEmail,
@@ -348,4 +362,6 @@ module.exports = {
348
362
  sendSubscriptionEmail,
349
363
  sendWaitingListEmail,
350
364
  replaceTemplateVariables,
365
+ clearCache,
366
+ clearCacheAndReinitialize,
351
367
  };
@@ -57,7 +57,7 @@ class WebhookService {
57
57
  headers: {
58
58
  'Content-Type': 'application/json',
59
59
  'X-SaaS-Signature': signature,
60
- 'User-Agent': 'SaaSBackend-Webhook/1.0'
60
+ 'User-Agent': 'SuperBackend-Webhook/1.0'
61
61
  },
62
62
  timeout: timeout
63
63
  }).catch(err => {
@@ -87,7 +87,7 @@ class WebhookService {
87
87
  headers: {
88
88
  'Content-Type': 'application/json',
89
89
  'X-SaaS-Signature': signature,
90
- 'User-Agent': 'SaaSBackend-Webhook/1.0'
90
+ 'User-Agent': 'SuperBackend-Webhook/1.0'
91
91
  },
92
92
  timeout: timeout
93
93
  });
@@ -5,7 +5,7 @@ const { NodeVM } = require('vm2');
5
5
 
6
6
  /**
7
7
  * Workflow Service
8
- * Handles execution of stacked workflow nodes within SaaSBackend.
8
+ * Handles execution of stacked workflow nodes within SuperBackend.
9
9
  */
10
10
  class WorkflowService {
11
11
  constructor(workflowId, initialContext = {}) {
@@ -1,9 +1,11 @@
1
1
  const crypto = require('crypto');
2
2
 
3
3
  function getEncryptionKey() {
4
- const raw = process.env.SAASBACKEND_ENCRYPTION_KEY;
4
+ // Try new name first, then fallback to old name for backward compatibility
5
+ const raw = process.env.SUPERBACKEND_ENCRYPTION_KEY || process.env.SAASBACKEND_ENCRYPTION_KEY;
6
+
5
7
  if (!raw) {
6
- throw new Error('SAASBACKEND_ENCRYPTION_KEY is required for encrypted settings');
8
+ throw new Error('SUPERBACKEND_ENCRYPTION_KEY (or SAASBACKEND_ENCRYPTION_KEY for compatibility) is required for encrypted settings');
7
9
  }
8
10
 
9
11
  let key;
@@ -23,7 +25,7 @@ function getEncryptionKey() {
23
25
 
24
26
  if (key.length !== 32) {
25
27
  throw new Error(
26
- 'SAASBACKEND_ENCRYPTION_KEY must be 32 bytes (base64-encoded 32 bytes, hex 64 chars, or 32-char utf8)',
28
+ 'SUPERBACKEND_ENCRYPTION_KEY (or SAASBACKEND_ENCRYPTION_KEY) must be 32 bytes (base64-encoded 32 bytes, hex 64 chars, or 32-char utf8)',
27
29
  );
28
30
  }
29
31
 
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Coolify Headless Deploy - SaaSBackend</title>
6
+ <title>Coolify Headless Deploy - SuperBackend</title>
7
7
  <script src="https://cdn.tailwindcss.com"></script>
8
8
  <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
9
9
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/dist/tabler-icons.min.css">
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>SaaSBackend Command Center</title>
6
+ <title>SuperBackend Command Center</title>
7
7
  <script src="https://cdn.tailwindcss.com"></script>
8
8
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
9
9
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/dist/tabler-icons.min.css">
@@ -19,7 +19,7 @@
19
19
  <div class="flex items-center gap-4">
20
20
  <i class="ti ti-layout-dashboard text-2xl text-blue-600"></i>
21
21
  <h1 class="text-xl font-bold text-gray-800">
22
- Superbackend <span class="text-xs font-normal text-gray-500 ml-2 align-middle">(saasbackend)</span>
22
+ SuperBackend <span class="text-xs font-normal text-gray-500 ml-2 align-middle">(@intranefr/superbackend)</span>
23
23
  </h1>
24
24
  </div>
25
25
  <div class="flex items-center gap-6">
@@ -50,7 +50,7 @@
50
50
  <div>
51
51
  <div class="text-xs font-semibold text-gray-600 uppercase tracking-wide mb-2">Identify user (JWT header)</div>
52
52
  <div class="relative">
53
- <pre class="p-3 bg-gray-50 rounded text-xs overflow-x-auto"><code id="snippet-jwt">saasbackend.errorTracking.config({
53
+ <pre class="p-3 bg-gray-50 rounded text-xs overflow-x-auto"><code id="snippet-jwt">superbackend.errorTracking.config({
54
54
  headers: { authorization: "Bearer XXX" }
55
55
  })</code></pre>
56
56
  <button type="button" class="absolute top-2 right-2 text-xs px-2 py-1 bg-white border border-gray-200 rounded hover:border-blue-500" onclick="copySnippet('snippet-jwt')">Copy</button>
@@ -60,7 +60,7 @@
60
60
  <div>
61
61
  <div class="text-xs font-semibold text-gray-600 uppercase tracking-wide mb-2">Future npm package (bundlers)</div>
62
62
  <div class="relative">
63
- <pre class="p-3 bg-gray-50 rounded text-xs overflow-x-auto"><code id="snippet-npm">import { createErrorTrackingClient } from '@saasbackend/sdk/error-tracking/browser';
63
+ <pre class="p-3 bg-gray-50 rounded text-xs overflow-x-auto"><code id="snippet-npm">import { createErrorTrackingClient } from '@intranefr/superbackend-error-tracking-browser';
64
64
 
65
65
  const client = createErrorTrackingClient({
66
66
  endpoint: '/api/log/error',
@@ -30,7 +30,7 @@
30
30
  <div class="flex justify-between items-center">
31
31
  <div>
32
32
  <h1 class="text-2xl font-bold text-gray-900">Global Settings Manager</h1>
33
- <p class="text-sm text-gray-600 mt-1">Configure system-wide settings for SaaSBackend</p>
33
+ <p class="text-sm text-gray-600 mt-1">Configure system-wide settings for SuperBackend</p>
34
34
  </div>
35
35
  </div>
36
36
  </div>
@@ -66,8 +66,8 @@
66
66
  <h3 class="font-semibold text-blue-900 mb-2">📋 Available Setting Keys</h3>
67
67
  <div class="text-sm text-blue-800 space-y-2">
68
68
  <p><strong>RESEND_API_KEY</strong> (string) - API key for Resend email service</p>
69
- <p><strong>EMAIL_FROM</strong> (string) - Default "From" address for emails (e.g., "SaaSBackend &lt;no-reply@yourdomain.com&gt;")</p>
70
- <p><strong>FRONTEND_URL</strong> (string) - Frontend application URL (e.g., "https://app.saasbackend.com")</p>
69
+ <p><strong>EMAIL_FROM</strong> (string) - Default "From" address for emails (e.g., "SuperBackend &lt;no-reply@yourdomain.com&gt;")</p>
70
+ <p><strong>FRONTEND_URL</strong> (string) - Frontend application URL (e.g., "https://app.superbackend.com")</p>
71
71
  <p><strong>EMAIL_PASSWORD_RESET_SUBJECT</strong> (string) - Subject line for password reset emails</p>
72
72
  <p><strong>EMAIL_PASSWORD_RESET_HTML</strong> (html) - HTML template for password reset emails</p>
73
73
  <p class="pl-4 text-blue-700">Available variables: <code class="bg-blue-100 px-1 rounded">{{resetUrl}}</code></p>
@@ -229,7 +229,14 @@
229
229
  function updateSnippet() {
230
230
  const slug = $('slug').value || '<slug>';
231
231
  const snippet =
232
- `const saasbackend = require('saasbackend');\n\n// Works when your host app mounts saasbackend.middleware(...) and shares the same process/DB connection\nconst { getJsonConfig } = saasbackend.services.jsonConfigs;\n\nconst cfg = await getJsonConfig('${slug}', {\n bypassCache: false,\n});\n`;
232
+ `const superbackend = require('@intranefr/superbackend');
233
+
234
+ // Works when your host app mounts superbackend.middleware(...) and shares the same process/DB connection
235
+ const { getJsonConfig } = superbackend.services.jsonConfigs;
236
+
237
+ const cfg = await getJsonConfig('${slug}', {
238
+ bypassCache: false,
239
+ });\n`;
233
240
  $('snippet').textContent = snippet;
234
241
  }
235
242
 
@@ -369,9 +369,9 @@
369
369
  <div class="mt-4 bg-blue-50 border-l-4 border-blue-500 p-4 text-sm text-blue-900">
370
370
  <p class="font-semibold mb-1">Storage model</p>
371
371
  <p>Providers and prompts are stored in <code>GlobalSetting</code> rows as JSON under keys <code>llm.providers</code> and <code>llm.prompts</code>. Use the internal service:</p>
372
- <pre class="mt-2 bg-blue-100 rounded p-2 text-xs overflow-auto">const saasbackend = require('saasbackend');
372
+ <pre class="mt-2 bg-blue-100 rounded p-2 text-xs overflow-auto">const superbackend = require('@intranefr/superbackend');
373
373
 
374
- const result = await saasbackend.services.llm.call('tell_joke', {
374
+ const result = await superbackend.services.llm.call('tell_joke', {
375
375
  theme: 'universe',
376
376
  }, {
377
377
  temperature: 0.8,
@@ -248,7 +248,7 @@
248
248
  }
249
249
 
250
250
  function buildSnippet() {
251
- const snippet = `const { getJsonConfigValueBySlug } = require('saasbackend').services.jsonConfigs;\n\nasync function loadSeoConfig() {\n const seo = await getJsonConfigValueBySlug('seo-config');\n return seo;\n}`;
251
+ const snippet = `const { getJsonConfigValueBySlug } = require('@intranefr/superbackend').services.jsonConfigs;\n\nasync function loadSeoConfig() {\n const seo = await getJsonConfigValueBySlug('seo-config');\n return seo;\n}`;
252
252
  document.getElementById('dev-snippet').textContent = snippet;
253
253
  }
254
254
 
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>SaaSBackend API Laboratory</title>
6
+ <title>SuperBackend API Laboratory</title>
7
7
  <script src="https://cdn.tailwindcss.com"></script>
8
8
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css">
9
9
  <style>
@@ -45,7 +45,7 @@
45
45
 
46
46
  <div class="p-4 border-t border-slate-800 bg-slate-900/50">
47
47
  <div class="flex items-center justify-between text-xs text-slate-500 mb-2">
48
- <span>SaaSBackend Framework</span>
48
+ <span>SuperBackend Framework</span>
49
49
  <span class="px-2 py-0.5 bg-slate-800 rounded">v1.2.0</span>
50
50
  </div>
51
51
  </div>
@@ -76,7 +76,7 @@
76
76
  <div class="text-6xl mb-6">🚀</div>
77
77
  <h2 class="text-2xl font-bold">Framework Technical Console</h2>
78
78
  <p class="text-slate-400 max-w-lg mx-auto">
79
- Test and debug SaaSBackend internal APIs. Documented endpoints are automatically grouped by service.
79
+ Test and debug SuperBackend internal APIs. Documented endpoints are automatically grouped by service.
80
80
  </p>
81
81
  <div class="grid grid-cols-3 gap-4 pt-8 max-w-2xl mx-auto">
82
82
  <div class="p-4 bg-slate-800/50 rounded-lg border border-slate-700">
@@ -22,6 +22,7 @@
22
22
  <p class="text-sm text-gray-600 mt-1">Manage system users</p>
23
23
  </div>
24
24
  <div class="flex items-center gap-4">
25
+ <button id="btn-register-user" class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600">Register New User</button>
25
26
  </div>
26
27
  </div>
27
28
  </div>
@@ -199,6 +200,46 @@
199
200
  </div>
200
201
  </div>
201
202
 
203
+ <div id="modal-register" class="fixed inset-0 bg-black bg-opacity-50 hidden z-50 flex items-center justify-center">
204
+ <div class="bg-white rounded-lg shadow-xl p-6 w-full max-w-md mx-4">
205
+ <h3 class="text-lg font-semibold text-gray-900 mb-4">Register New User</h3>
206
+ <form id="register-form" class="space-y-4">
207
+ <div>
208
+ <label class="block text-sm font-medium text-gray-700 mb-1">Email *</label>
209
+ <input id="register-email" type="email" required class="w-full border rounded px-3 py-2" placeholder="user@example.com">
210
+ </div>
211
+ <div>
212
+ <label class="block text-sm font-medium text-gray-700 mb-1">Password *</label>
213
+ <div class="relative">
214
+ <input id="register-password" type="password" required minlength="6" class="w-full border rounded px-3 py-2 pr-10" placeholder="Min 6 characters">
215
+ <button type="button" id="toggle-password" class="absolute right-2 top-2 text-gray-500 hover:text-gray-700">
216
+ <svg id="eye-icon" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
217
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
218
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
219
+ </svg>
220
+ </button>
221
+ </div>
222
+ </div>
223
+ <div>
224
+ <label class="block text-sm font-medium text-gray-700 mb-1">Name</label>
225
+ <input id="register-name" type="text" class="w-full border rounded px-3 py-2" placeholder="Optional">
226
+ </div>
227
+ <div>
228
+ <label class="block text-sm font-medium text-gray-700 mb-1">Role</label>
229
+ <select id="register-role" class="w-full border rounded px-3 py-2">
230
+ <option value="user">User</option>
231
+ <option value="admin">Admin</option>
232
+ </select>
233
+ </div>
234
+ <div id="register-error" class="hidden text-red-600 text-sm"></div>
235
+ </form>
236
+ <div class="flex justify-end gap-2 mt-6">
237
+ <button id="btn-register-cancel" class="bg-gray-100 text-gray-800 px-4 py-2 rounded hover:bg-gray-200">Cancel</button>
238
+ <button id="btn-register-submit" class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600">Register</button>
239
+ </div>
240
+ </div>
241
+ </div>
242
+
202
243
  <div id="toast-container" class="fixed top-4 right-4 space-y-2 z-50"></div>
203
244
 
204
245
  <script>
@@ -417,6 +458,132 @@
417
458
  } catch (e) { showToast(e.message, 'error'); }
418
459
  }
419
460
 
461
+ function openRegisterModal() {
462
+ document.getElementById('register-email').value = '';
463
+ document.getElementById('register-password').value = '';
464
+ document.getElementById('register-name').value = '';
465
+ document.getElementById('register-role').value = 'user';
466
+ document.getElementById('register-error').classList.add('hidden');
467
+ document.getElementById('modal-register').classList.remove('hidden');
468
+ }
469
+
470
+ function closeRegisterModal() {
471
+ document.getElementById('modal-register').classList.add('hidden');
472
+ }
473
+
474
+ function togglePasswordVisibility() {
475
+ const passwordInput = document.getElementById('register-password');
476
+ const eyeIcon = document.getElementById('eye-icon');
477
+
478
+ if (passwordInput.type === 'password') {
479
+ passwordInput.type = 'text';
480
+ eyeIcon.innerHTML = `
481
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"></path>
482
+ `;
483
+ } else {
484
+ passwordInput.type = 'password';
485
+ eyeIcon.innerHTML = `
486
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
487
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
488
+ `;
489
+ }
490
+ }
491
+
492
+ async function registerUser() {
493
+ const email = document.getElementById('register-email').value.trim();
494
+ const password = document.getElementById('register-password').value;
495
+ const name = document.getElementById('register-name').value.trim();
496
+ const role = document.getElementById('register-role').value;
497
+ const errorDiv = document.getElementById('register-error');
498
+
499
+ // Clear previous errors
500
+ errorDiv.classList.add('hidden');
501
+ errorDiv.textContent = '';
502
+
503
+ // Basic validation
504
+ if (!email || !password) {
505
+ errorDiv.textContent = 'Email and password are required';
506
+ errorDiv.classList.remove('hidden');
507
+ return;
508
+ }
509
+
510
+ if (password.length < 6) {
511
+ errorDiv.textContent = 'Password must be at least 6 characters';
512
+ errorDiv.classList.remove('hidden');
513
+ return;
514
+ }
515
+
516
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
517
+ if (!emailRegex.test(email)) {
518
+ errorDiv.textContent = 'Invalid email format';
519
+ errorDiv.classList.remove('hidden');
520
+ return;
521
+ }
522
+
523
+ // Disable submit button
524
+ const submitBtn = document.getElementById('btn-register-submit');
525
+ const originalText = submitBtn.textContent;
526
+ submitBtn.disabled = true;
527
+ submitBtn.textContent = 'Registering...';
528
+
529
+ try {
530
+ // Determine API base URL considering relative path mounting
531
+ const apiBase = getApiUrl('/api/admin/users/register');
532
+
533
+ const response = await fetch(apiBase, {
534
+ method: 'POST',
535
+ headers: {
536
+ 'Content-Type': 'application/json'
537
+ },
538
+ body: JSON.stringify({ email, password, name, role })
539
+ });
540
+
541
+ const data = await response.json();
542
+
543
+ if (!response.ok) {
544
+ if (response.status === 401) {
545
+ throw new Error('Admin authentication required. Please refresh the page and log in again.');
546
+ }
547
+ throw new Error(data.error || 'Registration failed');
548
+ }
549
+
550
+ showToast(`User ${email} registered successfully`, 'success');
551
+ closeRegisterModal();
552
+ await Promise.all([loadUsers(), loadStats()]);
553
+ } catch (error) {
554
+ errorDiv.textContent = error.message || 'Registration failed';
555
+ errorDiv.classList.remove('hidden');
556
+ } finally {
557
+ submitBtn.disabled = false;
558
+ submitBtn.textContent = originalText;
559
+ }
560
+ }
561
+
562
+ function getApiUrl(endpoint) {
563
+ // Detect if we're mounted on a relative path
564
+ const pathname = window.location.pathname;
565
+ const pathSegments = pathname.split('/').filter(Boolean);
566
+
567
+ // Check for common mounting patterns
568
+ if (pathSegments.includes('admin')) {
569
+ // If we're in /admin/ context, try to detect the base path
570
+ // Look for patterns like /super/admin/ or /api/admin/
571
+ const adminIndex = pathSegments.indexOf('admin');
572
+
573
+ if (adminIndex > 0) {
574
+ // There's a prefix before 'admin', use it as base
575
+ const basePath = '/' + pathSegments.slice(0, adminIndex).join('/');
576
+ return basePath + endpoint;
577
+ } else {
578
+ // Admin is at root, just use the endpoint
579
+ return endpoint;
580
+ }
581
+ }
582
+
583
+ // Default case - use current origin with endpoint
584
+ return window.location.origin + endpoint;
585
+ }
586
+
420
587
  function bindEvents() {
421
588
  document.getElementById('btn-refresh').onclick = () => Promise.all([loadUsers(), loadStats()]);
422
589
  document.getElementById('btn-apply').onclick = () => { state.offset = 0; loadUsers(); };
@@ -435,6 +602,18 @@
435
602
  document.getElementById('btn-modal-save').onclick = saveUser;
436
603
  document.getElementById('btn-notify-cancel').onclick = closeNotifyModal;
437
604
  document.getElementById('btn-notify-send').onclick = sendNotification;
605
+
606
+ // Registration modal events
607
+ document.getElementById('btn-register-user').onclick = openRegisterModal;
608
+ document.getElementById('btn-register-cancel').onclick = closeRegisterModal;
609
+ document.getElementById('btn-register-submit').onclick = registerUser;
610
+ document.getElementById('toggle-password').onclick = togglePasswordVisibility;
611
+
612
+ // Form submission
613
+ document.getElementById('register-form').onsubmit = (e) => {
614
+ e.preventDefault();
615
+ registerUser();
616
+ };
438
617
  }
439
618
 
440
619
  bindEvents();
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Webhook Management | SaaSBackend</title>
6
+ <title>Webhook Management | SuperBackend</title>
7
7
  <script src="https://cdn.tailwindcss.com"></script>
8
8
  <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
9
9
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/dist/tabler-icons.min.css">
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Workflow Editor - SaaSBackend</title>
6
+ <title>Workflow Editor - SuperBackend</title>
7
7
  <script src="https://cdn.tailwindcss.com"></script>
8
8
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/dist/tabler-icons.min.css">
9
9
  <style>
@@ -1436,10 +1436,10 @@
1436
1436
  const publicUrl = selected?.publicUrl ? `${API_BASE}${selected.publicUrl}` : null;
1437
1437
 
1438
1438
  const snippet =
1439
- `const saasbackend = require('saasbackend');
1439
+ `const superbackend = require('@intranefr/superbackend');
1440
1440
 
1441
- // Works when your host app mounts saasbackend.middleware(...) and shares the same process/DB connection
1442
- const { assets } = saasbackend.services;
1441
+ // Works when your host app mounts superbackend.middleware(...) and shares the same process/DB connection
1442
+ const { assets } = superbackend.services;
1443
1443
 
1444
1444
  // 1) List assets (metadata)
1445
1445
  const { assets: list } = await assets.listAssets({
@@ -57,7 +57,7 @@
57
57
  <span><kbd class="bg-white border rounded px-1.5 py-0.5">Enter</kbd> Select</span>
58
58
  <span><kbd class="bg-white border rounded px-1.5 py-0.5">Esc</kbd> Close</span>
59
59
  </div>
60
- <div>Superbackend <span class="text-[10px] font-normal opacity-60 ml-1">(saasbackend)</span></div>
60
+ <div>SuperBackend <span class="text-[10px] font-normal opacity-60 ml-1">(@intranefr/superbackend)</span></div>
61
61
  </div>
62
62
  </div>
63
63
  </div>