@intranefr/superbackend 1.5.0 → 1.5.2

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 (198) hide show
  1. package/.env.example +15 -0
  2. package/README.md +11 -0
  3. package/analysis-only.skill +0 -0
  4. package/index.js +23 -0
  5. package/package.json +8 -2
  6. package/src/admin/endpointRegistry.js +120 -0
  7. package/src/controllers/admin.controller.js +90 -6
  8. package/src/controllers/adminBlockDefinitions.controller.js +127 -0
  9. package/src/controllers/adminBlockDefinitionsAi.controller.js +54 -0
  10. package/src/controllers/adminCache.controller.js +342 -0
  11. package/src/controllers/adminContextBlockDefinitions.controller.js +141 -0
  12. package/src/controllers/adminCrons.controller.js +388 -0
  13. package/src/controllers/adminDbBrowser.controller.js +124 -0
  14. package/src/controllers/adminEjsVirtual.controller.js +13 -3
  15. package/src/controllers/adminExperiments.controller.js +200 -0
  16. package/src/controllers/adminHeadless.controller.js +9 -2
  17. package/src/controllers/adminHealthChecks.controller.js +570 -0
  18. package/src/controllers/adminI18n.controller.js +51 -29
  19. package/src/controllers/adminLlm.controller.js +126 -2
  20. package/src/controllers/adminPages.controller.js +720 -0
  21. package/src/controllers/adminPagesContextBlocksAi.controller.js +54 -0
  22. package/src/controllers/adminProxy.controller.js +113 -0
  23. package/src/controllers/adminRateLimits.controller.js +138 -0
  24. package/src/controllers/adminRbac.controller.js +803 -0
  25. package/src/controllers/adminScripts.controller.js +126 -4
  26. package/src/controllers/adminSeoConfig.controller.js +71 -48
  27. package/src/controllers/blogAdmin.controller.js +279 -0
  28. package/src/controllers/blogAiAdmin.controller.js +224 -0
  29. package/src/controllers/blogAutomationAdmin.controller.js +141 -0
  30. package/src/controllers/blogInternal.controller.js +26 -0
  31. package/src/controllers/blogPublic.controller.js +89 -0
  32. package/src/controllers/experiments.controller.js +85 -0
  33. package/src/controllers/fileManager.controller.js +190 -0
  34. package/src/controllers/fileManagerStoragePolicy.controller.js +23 -0
  35. package/src/controllers/healthChecksPublic.controller.js +196 -0
  36. package/src/controllers/internalExperiments.controller.js +17 -0
  37. package/src/controllers/metrics.controller.js +64 -4
  38. package/src/controllers/orgAdmin.controller.js +80 -0
  39. package/src/helpers/mongooseHelper.js +258 -0
  40. package/src/helpers/scriptBase.js +230 -0
  41. package/src/helpers/scriptRunner.js +335 -0
  42. package/src/middleware/rbac.js +62 -0
  43. package/src/middleware.js +810 -48
  44. package/src/models/BlockDefinition.js +27 -0
  45. package/src/models/BlogAutomationLock.js +14 -0
  46. package/src/models/BlogAutomationRun.js +39 -0
  47. package/src/models/BlogPost.js +42 -0
  48. package/src/models/CacheEntry.js +26 -0
  49. package/src/models/ConsoleEntry.js +32 -0
  50. package/src/models/ConsoleLog.js +23 -0
  51. package/src/models/ContextBlockDefinition.js +33 -0
  52. package/src/models/CronExecution.js +47 -0
  53. package/src/models/CronJob.js +70 -0
  54. package/src/models/Experiment.js +75 -0
  55. package/src/models/ExperimentAssignment.js +23 -0
  56. package/src/models/ExperimentEvent.js +26 -0
  57. package/src/models/ExperimentMetricBucket.js +30 -0
  58. package/src/models/ExternalDbConnection.js +49 -0
  59. package/src/models/FileEntry.js +22 -0
  60. package/src/models/GlobalSetting.js +1 -2
  61. package/src/models/HealthAutoHealAttempt.js +57 -0
  62. package/src/models/HealthCheck.js +132 -0
  63. package/src/models/HealthCheckRun.js +51 -0
  64. package/src/models/HealthIncident.js +49 -0
  65. package/src/models/Page.js +95 -0
  66. package/src/models/PageCollection.js +42 -0
  67. package/src/models/ProxyEntry.js +66 -0
  68. package/src/models/RateLimitCounter.js +19 -0
  69. package/src/models/RateLimitMetricBucket.js +20 -0
  70. package/src/models/RbacGrant.js +25 -0
  71. package/src/models/RbacGroup.js +16 -0
  72. package/src/models/RbacGroupMember.js +13 -0
  73. package/src/models/RbacGroupRole.js +13 -0
  74. package/src/models/RbacRole.js +25 -0
  75. package/src/models/RbacUserRole.js +13 -0
  76. package/src/models/ScriptDefinition.js +1 -0
  77. package/src/models/Webhook.js +2 -0
  78. package/src/routes/admin.routes.js +2 -0
  79. package/src/routes/adminBlog.routes.js +21 -0
  80. package/src/routes/adminBlogAi.routes.js +16 -0
  81. package/src/routes/adminBlogAutomation.routes.js +27 -0
  82. package/src/routes/adminCache.routes.js +20 -0
  83. package/src/routes/adminConsoleManager.routes.js +302 -0
  84. package/src/routes/adminCrons.routes.js +25 -0
  85. package/src/routes/adminDbBrowser.routes.js +65 -0
  86. package/src/routes/adminEjsVirtual.routes.js +2 -1
  87. package/src/routes/adminExperiments.routes.js +29 -0
  88. package/src/routes/adminHeadless.routes.js +2 -1
  89. package/src/routes/adminHealthChecks.routes.js +28 -0
  90. package/src/routes/adminI18n.routes.js +4 -3
  91. package/src/routes/adminLlm.routes.js +4 -2
  92. package/src/routes/adminPages.routes.js +55 -0
  93. package/src/routes/adminProxy.routes.js +15 -0
  94. package/src/routes/adminRateLimits.routes.js +17 -0
  95. package/src/routes/adminRbac.routes.js +38 -0
  96. package/src/routes/adminSeoConfig.routes.js +5 -4
  97. package/src/routes/adminUiComponents.routes.js +2 -1
  98. package/src/routes/blogInternal.routes.js +14 -0
  99. package/src/routes/blogPublic.routes.js +9 -0
  100. package/src/routes/experiments.routes.js +30 -0
  101. package/src/routes/fileManager.routes.js +62 -0
  102. package/src/routes/fileManagerStoragePolicy.routes.js +9 -0
  103. package/src/routes/healthChecksPublic.routes.js +9 -0
  104. package/src/routes/internalExperiments.routes.js +15 -0
  105. package/src/routes/log.routes.js +43 -60
  106. package/src/routes/metrics.routes.js +4 -2
  107. package/src/routes/orgAdmin.routes.js +1 -0
  108. package/src/routes/pages.routes.js +123 -0
  109. package/src/routes/proxy.routes.js +46 -0
  110. package/src/routes/rbac.routes.js +47 -0
  111. package/src/routes/webhook.routes.js +2 -1
  112. package/src/routes/workflows.routes.js +4 -0
  113. package/src/services/blockDefinitionsAi.service.js +247 -0
  114. package/src/services/blog.service.js +99 -0
  115. package/src/services/blogAutomation.service.js +978 -0
  116. package/src/services/blogCronsBootstrap.service.js +185 -0
  117. package/src/services/blogPublishing.service.js +58 -0
  118. package/src/services/cacheLayer.service.js +696 -0
  119. package/src/services/consoleManager.service.js +738 -0
  120. package/src/services/consoleOverride.service.js +7 -1
  121. package/src/services/cronScheduler.service.js +350 -0
  122. package/src/services/dbBrowser.service.js +536 -0
  123. package/src/services/ejsVirtual.service.js +102 -32
  124. package/src/services/experiments.service.js +273 -0
  125. package/src/services/experimentsAggregation.service.js +308 -0
  126. package/src/services/experimentsCronsBootstrap.service.js +118 -0
  127. package/src/services/experimentsRetention.service.js +43 -0
  128. package/src/services/experimentsWs.service.js +134 -0
  129. package/src/services/fileManager.service.js +475 -0
  130. package/src/services/fileManagerStoragePolicy.service.js +285 -0
  131. package/src/services/globalSettings.service.js +15 -0
  132. package/src/services/healthChecks.service.js +650 -0
  133. package/src/services/healthChecksBootstrap.service.js +109 -0
  134. package/src/services/healthChecksScheduler.service.js +106 -0
  135. package/src/services/jsonConfigs.service.js +2 -2
  136. package/src/services/llmDefaults.service.js +190 -0
  137. package/src/services/migrationAssets/s3.js +2 -2
  138. package/src/services/pages.service.js +602 -0
  139. package/src/services/pagesContext.service.js +331 -0
  140. package/src/services/pagesContextBlocksAi.service.js +349 -0
  141. package/src/services/proxy.service.js +535 -0
  142. package/src/services/rateLimiter.service.js +623 -0
  143. package/src/services/rbac.service.js +212 -0
  144. package/src/services/scriptsRunner.service.js +215 -15
  145. package/src/services/uiComponentsAi.service.js +6 -19
  146. package/src/services/workflow.service.js +23 -8
  147. package/src/utils/orgRoles.js +14 -0
  148. package/src/utils/rbac/engine.js +60 -0
  149. package/src/utils/rbac/rightsRegistry.js +33 -0
  150. package/views/admin-blog-automation.ejs +877 -0
  151. package/views/admin-blog-edit.ejs +542 -0
  152. package/views/admin-blog.ejs +399 -0
  153. package/views/admin-cache.ejs +681 -0
  154. package/views/admin-console-manager.ejs +680 -0
  155. package/views/admin-crons.ejs +645 -0
  156. package/views/admin-dashboard.ejs +28 -8
  157. package/views/admin-db-browser.ejs +445 -0
  158. package/views/admin-ejs-virtual.ejs +16 -10
  159. package/views/admin-experiments.ejs +91 -0
  160. package/views/admin-file-manager.ejs +942 -0
  161. package/views/admin-health-checks.ejs +725 -0
  162. package/views/admin-i18n.ejs +59 -5
  163. package/views/admin-llm.ejs +99 -1
  164. package/views/admin-organizations.ejs +163 -1
  165. package/views/admin-pages.ejs +2424 -0
  166. package/views/admin-proxy.ejs +491 -0
  167. package/views/admin-rate-limiter.ejs +625 -0
  168. package/views/admin-rbac.ejs +1331 -0
  169. package/views/admin-scripts.ejs +597 -3
  170. package/views/admin-seo-config.ejs +61 -7
  171. package/views/admin-ui-components.ejs +57 -25
  172. package/views/admin-workflows.ejs +7 -7
  173. package/views/file-manager.ejs +866 -0
  174. package/views/pages/blocks/contact.ejs +27 -0
  175. package/views/pages/blocks/cta.ejs +18 -0
  176. package/views/pages/blocks/faq.ejs +20 -0
  177. package/views/pages/blocks/features.ejs +19 -0
  178. package/views/pages/blocks/hero.ejs +13 -0
  179. package/views/pages/blocks/html.ejs +5 -0
  180. package/views/pages/blocks/image.ejs +14 -0
  181. package/views/pages/blocks/testimonials.ejs +26 -0
  182. package/views/pages/blocks/text.ejs +10 -0
  183. package/views/pages/layouts/default.ejs +51 -0
  184. package/views/pages/layouts/minimal.ejs +42 -0
  185. package/views/pages/layouts/sidebar.ejs +54 -0
  186. package/views/pages/partials/footer.ejs +13 -0
  187. package/views/pages/partials/header.ejs +12 -0
  188. package/views/pages/partials/sidebar.ejs +8 -0
  189. package/views/pages/runtime/page.ejs +10 -0
  190. package/views/pages/templates/article.ejs +20 -0
  191. package/views/pages/templates/default.ejs +12 -0
  192. package/views/pages/templates/landing.ejs +14 -0
  193. package/views/pages/templates/listing.ejs +15 -0
  194. package/views/partials/admin-image-upload-modal.ejs +221 -0
  195. package/views/partials/dashboard/nav-items.ejs +12 -0
  196. package/views/partials/dashboard/palette.ejs +5 -3
  197. package/views/partials/llm-provider-model-picker.ejs +183 -0
  198. package/src/routes/llmUi.routes.js +0 -26
