@intranefr/superbackend 1.4.3

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 (188) hide show
  1. package/.commiat +4 -0
  2. package/.env.example +47 -0
  3. package/README.md +110 -0
  4. package/index.js +94 -0
  5. package/package.json +67 -0
  6. package/public/css/styles.css +139 -0
  7. package/public/js/animations.js +41 -0
  8. package/sdk/error-tracking/browser/package.json +16 -0
  9. package/sdk/error-tracking/browser/src/core.js +270 -0
  10. package/sdk/error-tracking/browser/src/embed.js +18 -0
  11. package/sdk/error-tracking/browser/src/index.js +1 -0
  12. package/server.js +5 -0
  13. package/src/admin/endpointRegistry.js +300 -0
  14. package/src/controllers/admin.controller.js +321 -0
  15. package/src/controllers/adminAssets.controller.js +530 -0
  16. package/src/controllers/adminAssetsStorage.controller.js +260 -0
  17. package/src/controllers/adminEjsVirtual.controller.js +354 -0
  18. package/src/controllers/adminFeatureFlags.controller.js +155 -0
  19. package/src/controllers/adminHeadless.controller.js +1071 -0
  20. package/src/controllers/adminI18n.controller.js +604 -0
  21. package/src/controllers/adminJsonConfigs.controller.js +97 -0
  22. package/src/controllers/adminLlm.controller.js +273 -0
  23. package/src/controllers/adminMigration.controller.js +257 -0
  24. package/src/controllers/adminSeoConfig.controller.js +515 -0
  25. package/src/controllers/adminStats.controller.js +121 -0
  26. package/src/controllers/adminUploadNamespaces.controller.js +208 -0
  27. package/src/controllers/assets.controller.js +248 -0
  28. package/src/controllers/auth.controller.js +93 -0
  29. package/src/controllers/billing.controller.js +223 -0
  30. package/src/controllers/featureFlags.controller.js +35 -0
  31. package/src/controllers/forms.controller.js +217 -0
  32. package/src/controllers/globalSettings.controller.js +252 -0
  33. package/src/controllers/headlessCrud.controller.js +126 -0
  34. package/src/controllers/i18n.controller.js +12 -0
  35. package/src/controllers/invite.controller.js +249 -0
  36. package/src/controllers/jsonConfigs.controller.js +19 -0
  37. package/src/controllers/metrics.controller.js +149 -0
  38. package/src/controllers/notificationAdmin.controller.js +264 -0
  39. package/src/controllers/notifications.controller.js +131 -0
  40. package/src/controllers/org.controller.js +357 -0
  41. package/src/controllers/orgAdmin.controller.js +491 -0
  42. package/src/controllers/stripeAdmin.controller.js +410 -0
  43. package/src/controllers/user.controller.js +361 -0
  44. package/src/controllers/userAdmin.controller.js +277 -0
  45. package/src/controllers/waitingList.controller.js +167 -0
  46. package/src/controllers/webhook.controller.js +200 -0
  47. package/src/middleware/auth.js +66 -0
  48. package/src/middleware/errorCapture.js +170 -0
  49. package/src/middleware/headlessApiTokenAuth.js +57 -0
  50. package/src/middleware/org.js +108 -0
  51. package/src/middleware.js +901 -0
  52. package/src/models/ActionEvent.js +31 -0
  53. package/src/models/ActivityLog.js +41 -0
  54. package/src/models/Asset.js +84 -0
  55. package/src/models/AuditEvent.js +93 -0
  56. package/src/models/EmailLog.js +28 -0
  57. package/src/models/ErrorAggregate.js +72 -0
  58. package/src/models/FormSubmission.js +41 -0
  59. package/src/models/GlobalSetting.js +38 -0
  60. package/src/models/HeadlessApiToken.js +24 -0
  61. package/src/models/HeadlessModelDefinition.js +41 -0
  62. package/src/models/I18nEntry.js +77 -0
  63. package/src/models/I18nLocale.js +33 -0
  64. package/src/models/Invite.js +70 -0
  65. package/src/models/JsonConfig.js +46 -0
  66. package/src/models/Notification.js +60 -0
  67. package/src/models/Organization.js +57 -0
  68. package/src/models/OrganizationMember.js +43 -0
  69. package/src/models/StripeCatalogItem.js +77 -0
  70. package/src/models/StripeWebhookEvent.js +57 -0
  71. package/src/models/User.js +89 -0
  72. package/src/models/VirtualEjsFile.js +60 -0
  73. package/src/models/VirtualEjsFileVersion.js +43 -0
  74. package/src/models/VirtualEjsGroupChange.js +32 -0
  75. package/src/models/WaitingList.js +41 -0
  76. package/src/models/Webhook.js +63 -0
  77. package/src/models/Workflow.js +29 -0
  78. package/src/models/WorkflowExecution.js +12 -0
  79. package/src/routes/admin.routes.js +26 -0
  80. package/src/routes/adminAssets.routes.js +28 -0
  81. package/src/routes/adminAssetsStorage.routes.js +13 -0
  82. package/src/routes/adminAudit.routes.js +196 -0
  83. package/src/routes/adminEjsVirtual.routes.js +17 -0
  84. package/src/routes/adminErrors.routes.js +164 -0
  85. package/src/routes/adminFeatureFlags.routes.js +12 -0
  86. package/src/routes/adminHeadless.routes.js +38 -0
  87. package/src/routes/adminI18n.routes.js +22 -0
  88. package/src/routes/adminJsonConfigs.routes.js +15 -0
  89. package/src/routes/adminLlm.routes.js +12 -0
  90. package/src/routes/adminMigration.routes.js +81 -0
  91. package/src/routes/adminSeoConfig.routes.js +20 -0
  92. package/src/routes/adminUploadNamespaces.routes.js +13 -0
  93. package/src/routes/assets.routes.js +21 -0
  94. package/src/routes/auth.routes.js +12 -0
  95. package/src/routes/billing.routes.js +11 -0
  96. package/src/routes/errorTracking.routes.js +31 -0
  97. package/src/routes/featureFlags.routes.js +9 -0
  98. package/src/routes/forms.routes.js +9 -0
  99. package/src/routes/formsAdmin.routes.js +13 -0
  100. package/src/routes/globalSettings.routes.js +18 -0
  101. package/src/routes/headless.routes.js +15 -0
  102. package/src/routes/i18n.routes.js +8 -0
  103. package/src/routes/invite.routes.js +9 -0
  104. package/src/routes/jsonConfigs.routes.js +8 -0
  105. package/src/routes/log.routes.js +111 -0
  106. package/src/routes/metrics.routes.js +9 -0
  107. package/src/routes/notificationAdmin.routes.js +15 -0
  108. package/src/routes/notifications.routes.js +12 -0
  109. package/src/routes/org.routes.js +31 -0
  110. package/src/routes/orgAdmin.routes.js +20 -0
  111. package/src/routes/publicAssets.routes.js +7 -0
  112. package/src/routes/stripeAdmin.routes.js +20 -0
  113. package/src/routes/user.routes.js +22 -0
  114. package/src/routes/userAdmin.routes.js +15 -0
  115. package/src/routes/waitingList.routes.js +13 -0
  116. package/src/routes/waitingListAdmin.routes.js +9 -0
  117. package/src/routes/webhook.routes.js +32 -0
  118. package/src/routes/workflowWebhook.routes.js +54 -0
  119. package/src/routes/workflows.routes.js +110 -0
  120. package/src/services/assets.service.js +110 -0
  121. package/src/services/audit.service.js +62 -0
  122. package/src/services/auditLogger.js +165 -0
  123. package/src/services/ejsVirtual.service.js +614 -0
  124. package/src/services/email.service.js +351 -0
  125. package/src/services/errorLogger.js +221 -0
  126. package/src/services/featureFlags.service.js +202 -0
  127. package/src/services/forms.service.js +214 -0
  128. package/src/services/globalSettings.service.js +49 -0
  129. package/src/services/headlessApiTokens.service.js +158 -0
  130. package/src/services/headlessCrypto.service.js +31 -0
  131. package/src/services/headlessModels.service.js +356 -0
  132. package/src/services/i18n.service.js +314 -0
  133. package/src/services/i18nInferredKeys.service.js +337 -0
  134. package/src/services/jsonConfigs.service.js +392 -0
  135. package/src/services/llm.service.js +749 -0
  136. package/src/services/migration.service.js +581 -0
  137. package/src/services/migrationAssets/fsLocal.js +58 -0
  138. package/src/services/migrationAssets/index.js +134 -0
  139. package/src/services/migrationAssets/s3.js +75 -0
  140. package/src/services/migrationAssets/sftp.js +92 -0
  141. package/src/services/notification.service.js +212 -0
  142. package/src/services/objectStorage.service.js +514 -0
  143. package/src/services/seoConfig.service.js +402 -0
  144. package/src/services/storage.js +150 -0
  145. package/src/services/stripe.service.js +185 -0
  146. package/src/services/stripeHelper.service.js +264 -0
  147. package/src/services/uploadNamespaces.service.js +326 -0
  148. package/src/services/webhook.service.js +157 -0
  149. package/src/services/workflow.service.js +271 -0
  150. package/src/utils/asyncHandler.js +5 -0
  151. package/src/utils/encryption.js +80 -0
  152. package/src/utils/jwt.js +40 -0
  153. package/src/utils/orgRoles.js +156 -0
  154. package/src/utils/validation.js +26 -0
  155. package/src/utils/webhookRetry.js +93 -0
  156. package/views/admin-assets.ejs +444 -0
  157. package/views/admin-audit.ejs +283 -0
  158. package/views/admin-coolify-deploy.ejs +207 -0
  159. package/views/admin-dashboard-home.ejs +291 -0
  160. package/views/admin-dashboard.ejs +397 -0
  161. package/views/admin-ejs-virtual.ejs +280 -0
  162. package/views/admin-errors.ejs +368 -0
  163. package/views/admin-feature-flags.ejs +390 -0
  164. package/views/admin-forms.ejs +526 -0
  165. package/views/admin-global-settings.ejs +436 -0
  166. package/views/admin-headless.ejs +2020 -0
  167. package/views/admin-i18n-locales.ejs +221 -0
  168. package/views/admin-i18n.ejs +728 -0
  169. package/views/admin-json-configs.ejs +410 -0
  170. package/views/admin-llm.ejs +884 -0
  171. package/views/admin-metrics.ejs +274 -0
  172. package/views/admin-migration.ejs +814 -0
  173. package/views/admin-notifications.ejs +430 -0
  174. package/views/admin-organizations.ejs +984 -0
  175. package/views/admin-seo-config.ejs +673 -0
  176. package/views/admin-stripe-pricing.ejs +558 -0
  177. package/views/admin-test.ejs +342 -0
  178. package/views/admin-users.ejs +452 -0
  179. package/views/admin-waiting-list.ejs +547 -0
  180. package/views/admin-webhooks.ejs +329 -0
  181. package/views/admin-workflows.ejs +310 -0
  182. package/views/partials/admin-assets-script.ejs +2022 -0
  183. package/views/partials/admin-test-sidebar.ejs +14 -0
  184. package/views/partials/dashboard/nav-items.ejs +66 -0
  185. package/views/partials/dashboard/palette.ejs +63 -0
  186. package/views/partials/dashboard/sidebar.ejs +21 -0
  187. package/views/partials/dashboard/tab-bar.ejs +26 -0
  188. package/views/partials/footer.ejs +3 -0
