@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,110 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const Workflow = require('../models/Workflow');
4
+ const WorkflowExecution = require('../models/WorkflowExecution');
5
+ const workflowService = require('../services/workflow.service');
6
+
7
+ // List workflows
8
+ router.get('/', async (req, res) => {
9
+ const query = req.currentOrganization ? { organizationId: req.currentOrganization.id } : {};
10
+ const workflows = await Workflow.find(query).sort('-createdAt');
11
+ res.json(workflows);
12
+ });
13
+
14
+ // Get single workflow
15
+ router.get('/:id', async (req, res) => {
16
+ const workflow = await Workflow.findById(req.params.id);
17
+ if (!workflow) return res.status(404).json({ error: 'Workflow not found' });
18
+ res.json(workflow);
19
+ });
20
+
21
+ // Create workflow
22
+ router.post('/', async (req, res) => {
23
+ const workflow = await Workflow.create({
24
+ ...req.body,
25
+ organizationId: req.currentOrganization?.id
26
+ });
27
+ res.status(201).json(workflow);
28
+ });
29
+
30
+ // Update workflow
31
+ router.put('/:id', async (req, res) => {
32
+ const workflow = await Workflow.findByIdAndUpdate(req.params.id, req.body, { new: true });
33
+ if (!workflow) return res.status(404).json({ error: 'Workflow not found' });
34
+ res.json(workflow);
35
+ });
36
+
37
+ // Delete workflow
38
+ router.delete('/:id', async (req, res) => {
39
+ await Workflow.findByIdAndDelete(req.params.id);
40
+ await WorkflowExecution.deleteMany({ workflowId: req.params.id });
41
+ res.json({ success: true });
42
+ });
43
+
44
+ // Get executions
45
+ router.get('/:id/runs', async (req, res) => {
46
+ const executions = await WorkflowExecution.find({ workflowId: req.params.id }).sort('-executedAt').limit(50);
47
+ res.json(executions);
48
+ });
49
+
50
+ // Test execution
51
+ router.post('/:id/test', async (req, res) => {
52
+ try {
53
+ const initialContext = {
54
+ body: req.body.body || {},
55
+ query: req.body.query || {},
56
+ headers: req.body.headers || {},
57
+ method: req.body.method || 'POST'
58
+ };
59
+ const service = await workflowService.execute(req.params.id, initialContext);
60
+ res.json({
61
+ status: service.status,
62
+ log: service.executionLog,
63
+ context: service.context
64
+ });
65
+ } catch (err) {
66
+ res.status(500).json({ error: err.message });
67
+ }
68
+ });
69
+
70
+ // Isolated node test
71
+ router.post('/:id/nodes/:nodeId/test', async (req, res) => {
72
+ try {
73
+ const { WorkflowService } = require('../services/workflow.service');
74
+ const Workflow = require('../models/Workflow');
75
+
76
+ const workflow = await Workflow.findById(req.params.id);
77
+ if (!workflow) return res.status(404).json({ error: 'Workflow not found' });
78
+
79
+ // Use dirty node data from request body if provided, otherwise fallback to DB
80
+ const node = req.body.node || workflow.nodes.find(n => n.id === req.params.nodeId);
81
+ if (!node) return res.status(404).json({ error: 'Node not found' });
82
+
83
+ const service = new WorkflowService(req.params.id);
84
+ // Standard runNode logic but with the provided node object
85
+ service.context = {
86
+ ...req.body.context,
87
+ entrypoint: req.body.context?.entrypoint || workflow.testDataset || {},
88
+ payload: req.body.context?.payload || workflow.testDataset || {},
89
+ nodes: req.body.context?.nodes || {},
90
+ lastNode: req.body.context?.lastNode || workflow.testDataset || {},
91
+ env: process.env
92
+ };
93
+
94
+ const result = await service.executeNode(node);
95
+
96
+ // Persist result in the node's testResult field in DB
97
+ const nodeIndex = workflow.nodes.findIndex(n => n.id === req.params.nodeId);
98
+ if (nodeIndex !== -1) {
99
+ workflow.nodes[nodeIndex].testResult = result;
100
+ workflow.markModified('nodes');
101
+ await workflow.save();
102
+ }
103
+
104
+ res.json({ result, context: service.context });
105
+ } catch (err) {
106
+ res.status(500).json({ error: err.message });
107
+ }
108
+ });
109
+
110
+ module.exports = router;
@@ -0,0 +1,110 @@
1
+ const Asset = require('../models/Asset');
2
+ const objectStorage = require('./objectStorage.service');
3
+
4
+ async function getAssetById(id, { status = 'uploaded' } = {}) {
5
+ const asset = await Asset.findById(id);
6
+ if (!asset) {
7
+ const err = new Error('Asset not found');
8
+ err.code = 'NOT_FOUND';
9
+ throw err;
10
+ }
11
+
12
+ if (status && asset.status !== status) {
13
+ const err = new Error('Asset not found');
14
+ err.code = 'NOT_FOUND';
15
+ throw err;
16
+ }
17
+
18
+ return asset.toObject();
19
+ }
20
+
21
+ async function getAssetByKey(key, { status = 'uploaded' } = {}) {
22
+ const asset = await Asset.findOne({ key: String(key || '').trim() });
23
+ if (!asset) {
24
+ const err = new Error('Asset not found');
25
+ err.code = 'NOT_FOUND';
26
+ throw err;
27
+ }
28
+
29
+ if (status && asset.status !== status) {
30
+ const err = new Error('Asset not found');
31
+ err.code = 'NOT_FOUND';
32
+ throw err;
33
+ }
34
+
35
+ return asset.toObject();
36
+ }
37
+
38
+ async function listAssets({
39
+ namespace,
40
+ tag,
41
+ visibility,
42
+ status = 'uploaded',
43
+ page = 1,
44
+ limit = 50,
45
+ } = {}) {
46
+ const normalizedLimit = Math.min(100, Math.max(1, Number(limit) || 50));
47
+ const normalizedPage = Math.max(1, Number(page) || 1);
48
+ const skip = (normalizedPage - 1) * normalizedLimit;
49
+
50
+ const filter = {};
51
+ if (status) filter.status = status;
52
+ if (namespace) filter.namespace = String(namespace);
53
+ if (visibility) filter.visibility = String(visibility);
54
+ if (tag) filter.tags = String(tag).trim().toLowerCase();
55
+
56
+ const [assets, total] = await Promise.all([
57
+ Asset.find(filter).sort({ createdAt: -1 }).skip(skip).limit(normalizedLimit).lean(),
58
+ Asset.countDocuments(filter),
59
+ ]);
60
+
61
+ return {
62
+ assets,
63
+ pagination: {
64
+ page: normalizedPage,
65
+ limit: normalizedLimit,
66
+ total,
67
+ pages: Math.ceil(total / normalizedLimit),
68
+ },
69
+ };
70
+ }
71
+
72
+ async function getAssetBytesById(id, { status = 'uploaded' } = {}) {
73
+ const asset = await getAssetById(id, { status });
74
+ const result = await objectStorage.getObject({ key: asset.key });
75
+ if (!result || !result.body) {
76
+ const err = new Error('File not found in storage');
77
+ err.code = 'NOT_FOUND';
78
+ throw err;
79
+ }
80
+
81
+ return {
82
+ asset,
83
+ contentType: result.contentType || asset.contentType,
84
+ body: result.body,
85
+ };
86
+ }
87
+
88
+ async function getAssetBytesByKey(key, { status = 'uploaded' } = {}) {
89
+ const asset = await getAssetByKey(key, { status });
90
+ const result = await objectStorage.getObject({ key: asset.key });
91
+ if (!result || !result.body) {
92
+ const err = new Error('File not found in storage');
93
+ err.code = 'NOT_FOUND';
94
+ throw err;
95
+ }
96
+
97
+ return {
98
+ asset,
99
+ contentType: result.contentType || asset.contentType,
100
+ body: result.body,
101
+ };
102
+ }
103
+
104
+ module.exports = {
105
+ getAssetById,
106
+ getAssetByKey,
107
+ listAssets,
108
+ getAssetBytesById,
109
+ getAssetBytesByKey,
110
+ };
@@ -0,0 +1,62 @@
1
+ const AuditEvent = require('../models/AuditEvent');
2
+ const webhookService = require('./webhook.service');
3
+
4
+ async function createAuditEvent({
5
+ actorType,
6
+ actorId,
7
+ action,
8
+ entityType,
9
+ entityId,
10
+ before,
11
+ after,
12
+ meta,
13
+ }) {
14
+ try {
15
+ await AuditEvent.create({
16
+ actorType,
17
+ actorId,
18
+ action,
19
+ entityType,
20
+ entityId,
21
+ before,
22
+ after,
23
+ meta,
24
+ });
25
+
26
+ // Trigger Webhooks for critical events or all audit events
27
+ // Assuming organizationId is available in meta or can be inferred
28
+ const organizationId = meta?.organizationId || meta?.orgId;
29
+ if (organizationId) {
30
+ webhookService.emit('audit.event', {
31
+ action,
32
+ entityType,
33
+ entityId,
34
+ actorType,
35
+ actorId,
36
+ timestamp: new Date().toISOString()
37
+ }, organizationId);
38
+ }
39
+ } catch (error) {
40
+ console.error('Error creating audit event:', error);
41
+ }
42
+ }
43
+
44
+ function getBasicAuthActor(req) {
45
+ const authHeader = req.headers.authorization;
46
+ if (!authHeader || !authHeader.startsWith('Basic ')) {
47
+ return { actorType: 'admin', actorId: null };
48
+ }
49
+
50
+ try {
51
+ const credentials = Buffer.from(authHeader.substring(6), 'base64').toString('utf-8');
52
+ const [username] = credentials.split(':');
53
+ return { actorType: 'admin', actorId: username || null };
54
+ } catch (e) {
55
+ return { actorType: 'admin', actorId: null };
56
+ }
57
+ }
58
+
59
+ module.exports = {
60
+ createAuditEvent,
61
+ getBasicAuthActor,
62
+ };
@@ -0,0 +1,165 @@
1
+ const AuditEvent = require('../models/AuditEvent');
2
+
3
+ const SENSITIVE_KEYS = [
4
+ 'password',
5
+ 'token',
6
+ 'secret',
7
+ 'authorization',
8
+ 'cookie',
9
+ 'apikey',
10
+ 'api_key',
11
+ 'accesstoken',
12
+ 'refreshtoken',
13
+ 'passwordhash',
14
+ ];
15
+
16
+ async function getConfig() {
17
+ return {
18
+ auditTrackingEnabled: process.env.AUDIT_TRACKING_ENABLED !== 'false',
19
+ auditLogFailedAttempts: process.env.AUDIT_LOG_FAILED_ATTEMPTS !== 'false',
20
+ auditRetentionDays: parseInt(process.env.AUDIT_RETENTION_DAYS, 10) || 90,
21
+ };
22
+ }
23
+
24
+ function scrubValue(key, value) {
25
+ const lowerKey = String(key).toLowerCase();
26
+ for (const sensitive of SENSITIVE_KEYS) {
27
+ if (lowerKey.includes(sensitive)) {
28
+ return '[REDACTED]';
29
+ }
30
+ }
31
+ return value;
32
+ }
33
+
34
+ function scrubObject(obj, depth = 0) {
35
+ if (depth > 5 || !obj || typeof obj !== 'object') return obj;
36
+ if (Array.isArray(obj)) {
37
+ return obj.slice(0, 10).map((item) => scrubObject(item, depth + 1));
38
+ }
39
+ const result = {};
40
+ for (const [key, value] of Object.entries(obj)) {
41
+ if (typeof value === 'object' && value !== null) {
42
+ result[key] = scrubObject(value, depth + 1);
43
+ } else {
44
+ result[key] = scrubValue(key, value);
45
+ }
46
+ }
47
+ return result;
48
+ }
49
+
50
+ function extractContext(req) {
51
+ if (!req) return {};
52
+ return {
53
+ ip: req.ip || req.headers?.['x-forwarded-for']?.split(',')[0]?.trim() || req.connection?.remoteAddress,
54
+ userAgent: req.headers?.['user-agent']?.slice(0, 500),
55
+ requestId: req.headers?.['x-request-id'] || req.requestId,
56
+ path: req.path || req.url,
57
+ method: req.method,
58
+ };
59
+ }
60
+
61
+ function extractActor(req) {
62
+ if (!req) {
63
+ return { actorType: 'system', actorId: null };
64
+ }
65
+
66
+ if (req.user) {
67
+ return {
68
+ actorType: 'user',
69
+ actorId: String(req.user._id || req.user.id),
70
+ };
71
+ }
72
+
73
+ const authHeader = req.headers?.authorization || '';
74
+ if (authHeader.startsWith('Basic ')) {
75
+ try {
76
+ const credentials = Buffer.from(authHeader.substring(6), 'base64').toString('utf-8');
77
+ const [username] = credentials.split(':');
78
+ return { actorType: 'admin', actorId: username || null };
79
+ } catch (e) {
80
+ return { actorType: 'admin', actorId: null };
81
+ }
82
+ }
83
+
84
+ return { actorType: 'system', actorId: null };
85
+ }
86
+
87
+ async function logAudit(event) {
88
+ try {
89
+ const config = await getConfig();
90
+ if (!config.auditTrackingEnabled) {
91
+ return null;
92
+ }
93
+
94
+ if (event.outcome === 'failure' && !config.auditLogFailedAttempts) {
95
+ return null;
96
+ }
97
+
98
+ const actor = event.actor || extractActor(event.req);
99
+ const context = event.context || extractContext(event.req);
100
+
101
+ const auditEvent = await AuditEvent.create({
102
+ actorType: actor.actorType || 'system',
103
+ actorId: actor.actorId || null,
104
+ action: event.action,
105
+ entityType: event.entityType || event.targetType || 'unknown',
106
+ entityId: event.entityId || event.targetId || null,
107
+ before: event.before || null,
108
+ after: event.after || null,
109
+ meta: scrubObject({
110
+ ...(event.meta || {}),
111
+ outcome: event.outcome || 'success',
112
+ context,
113
+ details: scrubObject(event.details || {}),
114
+ }),
115
+ outcome: event.outcome || 'success',
116
+ context,
117
+ targetType: event.targetType || event.entityType,
118
+ targetId: event.targetId || event.entityId,
119
+ });
120
+
121
+ return auditEvent;
122
+ } catch (err) {
123
+ try {
124
+ console.log('[AuditLogger] Failed to log audit:', err.message);
125
+ } catch (e) {
126
+ // ignore
127
+ }
128
+ return null;
129
+ }
130
+ }
131
+
132
+ function logAuditSync(event) {
133
+ setImmediate(() => {
134
+ logAudit(event).catch(() => {});
135
+ });
136
+ }
137
+
138
+ function auditMiddleware(action, options = {}) {
139
+ return (req, res, next) => {
140
+ const originalEnd = res.end;
141
+ res.end = function (...args) {
142
+ const outcome = res.statusCode >= 400 ? 'failure' : 'success';
143
+ logAuditSync({
144
+ req,
145
+ action,
146
+ outcome,
147
+ entityType: options.entityType || options.targetType,
148
+ entityId: options.getEntityId ? options.getEntityId(req) : (req.params?.id || req.params?.key),
149
+ details: options.getDetails ? options.getDetails(req, res) : undefined,
150
+ });
151
+ return originalEnd.apply(this, args);
152
+ };
153
+ next();
154
+ };
155
+ }
156
+
157
+ module.exports = {
158
+ logAudit,
159
+ logAuditSync,
160
+ auditMiddleware,
161
+ getConfig,
162
+ extractActor,
163
+ extractContext,
164
+ scrubObject,
165
+ };