@@ -0,0 +1,212 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ const OrganizationMember = require('../models/OrganizationMember');
4
+ const RbacUserRole = require('../models/RbacUserRole');
5
+ const RbacGroup = require('../models/RbacGroup');
6
+ const RbacGroupMember = require('../models/RbacGroupMember');
7
+ const RbacGroupRole = require('../models/RbacGroupRole');
8
+ const RbacGrant = require('../models/RbacGrant');
9
+ const { matches } = require('../utils/rbac/engine');
10
+
11
+ function normalizeId(input) {
12
+ if (!input) return null;
13
+ const str = String(input);
14
+ if (!mongoose.Types.ObjectId.isValid(str)) return null;
15
+ return new mongoose.Types.ObjectId(str);
16
+ }
17
+
18
+ function normalizeRight(input) {
19
+ return String(input || '').trim();
20
+ }
21
+
22
+ function buildExplainItem(grant, source) {
23
+ if (!grant) return null;
24
+ return {
25
+ source,
26
+ effect: grant.effect,
27
+ right: grant.right,
28
+ subjectType: grant.subjectType,
29
+ subjectId: String(grant.subjectId),
30
+ scopeType: grant.scopeType,
31
+ scopeId: grant.scopeId ? String(grant.scopeId) : null,
32
+ id: String(grant._id),
33
+ };
34
+ }
35
+
36
+ function extractMatches(grants, requiredRight) {
37
+ const denies = [];
38
+ const allows = [];
39
+
40
+ for (const g of grants || []) {
41
+ if (!g) continue;
42
+ if (!matches(requiredRight, g.right)) continue;
43
+
44
+ if (g.effect === 'deny') {
45
+ denies.push(g);
46
+ } else {
47
+ allows.push(g);
48
+ }
49
+ }
50
+
51
+ return { denies, allows };
52
+ }
53
+
54
+ async function getUserOrgIds(userId) {
55
+ const uid = normalizeId(userId);
56
+ if (!uid) return [];
57
+ const rows = await OrganizationMember.find({ userId: uid, status: 'active' }).select('orgId').lean();
58
+ return rows.map((r) => String(r.orgId));
59
+ }
60
+
61
+ async function getEffectiveGrants({ userId, orgId }) {
62
+ const uid = normalizeId(userId);
63
+ const oid = normalizeId(orgId);
64
+ if (!uid || !oid) {
65
+ return {
66
+ grants: [],
67
+ layers: { org: [], group: [], role: [], user: [] },
68
+ explain: [],
69
+ context: { roles: [], groups: [] },
70
+ orgMember: null,
71
+ };
72
+ }
73
+
74
+ const orgMember = await OrganizationMember.findOne({ userId: uid, orgId: oid, status: 'active' }).lean();
75
+ if (!orgMember) {
76
+ return {
77
+ grants: [],
78
+ layers: { org: [], group: [], role: [], user: [] },
79
+ explain: [],
80
+ context: { roles: [], groups: [] },
81
+ orgMember: null,
82
+ };
83
+ }
84
+
85
+ const [userRoleLinks, groupLinks, orgGrantsGlobal, orgGrantsOrg] = await Promise.all([
86
+ RbacUserRole.find({ userId: uid }).select('roleId').lean(),
87
+ RbacGroupMember.find({ userId: uid }).select('groupId').lean(),
88
+ RbacGrant.find({ subjectType: 'org', subjectId: oid, scopeType: 'global' }).lean(),
89
+ RbacGrant.find({ subjectType: 'org', subjectId: oid, scopeType: 'org', scopeId: oid }).lean(),
90
+ ]);
91
+
92
+ const directRoleIds = userRoleLinks.map((r) => r.roleId).filter(Boolean);
93
+ const groupIds = groupLinks.map((g) => g.groupId).filter(Boolean);
94
+
95
+ const groups = groupIds.length
96
+ ? await RbacGroup.find({ _id: { $in: groupIds }, status: 'active' }).select('_id isGlobal orgId').lean()
97
+ : [];
98
+
99
+ const allowedGroupIds = [];
100
+ for (const g of groups) {
101
+ if (g.isGlobal) {
102
+ allowedGroupIds.push(g._id);
103
+ continue;
104
+ }
105
+ if (g.orgId && String(g.orgId) === String(oid)) {
106
+ allowedGroupIds.push(g._id);
107
+ }
108
+ }
109
+
110
+ const groupRoleLinks = allowedGroupIds.length
111
+ ? await RbacGroupRole.find({ groupId: { $in: allowedGroupIds } }).select('groupId roleId').lean()
112
+ : [];
113
+
114
+ const groupRoleIds = groupRoleLinks.map((l) => l.roleId).filter(Boolean);
115
+ const effectiveRoleIds = Array.from(new Set([...directRoleIds, ...groupRoleIds]));
116
+
117
+ const [userGrantsGlobal, userGrantsOrg, roleGrantsGlobal, roleGrantsOrg, groupGrantsGlobal, groupGrantsOrg] = await Promise.all([
118
+ RbacGrant.find({ subjectType: 'user', subjectId: uid, scopeType: 'global' }).lean(),
119
+ RbacGrant.find({ subjectType: 'user', subjectId: uid, scopeType: 'org', scopeId: oid }).lean(),
120
+ effectiveRoleIds.length ? RbacGrant.find({ subjectType: 'role', subjectId: { $in: effectiveRoleIds }, scopeType: 'global' }).lean() : [],
121
+ effectiveRoleIds.length ? RbacGrant.find({ subjectType: 'role', subjectId: { $in: effectiveRoleIds }, scopeType: 'org', scopeId: oid }).lean() : [],
122
+ allowedGroupIds.length ? RbacGrant.find({ subjectType: 'group', subjectId: { $in: allowedGroupIds }, scopeType: 'global' }).lean() : [],
123
+ allowedGroupIds.length ? RbacGrant.find({ subjectType: 'group', subjectId: { $in: allowedGroupIds }, scopeType: 'org', scopeId: oid }).lean() : [],
124
+ ]);
125
+
126
+ const layers = {
127
+ org: [...orgGrantsGlobal, ...orgGrantsOrg],
128
+ group: [...groupGrantsGlobal, ...groupGrantsOrg],
129
+ role: [...roleGrantsGlobal, ...roleGrantsOrg],
130
+ user: [...userGrantsGlobal, ...userGrantsOrg],
131
+ };
132
+
133
+ const all = [...layers.org, ...layers.group, ...layers.role, ...layers.user];
134
+
135
+ const explain = [];
136
+ for (const g of userGrantsGlobal) explain.push(buildExplainItem(g, 'user:global'));
137
+ for (const g of userGrantsOrg) explain.push(buildExplainItem(g, 'user:org'));
138
+ for (const g of roleGrantsGlobal) explain.push(buildExplainItem(g, 'role:global'));
139
+ for (const g of roleGrantsOrg) explain.push(buildExplainItem(g, 'role:org'));
140
+ for (const g of groupGrantsGlobal) explain.push(buildExplainItem(g, 'group:global'));
141
+ for (const g of groupGrantsOrg) explain.push(buildExplainItem(g, 'group:org'));
142
+ for (const g of orgGrantsGlobal) explain.push(buildExplainItem(g, 'org:global'));
143
+ for (const g of orgGrantsOrg) explain.push(buildExplainItem(g, 'org:org'));
144
+
145
+ const context = {
146
+ groups: groups.map((g) => ({
147
+ id: String(g._id),
148
+ isGlobal: !!g.isGlobal,
149
+ orgId: g.orgId ? String(g.orgId) : null,
150
+ })),
151
+ roles: [
152
+ ...directRoleIds.map((rid) => ({ roleId: String(rid), source: 'user' })),
153
+ ...groupRoleLinks.map((l) => ({ roleId: String(l.roleId), source: 'group', groupId: String(l.groupId) })),
154
+ ],
155
+ };
156
+
157
+ return { grants: all, layers, explain: explain.filter(Boolean), context, orgMember };
158
+ }
159
+
160
+ async function checkRight({ userId, orgId, right }) {
161
+ const r = normalizeRight(right);
162
+ if (!r) {
163
+ return { allowed: false, reason: 'invalid_right', explain: [], context: null, decisionLayer: null };
164
+ }
165
+
166
+ const { layers, explain, context, orgMember } = await getEffectiveGrants({ userId, orgId });
167
+ if (!orgMember) {
168
+ return { allowed: false, reason: 'not_org_member', explain: [], context: null, decisionLayer: null };
169
+ }
170
+
171
+ const denyMatches = [];
172
+ for (const [layerName, grants] of Object.entries(layers || {})) {
173
+ const { denies } = extractMatches(grants, r);
174
+ for (const d of denies) denyMatches.push({ layerName, grant: d });
175
+ }
176
+
177
+ if (denyMatches.length) {
178
+ const matchedIds = new Set(denyMatches.map((m) => String(m.grant._id)));
179
+ const filteredExplain = explain.filter((e) => matchedIds.has(String(e.id)));
180
+ return {
181
+ allowed: false,
182
+ reason: 'denied',
183
+ decisionLayer: 'deny',
184
+ explain: filteredExplain,
185
+ context,
186
+ };
187
+ }
188
+
189
+ const allowPriority = ['org', 'group', 'role', 'user'];
190
+ for (const layer of allowPriority) {
191
+ const { allows } = extractMatches(layers?.[layer] || [], r);
192
+ if (!allows.length) continue;
193
+
194
+ const matchedIds = new Set(allows.map((a) => String(a._id)));
195
+ const filteredExplain = explain.filter((e) => matchedIds.has(String(e.id)));
196
+ return {
197
+ allowed: true,
198
+ reason: 'allowed',
199
+ decisionLayer: layer,
200
+ explain: filteredExplain,
201
+ context,
202
+ };
203
+ }
204
+
205
+ return { allowed: false, reason: 'no_match', explain: [], context, decisionLayer: null };
206
+ }
207
+
208
+ module.exports = {
209
+ getUserOrgIds,
210
+ getEffectiveGrants,
211
+ checkRight,
212
+ };
@@ -1,11 +1,76 @@
1
1
  const { EventEmitter } = require('events');