@@ -0,0 +1,351 @@
1
+ // Email service wrapper using Resend
2
+ // Note: Resend package needs to be installed: npm install resend
3
+
4
+ const GlobalSetting = require("../models/GlobalSetting");
5
+ const EmailLog = require("../models/EmailLog");
6
+
7
+ let resendClient = null;
8
+
9
+ // Cache for settings
10
+ const settingsCache = new Map();
11
+ const CACHE_TTL = 60000; // 1 minute
12
+
13
+ // Helper to get setting with cache
14
+ const getSetting = async (key, defaultValue) => {
15
+ const cached = settingsCache.get(key);
16
+ if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
17
+ return cached.value;
18
+ }
19
+
20
+ try {
21
+ const setting = await GlobalSetting.findOne({ key }).lean();
22
+ const value = setting ? setting.value : defaultValue;
23
+ settingsCache.set(key, { value, timestamp: Date.now() });
24
+ return value;
25
+ } catch (error) {
26
+ console.error(`Error fetching setting ${key}:`, error);
27
+ return defaultValue;
28
+ }
29
+ };
30
+
31
+ // Helper to replace template variables
32
+ const replaceTemplateVars = (template, variables) => {
33
+ let result = template;
34
+ for (const [key, value] of Object.entries(variables)) {
35
+ const regex = new RegExp(`{{${key}}}`, "g");
36
+ result = result.replace(regex, value);
37
+ }
38
+ return result;
39
+ };
40
+
41
+ // Initialize Resend client if API key is available
42
+ const initResend = async () => {
43
+ // Try to get API key from settings first, then fall back to env
44
+ const apiKey = await getSetting("RESEND_API_KEY", process.env.RESEND_API_KEY);
45
+
46
+ if (apiKey && !resendClient) {
47
+ try {
48
+ const { Resend } = require("resend");
49
+ resendClient = new Resend(apiKey);
50
+ console.log("✅ Resend email service initialized");
51
+ } catch (error) {
52
+ console.warn(
53
+ "⚠️ Resend package not installed. Email functionality will be simulated.",
54
+ );
55
+ console.warn(" Install with: npm install resend");
56
+ }
57
+ }
58
+ };
59
+
60
+ // Initialize on module load
61
+ initResend().catch((err) => console.error("Error initializing Resend:", err));
62
+
63
+ const sendEmail = async ({
64
+ to,
65
+ subject,
66
+ html,
67
+ text,
68
+ from,
69
+ userId,
70
+ type = "other",
71
+ metadata,
72
+ }) => {
73
+ const defaultFrom =
74
+ from ||
75
+ (await getSetting(
76
+ "EMAIL_FROM",
77
+ process.env.EMAIL_FROM || "SaaSBackend <no-reply@resend.dev>",
78
+ ));
79
+ const toArray = Array.isArray(to) ? to : [to];
80
+
81
+ // If Resend is not configured, simulate email sending (for development)
82
+ if (!resendClient) {
83
+ console.log("📧 [SIMULATED EMAIL]");
84
+ console.log(" To:", toArray.join(", "));
85
+ console.log(" From:", defaultFrom);
86
+ console.log(" Subject:", subject);
87
+ console.log(
88
+ " Body Preview:",
89
+ html ? html.substring(0, 100) + "..." : "No HTML",
90
+ );
91
+ console.log(" [Email would be sent in production with Resend API key]");
92
+
93
+ // Log simulated email
94
+ try {
95
+ await EmailLog.create({
96
+ userId,
97
+ to: toArray,
98
+ subject,
99
+ type,
100
+ status: "sent",
101
+ metadata: { ...metadata, simulated: true },
102
+ });
103
+ } catch (err) {
104
+ console.error("Error logging simulated email:", err.message);
105
+ }
106
+
107
+ return {
108
+ success: true,
109
+ simulated: true,
110
+ message: "Email simulated (Resend not configured)",
111
+ };
112
+ }
113
+
114
+ try {
115
+ const { data, error } = await resendClient.emails.send({
116
+ from: defaultFrom,
117
+ to: toArray,
118
+ subject,
119
+ html,
120
+ text,
121
+ });
122
+
123
+ if (error) {
124
+ console.error("❌ Email send error:", error);
125
+
126
+ // Log failure
127
+ await EmailLog.create({
128
+ userId,
129
+ to: toArray,
130
+ subject,
131
+ type,
132
+ status: "failed",
133
+ error: error.message,
134
+ metadata,
135
+ });
136
+
137
+ throw new Error(error.message || "Failed to send email");
138
+ }
139
+
140
+ console.log("✅ Email sent successfully:", data);
141
+
142
+ // Log success
143
+ await EmailLog.create({
144
+ userId,
145
+ to: toArray,
146
+ subject,
147
+ type,
148
+ providerId: data.id,
149
+ status: "sent",
150
+ metadata,
151
+ });
152
+
153
+ return {
154
+ success: true,
155
+ data,
156
+ };
157
+ } catch (error) {
158
+ console.error("❌ Error sending email:", error);
159
+
160
+ // Log failure (catch-all)
161
+ try {
162
+ await EmailLog.create({
163
+ userId,
164
+ to: toArray,
165
+ subject,
166
+ type,
167
+ status: "failed",
168
+ error: error.message,
169
+ metadata,
170
+ });
171
+ } catch (logErr) {
172
+ console.error("Error logging failed email:", logErr.message);
173
+ }
174
+
175
+ throw error;
176
+ }
177
+ };
178
+
179
+ const sendPasswordResetEmail = async (email, resetToken) => {
180
+ const frontendUrl = await getSetting(
181
+ "FRONTEND_URL",
182
+ process.env.FRONTEND_URL || "http://localhost:3000",
183
+ );
184
+ const resetUrl = `${frontendUrl}/reset-password?token=${resetToken}`;
185
+
186
+ // Try to get custom template from settings
187
+ const customTemplate = await getSetting("EMAIL_PASSWORD_RESET_HTML", null);
188
+
189
+ let html;
190
+ if (customTemplate) {
191
+ // Use custom template with variable replacement
192
+ html = replaceTemplateVars(customTemplate, { resetUrl });
193
+ } else {
194
+ // Use default template
195
+ html = `
196
+ <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
197
+ <h2>Password Reset Request</h2>
198
+ <p>Hello,</p>
199
+ <p>We received a request to reset your password.</p>
200
+ <p>Click the button below to reset your password:</p>
201
+ <div style="margin: 30px 0;">
202
+ <a href="${resetUrl}"
203
+ style="background-color: #4F46E5; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px; display: inline-block;">
204
+ Reset Password
205
+ </a>
206
+ </div>
207
+ <p>Or copy and paste this link into your browser:</p>
208
+ <p style="color: #666; word-break: break-all;">${resetUrl}</p>
209
+ <p><strong>This link will expire in 1 hour.</strong></p>
210
+ <p>If you didn't request a password reset, you can safely ignore this email.</p>
211
+ </div>
212
+ `;
213
+ }
214
+
215
+ const subject = await getSetting(
216
+ "EMAIL_PASSWORD_RESET_SUBJECT",
217
+ "Reset Your Password",
218
+ );
219
+
220
+ return sendEmail({
221
+ to: email,
222
+ subject,
223
+ html,
224
+ type: "password-reset",
225
+ });
226
+ };
227
+
228
+ const sendPasswordChangedEmail = async (email) => {
229
+ const html = `
230
+ <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
231
+ <h2>Password Changed Successfully</h2>
232
+ <p>Hello,</p>
233
+ <p>This is a confirmation that your account password has been changed successfully.</p>
234
+ <p>If you made this change, you can safely ignore this email.</p>
235
+ <p><strong>If you did not make this change, please contact our support team immediately.</strong></p>
236
+ </div>
237
+ `;
238
+
239
+ return sendEmail({
240
+ to: email,
241
+ subject: "Password Changed",
242
+ html,
243
+ type: "password-changed",
244
+ });
245
+ };
246
+
247
+ const sendAccountDeletionEmail = async (email) => {
248
+ const html = `
249
+ <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
250
+ <h2>Account Deleted</h2>
251
+ <p>Hello,</p>
252
+ <p>Your account has been successfully deleted as requested.</p>
253
+ <p>We're sorry to see you go.</p>
254
+ </div>
255
+ `;
256
+
257
+ return sendEmail({
258
+ to: email,
259
+ subject: "Account Deleted",
260
+ html,
261
+ type: "account-deleted",
262
+ });
263
+ };
264
+
265
+ const sendWelcomeEmail = async (email, name) => {
266
+ const html = `
267
+ <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
268
+ <h2>Welcome ${name}!</h2>
269
+ <p>Thank you for joining our service. We're excited to have you on board.</p>
270
+ <p>If you have any questions, feel free to reach out to our support team.</p>
271
+ </div>
272
+ `;
273
+
274
+ return sendEmail({
275
+ to: email,
276
+ subject: `Welcome ${name}!`,
277
+ html,
278
+ type: "welcome",
279
+ });
280
+ };
281
+
282
+ const sendNotificationEmail = async (email, title, message) => {
283
+ const html = `
284
+ <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
285
+ <h2>${title}</h2>
286
+ <p>${message}</p>
287
+ </div>
288
+ `;
289
+
290
+ return sendEmail({
291
+ to: email,
292
+ subject: `Notification: ${title}`,
293
+ html,
294
+ type: "notification",
295
+ });
296
+ };
297
+
298
+ const sendSubscriptionEmail = async (email, planName, status) => {
299
+ const html = `
300
+ <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
301
+ <h2>Subscription ${status}</h2>
302
+ <p>Plan: ${planName}</p>
303
+ <p>Status: ${status}</p>
304
+ </div>
305
+ `;
306
+
307
+ return sendEmail({
308
+ to: email,
309
+ subject: `Subscription ${status}`,
310
+ html,
311
+ type: "subscription",
312
+ });
313
+ };
314
+
315
+ const sendWaitingListEmail = async (email) => {
316
+ const html = `
317
+ <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
318
+ <h2>Welcome to our waiting list!</h2>
319
+ <p>Thanks for joining, ${email}!</p>
320
+ <p>We'll notify you as soon as a spot becomes available.</p>
321
+ </div>
322
+ `;
323
+
324
+ return sendEmail({
325
+ to: email,
326
+ subject: "Welcome to our waiting list!",
327
+ html,
328
+ type: "waiting-list",
329
+ });
330
+ };
331
+
332
+ const replaceTemplateVariables = (template, variables) => {
333
+ let result = template;
334
+ for (const [key, value] of Object.entries(variables)) {
335
+ const regex = new RegExp(`{{${key}}}`, "g");
336
+ result = result.replace(regex, value);
337
+ }
338
+ return result;
339
+ };
340
+
341
+ module.exports = {
342
+ sendEmail,
343
+ sendPasswordResetEmail,
344
+ sendPasswordChangedEmail,
345
+ sendAccountDeletionEmail,
346
+ sendWelcomeEmail,
347
+ sendNotificationEmail,
348
+ sendSubscriptionEmail,
349
+ sendWaitingListEmail,
350
+ replaceTemplateVariables,
351
+ };
@@ -0,0 +1,221 @@
1
+ const crypto = require('crypto');
2
+ const os = require('os');
3
+
4
+ const ErrorAggregate = require('../models/ErrorAggregate');
5
+
6
+ const SENSITIVE_KEYS = [
7
+ 'password',
8
+ 'token',
9
+ 'secret',
10
+ 'authorization',
11
+ 'cookie',
12
+ 'apikey',
13
+ 'api_key',
14
+ 'accesstoken',
15
+ 'refreshtoken',
16
+ ];
17
+
18
+ async function getConfig() {
19
+ return {
20
+ errorTrackingEnabled: process.env.ERROR_TRACKING_ENABLED !== 'false',
21
+ errorMaxSamplesPerAggregate: parseInt(process.env.ERROR_MAX_SAMPLES, 10) || 20,
22
+ errorSampleRatePercent: parseInt(process.env.ERROR_SAMPLE_RATE_PERCENT, 10) || 100,
23
+ errorRateLimitPerMinute: parseInt(process.env.ERROR_RATE_LIMIT_PER_MINUTE, 10) || 30,
24
+ errorRateLimitAnonPerMinute: parseInt(process.env.ERROR_RATE_LIMIT_ANON_PER_MINUTE, 10) || 10,
25
+ };
26
+ }
27
+
28
+ function scrubValue(key, value) {
29
+ const lowerKey = String(key).toLowerCase();
30
+ for (const sensitive of SENSITIVE_KEYS) {
31
+ if (lowerKey.includes(sensitive)) {
32
+ return '[REDACTED]';
33
+ }
34
+ }
35
+ return value;
36
+ }
37
+
38
+ function scrubObject(obj, depth = 0) {
39
+ if (depth > 5 || !obj || typeof obj !== 'object') return obj;
40
+ if (Array.isArray(obj)) {
41
+ return obj.slice(0, 10).map((item) => scrubObject(item, depth + 1));
42
+ }
43
+ const result = {};
44
+ for (const [key, value] of Object.entries(obj)) {
45
+ if (typeof value === 'object' && value !== null) {
46
+ result[key] = scrubObject(value, depth + 1);
47
+ } else {
48
+ result[key] = scrubValue(key, value);
49
+ }
50
+ }
51
+ return result;
52
+ }
53
+
54
+ function normalizeMessage(message) {
55
+ if (!message) return '';
56
+ return String(message)
57
+ .replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, '<UUID>')
58
+ .replace(/[0-9a-f]{24}/gi, '<OBJECTID>')
59
+ .replace(/\b\d{4,}\b/g, '<NUM>')
60
+ .replace(/\s+/g, ' ')
61
+ .trim()
62
+ .slice(0, 500);
63
+ }
64
+
65
+ function extractTopFrame(stack) {
66
+ if (!stack) return null;
67
+ const lines = String(stack).split('\n').slice(1, 4);
68
+ for (const line of lines) {
69
+ const match = line.match(/at\s+(?:(.+?)\s+)?\(?(.+?):(\d+):(\d+)\)?/);
70
+ if (match) {
71
+ const fn = match[1] || '<anonymous>';
72
+ const file = match[2].split('/').pop();
73
+ return `${fn}@${file}:${match[3]}`;
74
+ }
75
+ }
76
+ return null;
77
+ }
78
+
79
+ function computeFingerprint({ source, errorName, messageTemplate, topFrame, path, statusBucket }) {
80
+ const parts = [
81
+ source || 'unknown',
82
+ errorName || 'Error',
83
+ messageTemplate || '',
84
+ topFrame || '',
85
+ path || '',
86
+ statusBucket || '',
87
+ ];
88
+ const hash = crypto.createHash('sha256').update(parts.join('|')).digest('hex');
89
+ return hash.slice(0, 32);
90
+ }
91
+
92
+ function getStatusBucket(statusCode) {
93
+ if (!statusCode) return null;
94
+ if (statusCode >= 500) return '5xx';
95
+ if (statusCode >= 400) return '4xx';
96
+ if (statusCode >= 300) return '3xx';
97
+ return null;
98
+ }
99
+
100
+ function getTodayKey() {
101
+ return new Date().toISOString().slice(0, 10);
102
+ }
103
+
104
+ let isLogging = false;
105
+
106
+ async function logError(event) {
107
+ if (isLogging) return null;
108
+ isLogging = true;
109
+
110
+ try {
111
+ const config = await getConfig();
112
+ if (!config.errorTrackingEnabled) {
113
+ return null;
114
+ }
115
+
116
+ if (config.errorSampleRatePercent < 100) {
117
+ if (Math.random() * 100 > config.errorSampleRatePercent) {
118
+ return null;
119
+ }
120
+ }
121
+
122
+ const source = event.source || 'backend';
123
+ const errorName = event.errorName || event.name || 'Error';
124
+ const rawMessage = String(event.message || '').slice(0, 2000);
125
+ const messageTemplate = normalizeMessage(rawMessage);
126
+ const stack = String(event.stack || '').slice(0, 5000);
127
+ const topFrame = extractTopFrame(stack);
128
+ const path = event.request?.path || event.path || '';
129
+ const statusCode = event.request?.statusCode || event.statusCode;
130
+ const statusBucket = getStatusBucket(statusCode);
131
+
132
+ const fingerprint = computeFingerprint({
133
+ source,
134
+ errorName,
135
+ messageTemplate,
136
+ topFrame,
137
+ path,
138
+ statusBucket,
139
+ });
140
+
141
+ const sample = {
142
+ at: new Date(),
143
+ message: rawMessage,
144
+ stack,
145
+ actor: scrubObject({
146
+ userId: event.actor?.userId || event.userId,
147
+ role: event.actor?.role || event.role,
148
+ ip: event.actor?.ip || event.ip,
149
+ userAgent: event.actor?.userAgent || event.userAgent,
150
+ }),
151
+ request: scrubObject({
152
+ method: event.request?.method || event.method,
153
+ path,
154
+ statusCode,
155
+ requestId: event.request?.requestId || event.requestId,
156
+ }),
157
+ runtime: scrubObject({
158
+ ...(event.runtime || {}),
159
+ nodeVersion: process.version,
160
+ hostname: os.hostname(),
161
+ }),
162
+ extra: scrubObject(event.extra || {}),
163
+ };
164
+
165
+ const todayKey = getTodayKey();
166
+ const maxSamples = config.errorMaxSamplesPerAggregate;
167
+
168
+ const result = await ErrorAggregate.findOneAndUpdate(
169
+ { fingerprint },
170
+ {
171
+ $inc: { countTotal: 1, [`countsByDay.${todayKey}`]: 1 },
172
+ $set: { lastSeenAt: new Date() },
173
+ $setOnInsert: {
174
+ fingerprint,
175
+ source,
176
+ severity: event.severity || 'error',
177
+ errorName,
178
+ errorCode: event.errorCode || event.code,
179
+ messageTemplate,
180
+ topFrame,
181
+ httpStatusBucket: statusBucket,
182
+ firstSeenAt: new Date(),
183
+ status: 'open',
184
+ },
185
+ $push: {
186
+ samples: {
187
+ $each: [sample],
188
+ $slice: -maxSamples,
189
+ },
190
+ },
191
+ },
192
+ { upsert: true, new: true },
193
+ );
194
+
195
+ return result;
196
+ } catch (err) {
197
+ try {
198
+ console.log('[ErrorLogger] Failed to log error:', err.message);
199
+ } catch (e) {
200
+ // ignore
201
+ }
202
+ return null;
203
+ } finally {
204
+ isLogging = false;
205
+ }
206
+ }
207
+
208
+ function logErrorSync(event) {
209
+ setImmediate(() => {
210
+ logError(event).catch(() => {});
211
+ });
212
+ }
213
+
214
+ module.exports = {
215
+ logError,
216
+ logErrorSync,
217
+ getConfig,
218
+ scrubObject,
219
+ normalizeMessage,
220
+ computeFingerprint,
221
+ };