2
2
  const { spawn } = require('child_process');
3
3
  const { NodeVM } = require('vm2');
4
+ const mongoose = require('mongoose');
4
5
 
5
6
  const ScriptRun = require('../models/ScriptRun');
7
+ const { mongooseHelper } = require('../helpers/mongooseHelper');
6
8
 
7
9
  const MAX_TAIL_BYTES = 64 * 1024;
8
10
 
11
+ function isTruthyEnv(v) {
12
+ const s = String(v || '').trim().toLowerCase();
13
+ return s === '1' || s === 'true' || s === 'yes' || s === 'y' || s === 'on';
14
+ }
15
+
16
+ function shouldAutoWrapAsyncScripts() {
17
+ if (process.env.SCRIPT_AUTO_ASYNC_WRAP === undefined) return true;
18
+ return isTruthyEnv(process.env.SCRIPT_AUTO_ASYNC_WRAP);
19
+ }
20
+
21
+ function detectTopLevelAwait(code) {
22
+ const s = String(code || '');
23
+ if (!/\bawait\b/.test(s)) return false;
24
+ if (/^\s*\(\s*async\s*\(/.test(s)) return false;
25
+ if (/^\s*async\s+function\b/.test(s)) return false;
26
+ if (/\bmodule\.exports\b/.test(s) || /\bexports\./.test(s)) return false;
27
+ return true;
28
+ }
29
+
30
+ function wrapInAsyncIife(code) {
31
+ const body = String(code || '');
32
+ return [
33
+ '(async () => {',
34
+ body,
35
+ '})().catch((err) => {',
36
+ ' try { console.error(err && err.stack ? err.stack : err); } catch {}',
37
+ '});',
38
+ '',
39
+ ].join('\n');
40
+ }
41
+
42
+ function prepareVmCodeForExecution(code) {
43
+ const raw = String(code || '');
44
+ if (!shouldAutoWrapAsyncScripts()) return { code: raw, wrapped: false };
45
+ if (!detectTopLevelAwait(raw)) return { code: raw, wrapped: false };
46
+ return { code: wrapInAsyncIife(raw), wrapped: true };
47
+ }
48
+
49
+ function buildAwaitSyntaxHelpMessage() {
50
+ return [
51
+ 'Your script uses `await` at top-level.',
52
+ 'Wrap it in an async IIFE, or rely on auto-wrapping:',
53
+ '',
54
+ '(async () => {',
55
+ ' const count = await countCollectionDocuments("users");',
56
+ ' console.log("count:", count);',
57
+ '})();',
58
+ '',
59
+ ].join('\n');
60
+ }
61
+
62
+ // Helper function to decode script content
63
+ function decodeScriptContent(script, format) {
64
+ if (format === 'base64') {
65
+ try {
66
+ return Buffer.from(script, 'base64').toString('utf8');
67
+ } catch (err) {
68
+ throw new Error('Failed to decode base64 script content');
69
+ }
70
+ }
71
+ return script;
72
+ }
73
+
9
74
  function nowIso() {
10
75
  return new Date().toISOString();
11
76
  }
@@ -114,24 +179,16 @@ async function startRun(scriptDef, options) {
114
179
  runId: runDoc._id,
115
180
  bus,
116
181
  command: 'bash',
117
- args: ['-lc', scriptDef.script],
182
+ args: ['-lc', decodeScriptContent(scriptDef.script, scriptDef.scriptFormat)],
118
183
  env,
119
184
  cwd,
120
185
  timeoutMs,
121
186
  });
122
187
  } else if (scriptDef.type === 'node') {
123
188
  if (scriptDef.runner === 'vm2') {
124
- exitCode = await runVm2({ runId: runDoc._id, bus, code: scriptDef.script, timeoutMs });
189
+ exitCode = await runVm2({ runId: runDoc._id, bus, code: decodeScriptContent(scriptDef.script, scriptDef.scriptFormat), timeoutMs });
125
190
  } else if (scriptDef.runner === 'host') {
126
- exitCode = await runSpawned({
127
- runId: runDoc._id,
128
- bus,
129
- command: 'node',
130
- args: ['-e', scriptDef.script],
131
- env,
132
- cwd,
133
- timeoutMs,
134
- });
191
+ exitCode = await runHostWithDatabase({ runId: runDoc._id, bus, code: decodeScriptContent(scriptDef.script, scriptDef.scriptFormat), env, cwd, timeoutMs });
135
192
  } else {
136
193
  throw Object.assign(new Error('Invalid runner for node script'), { code: 'VALIDATION' });
137
194
  }
@@ -171,7 +228,7 @@ async function startRun(scriptDef, options) {
171
228
  }
172
229
  });
173
230
 
174
- return { runId: String(runDoc._id) };
231
+ return runDoc;
175
232
  }
176
233
 
177
234
  async function runSpawned({ runId, bus, command, args, env, cwd, timeoutMs }) {
@@ -213,6 +270,144 @@ async function runSpawned({ runId, bus, command, args, env, cwd, timeoutMs }) {
213
270
  });
214
271
  }
215
272
 
273
+ async function runHostWithDatabase({ runId, bus, code, env, cwd, timeoutMs }) {
274
+ let tail = '';
275
+
276
+ function pushLog(stream, line) {
277
+ const s = String(line || '');
278
+ tail = appendTail(tail, s);
279
+ bus.push({ type: 'log', ts: nowIso(), stream, line: s });
280
+ return ScriptRun.updateOne({ _id: runId }, { $set: { outputTail: tail } });
281
+ }
282
+
283
+ try {
284
+ // Use existing app connection if available, otherwise create new one
285
+ if (mongoose.connection.readyState !== 1) {
286
+ await pushLog('stdout', 'No existing connection found, establishing new connection...\n');
287
+ await mongooseHelper.connect();
288
+
289
+ // Wait for connection to be fully ready
290
+ await mongooseHelper.waitForConnection(5000);
291
+ } else {
292
+ await pushLog('stdout', 'Using existing app database connection\n');
293
+ }
294
+
295
+ // Validate connection is ready
296
+ if (mongoose.connection.readyState !== 1) {
297
+ throw new Error('Database connection is not ready');
298
+ }
299
+
300
+ const prepared = prepareVmCodeForExecution(code);
301
+ if (prepared.wrapped) {
302
+ await pushLog('stdout', 'Auto-wrapping script in async function (detected top-level await)\n');
303
+ }
304
+
305
+ // Create a VM with database context
306
+ const vm = new NodeVM({
307
+ console: 'inherit',
308
+ sandbox: {
309
+ // Expose pre-connected mongoose instance
310
+ mongoose: mongoose,
311
+ db: mongoose.connection.db,
312
+
313
+ // Expose helper functions
314
+ countCollectionDocuments: async (collectionName, query = {}) => {
315
+ try {
316
+ // Ensure connection is still valid
317
+ if (mongoose.connection.readyState !== 1) {
318
+ throw new Error('Database connection lost during operation');
319
+ }
320
+
321
+ const db = mongoose.connection.db;
322
+ if (!db) {
323
+ throw new Error('Database instance not available');
324
+ }
325
+
326
+ const collection = db.collection(collectionName);
327
+ const count = await collection.countDocuments(query);
328
+ return count;
329
+ } catch (error) {
330
+ throw new Error(`Failed to count documents in ${collectionName}: ${error.message}`);
331
+ }
332
+ },
333
+
334
+ // Expose connection status helper
335
+ getConnectionStatus: () => {
336
+ const readyStateMap = {
337
+ 0: 'disconnected',
338
+ 1: 'connected',
339
+ 2: 'connecting',
340
+ 3: 'disconnecting'
341
+ };
342
+
343
+ return {
344
+ readyState: mongoose.connection.readyState,
345
+ readyStateText: readyStateMap[mongoose.connection.readyState] || 'unknown',
346
+ host: mongoose.connection.host,
347
+ name: mongoose.connection.name,
348
+ hasActiveConnection: mongoose.connection.readyState === 1
349
+ };
350
+ },
351
+
352
+ // Expose models if available
353
+ models: mongoose.models || {},
354
+
355
+ // Global objects
356
+ JSON,
357
+ Date,
358
+ Math,
359
+ parseInt,
360
+ parseFloat,
361
+ String,
362
+ Number,
363
+ Object,
364
+ Array,
365
+
366
+ // Process environment
367
+ process: {
368
+ env: { ...process.env, ...env }
369
+ }
370
+ },
371
+ require: {
372
+ external: false,
373
+ builtin: ['util', 'path', 'os'], // Allow some basic built-ins
374
+ },
375
+ timeout: timeoutMs,
376
+ eval: false,
377
+ wasm: false,
378
+ });
379
+
380
+ // Set up console redirection
381
+ vm.on('console.log', (...args) => {
382
+ pushLog('stdout', args.map((a) => (typeof a === 'string' ? a : JSON.stringify(a))).join(' ') + '\n');
383
+ });
384
+ vm.on('console.error', (...args) => {
385
+ pushLog('stderr', args.map((a) => (typeof a === 'string' ? a : JSON.stringify(a))).join(' ') + '\n');
386
+ });
387
+
388
+ // Run the script code with better error handling
389
+ try {
390
+ vm.run(prepared.code, 'script.host.js');
391
+ } catch (vmError) {
392
+ const baseMsg = vmError?.message || 'Unknown VM error';
393
+ const help = baseMsg.includes('await is only valid in async functions') ? `\n\n${buildAwaitSyntaxHelpMessage()}` : '';
394
+ const errorMsg = `VM execution error: ${baseMsg}${help}`;
395
+ await pushLog('stderr', errorMsg + '\n');
396
+ return 1;
397
+ }
398
+
399
+ return 0;
400
+
401
+ } catch (err) {
402
+ const msg = err?.message || 'Host script error';
403
+ await pushLog('stderr', msg + '\n');
404
+ return 1;
405
+ } finally {
406
+ // Don't disconnect here - let mongooseHelper manage connection pooling
407
+ // The connection will be cleaned up when the helper decides
408
+ }
409
+ }
410
+
216
411
  async function runVm2({ runId, bus, code, timeoutMs }) {
217
412
  let tail = '';
218
413
 
@@ -243,11 +438,16 @@ async function runVm2({ runId, bus, code, timeoutMs }) {
243
438
  });
244
439
 
245
440
  try {
246
- vm.run(code, 'script.vm2.js');
441
+ const prepared = prepareVmCodeForExecution(code);
442
+ if (prepared.wrapped) {
443
+ await pushLog('stdout', 'Auto-wrapping script in async function (detected top-level await)\n');
444
+ }
445
+ vm.run(prepared.code, 'script.vm2.js');
247
446
  return 0;
248
447
  } catch (err) {
249
- const msg = err?.message || 'vm2 error';
250
- await pushLog('stderr', msg + '\n');
448
+ const baseMsg = err?.message || 'vm2 error';
449
+ const help = baseMsg.includes('await is only valid in async functions') ? `\n\n${buildAwaitSyntaxHelpMessage()}` : '';
450
+ await pushLog('stderr', baseMsg + help + '\n');
251
451
  return 1;
252
452
  }
253
453
  }
@@ -1,6 +1,6 @@
1
1
  const UiComponent = require('../models/UiComponent');
2
- const { getSettingValue } = require('./globalSettings.service');
3
2
  const llmService = require('./llm.service');
3
+ const { resolveLlmProviderModel } = require('./llmDefaults.service');
4
4
  const { createAuditEvent } = require('./audit.service');
5
5
 
6
6
  const ALLOWED_FIELDS = new Set(['html', 'css', 'js', 'usageMarkdown']);
@@ -94,24 +94,11 @@ function computeWarnings(nextFields) {
94
94
  }
95
95
 
96
96
  async function resolveLlmDefaults({ providerKey, model }) {
97
- const uiProvider = String(providerKey || '').trim();
98
- const uiModel = String(model || '').trim();
99
-
100
- const settingProvider = String(await getSettingValue('uiComponents.ai.providerKey', '') || '').trim();
101
- const settingModel = String(await getSettingValue('uiComponents.ai.model', '') || '').trim();
102
-
103
- const envProvider = String(process.env.DEFAULT_LLM_PROVIDER_KEY || '').trim();
104
- const envModel = String(process.env.DEFAULT_LLM_MODEL || '').trim();
105
-
106
- const resolvedProviderKey = uiProvider || settingProvider || envProvider;
107
- if (!resolvedProviderKey) {
108
- const err = new Error('Missing LLM providerKey (configure uiComponents.ai.providerKey or DEFAULT_LLM_PROVIDER_KEY, or send from UI)');
109
- err.code = 'VALIDATION';
110
- throw err;
111
- }
112
-
113
- const resolvedModel = uiModel || settingModel || envModel || 'x-ai/grok-code-fast-1';
114
- return { providerKey: resolvedProviderKey, model: resolvedModel };
97
+ return resolveLlmProviderModel({
98
+ systemKey: 'uiComponents.proposeEdit',
99
+ providerKey,
100
+ model,
101
+ });
115
102
  }
116
103
 
117
104
  function buildSystemPrompt({ targets }) {
@@ -1,7 +1,8 @@
1
1
  const Workflow = require('../models/Workflow');
2
2
  const WorkflowExecution = require('../models/WorkflowExecution');
3
- const llmService = require('./llm.service');
4
3
  const { NodeVM } = require('vm2');
4
+ const llmService = require('./llm.service');
5
+ const { resolveLlmProviderModel } = require('./llmDefaults.service');
5
6
 
6
7
  /**
7
8
  * Workflow Service
@@ -163,13 +164,27 @@ class WorkflowService {
163
164
 
164
165
  async handleLLM(node) {
165
166
  const prompt = this.interpolate(node.prompt);
166
- const response = await llmService.callAdhoc({
167
- providerKey: node.provider || 'openrouter',
168
- messages: [{ role: 'user', content: prompt }]
169
- }, {
170
- model: node.model || 'minimax/minimax-m2.1',
171
- temperature: node.temperature !== undefined ? parseFloat(node.temperature) : 0.7
172
- });
167
+ const providerKeyRaw = node.provider;
168
+ const modelRaw = node.model;
169
+
170
+ const resolved = (!providerKeyRaw || !String(providerKeyRaw).trim() || !modelRaw || !String(modelRaw).trim())
171
+ ? await resolveLlmProviderModel({
172
+ systemKey: 'workflow.node.llm',
173
+ providerKey: providerKeyRaw,
174
+ model: modelRaw,
175
+ })
176
+ : { providerKey: String(providerKeyRaw).trim(), model: String(modelRaw || '').trim() };
177
+
178
+ const response = await llmService.callAdhoc(
179
+ {
180
+ providerKey: resolved.providerKey,
181
+ messages: [{ role: 'user', content: prompt }],
182
+ },
183
+ {
184
+ model: resolved.model || undefined,
185
+ temperature: node.temperature !== undefined ? parseFloat(node.temperature) : 0.7,
186
+ },
187
+ );
173
188
  return response.content;
174
189
  }
175
190
 
@@ -142,6 +142,18 @@ async function getOrgRoleLevel(role) {
142
142
  return hierarchy[r] || 0;
143
143
  }
144
144
 
145
+ async function isRoleAtLeast(role, requiredRole) {
146
+ const level = await getOrgRoleLevel(role);
147
+ const requiredLevel = await getOrgRoleLevel(requiredRole);
148
+ return level >= requiredLevel;
149
+ }
150
+
151
+ async function isRoleHigherThan(role, otherRole) {
152
+ const level = await getOrgRoleLevel(role);
153
+ const otherLevel = await getOrgRoleLevel(otherRole);
154
+ return level > otherLevel;
155
+ }
156
+
145
157
  function clearOrgRolesCache() {
146
158
  cached = null;
147
159
  }
@@ -152,5 +164,7 @@ module.exports = {
152
164
  getDefaultOrgRole,
153
165
  isValidOrgRole,
154
166
  getOrgRoleLevel,
167
+ isRoleAtLeast,
168
+ isRoleHigherThan,
155
169
  clearOrgRolesCache,
156
170
  };
@@ -0,0 +1,60 @@
1
+ function normalizeRight(input) {
2
+ return String(input || '').trim();
3
+ }
4
+
5
+ function escapeRegex(str) {
6
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
7
+ }
8
+
9
+ function patternToRegex(pattern) {
10
+ const parts = normalizeRight(pattern).split('*').map(escapeRegex);
11
+ return new RegExp('^' + parts.join('.*') + '$');
12
+ }
13
+
14
+ function matches(requiredRight, grantedPattern) {
15
+ const required = normalizeRight(requiredRight);
16
+ const pattern = normalizeRight(grantedPattern);
17
+ if (!required || !pattern) return false;
18
+ if (pattern === required) return true;
19
+ if (!pattern.includes('*')) return false;
20
+ return patternToRegex(pattern).test(required);
21
+ }
22
+
23
+ function evaluateEffects(entries, requiredRight) {
24
+ const required = normalizeRight(requiredRight);
25
+ if (!required) {
26
+ return { allowed: false, reason: 'invalid_required_right' };
27
+ }
28
+
29
+ const denies = [];
30
+ const allows = [];
31
+
32
+ for (const e of entries || []) {
33
+ if (!e) continue;
34
+ const right = normalizeRight(e.right);
35
+ const effect = normalizeRight(e.effect || 'allow');
36
+ if (!right) continue;
37
+ if (!matches(required, right)) continue;
38
+
39
+ if (effect === 'deny') {
40
+ denies.push(e);
41
+ } else {
42
+ allows.push(e);
43
+ }
44
+ }
45
+
46
+ if (denies.length) {
47
+ return { allowed: false, reason: 'denied', matched: denies };
48
+ }
49
+
50
+ if (allows.length) {
51
+ return { allowed: true, reason: 'allowed', matched: allows };
52
+ }
53
+
54
+ return { allowed: false, reason: 'no_match' };
55
+ }
56
+
57
+ module.exports = {
58
+ matches,
59
+ evaluateEffects,
60
+ };
@@ -0,0 +1,33 @@
1
+ const DEFAULT_RIGHTS = [
2
+ 'rbac:roles:read',
3
+ 'rbac:roles:write',
4
+ 'rbac:groups:read',
5
+ 'rbac:groups:write',
6
+ 'rbac:grants:read',
7
+ 'rbac:grants:write',
8
+ 'rbac:test',
9
+ 'experiments:*',
10
+ 'experiments:read',
11
+ 'experiments:events:write',
12
+ 'experiments:admin',
13
+ 'file_manager:*',
14
+ 'file_manager:access',
15
+ 'file_manager:drives:read',
16
+ 'file_manager:files:read',
17
+ 'file_manager:files:upload',
18
+ 'file_manager:files:download',
19
+ 'file_manager:files:update',
20
+ 'file_manager:files:delete',
21
+ 'file_manager:files:share',
22
+ 'backoffice:*',
23
+ 'backoffice:dashboard:access',
24
+ '*',
25
+ ];
26
+
27
+ function listRights() {
28
+ return Array.from(new Set(DEFAULT_RIGHTS)).sort();
29
+ }
30
+
31
+ module.exports = {
32
+ listRights,
33
+ };