@intranefr/superbackend 1.4.4 → 1.5.1
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 +5 -0
- package/README.md +11 -0
- package/index.js +39 -1
- package/package.json +11 -3
- package/public/sdk/ui-components.iife.js +191 -0
- package/sdk/ui-components/browser/src/index.js +228 -0
- package/src/admin/endpointRegistry.js +120 -0
- package/src/controllers/admin.controller.js +111 -5
- package/src/controllers/adminBlockDefinitions.controller.js +127 -0
- package/src/controllers/adminBlockDefinitionsAi.controller.js +54 -0
- package/src/controllers/adminCache.controller.js +342 -0
- package/src/controllers/adminContextBlockDefinitions.controller.js +141 -0
- package/src/controllers/adminCrons.controller.js +388 -0
- package/src/controllers/adminDbBrowser.controller.js +124 -0
- package/src/controllers/adminEjsVirtual.controller.js +13 -3
- package/src/controllers/adminHeadless.controller.js +91 -2
- package/src/controllers/adminHealthChecks.controller.js +570 -0
- package/src/controllers/adminI18n.controller.js +51 -29
- package/src/controllers/adminLlm.controller.js +126 -2
- package/src/controllers/adminPages.controller.js +720 -0
- package/src/controllers/adminPagesContextBlocksAi.controller.js +54 -0
- package/src/controllers/adminProxy.controller.js +113 -0
- package/src/controllers/adminRateLimits.controller.js +138 -0
- package/src/controllers/adminRbac.controller.js +803 -0
- package/src/controllers/adminScripts.controller.js +320 -0
- package/src/controllers/adminSeoConfig.controller.js +71 -48
- package/src/controllers/adminTerminals.controller.js +39 -0
- package/src/controllers/adminUiComponents.controller.js +315 -0
- package/src/controllers/adminUiComponentsAi.controller.js +34 -0
- package/src/controllers/blogAdmin.controller.js +279 -0
- package/src/controllers/blogAiAdmin.controller.js +224 -0
- package/src/controllers/blogAutomationAdmin.controller.js +141 -0
- package/src/controllers/blogInternal.controller.js +26 -0
- package/src/controllers/blogPublic.controller.js +89 -0
- package/src/controllers/fileManager.controller.js +190 -0
- package/src/controllers/fileManagerStoragePolicy.controller.js +23 -0
- package/src/controllers/healthChecksPublic.controller.js +196 -0
- package/src/controllers/metrics.controller.js +64 -4
- package/src/controllers/orgAdmin.controller.js +366 -0
- package/src/controllers/uiComponentsPublic.controller.js +118 -0
- package/src/middleware/auth.js +7 -0
- package/src/middleware/internalCronAuth.js +29 -0
- package/src/middleware/rbac.js +62 -0
- package/src/middleware.js +879 -56
- package/src/models/BlockDefinition.js +27 -0
- package/src/models/BlogAutomationLock.js +14 -0
- package/src/models/BlogAutomationRun.js +39 -0
- package/src/models/BlogPost.js +42 -0
- package/src/models/CacheEntry.js +26 -0
- package/src/models/ConsoleEntry.js +32 -0
- package/src/models/ConsoleLog.js +23 -0
- package/src/models/ContextBlockDefinition.js +33 -0
- package/src/models/CronExecution.js +47 -0
- package/src/models/CronJob.js +70 -0
- package/src/models/ExternalDbConnection.js +49 -0
- package/src/models/FileEntry.js +22 -0
- package/src/models/HeadlessModelDefinition.js +10 -0
- package/src/models/HealthAutoHealAttempt.js +57 -0
- package/src/models/HealthCheck.js +132 -0
- package/src/models/HealthCheckRun.js +51 -0
- package/src/models/HealthIncident.js +49 -0
- package/src/models/Page.js +95 -0
- package/src/models/PageCollection.js +42 -0
- package/src/models/ProxyEntry.js +66 -0
- package/src/models/RateLimitCounter.js +19 -0
- package/src/models/RateLimitMetricBucket.js +20 -0
- package/src/models/RbacGrant.js +25 -0
- package/src/models/RbacGroup.js +16 -0
- package/src/models/RbacGroupMember.js +13 -0
- package/src/models/RbacGroupRole.js +13 -0
- package/src/models/RbacRole.js +25 -0
- package/src/models/RbacUserRole.js +13 -0
- package/src/models/ScriptDefinition.js +42 -0
- package/src/models/ScriptRun.js +22 -0
- package/src/models/UiComponent.js +29 -0
- package/src/models/UiComponentProject.js +26 -0
- package/src/models/UiComponentProjectComponent.js +18 -0
- package/src/routes/admin.routes.js +1 -0
- package/src/routes/adminBlog.routes.js +21 -0
- package/src/routes/adminBlogAi.routes.js +16 -0
- package/src/routes/adminBlogAutomation.routes.js +27 -0
- package/src/routes/adminCache.routes.js +20 -0
- package/src/routes/adminConsoleManager.routes.js +302 -0
- package/src/routes/adminCrons.routes.js +25 -0
- package/src/routes/adminDbBrowser.routes.js +65 -0
- package/src/routes/adminEjsVirtual.routes.js +2 -1
- package/src/routes/adminHeadless.routes.js +8 -1
- package/src/routes/adminHealthChecks.routes.js +28 -0
- package/src/routes/adminI18n.routes.js +4 -3
- package/src/routes/adminLlm.routes.js +4 -2
- package/src/routes/adminPages.routes.js +55 -0
- package/src/routes/adminProxy.routes.js +15 -0
- package/src/routes/adminRateLimits.routes.js +17 -0
- package/src/routes/adminRbac.routes.js +38 -0
- package/src/routes/adminScripts.routes.js +21 -0
- package/src/routes/adminSeoConfig.routes.js +5 -4
- package/src/routes/adminTerminals.routes.js +13 -0
- package/src/routes/adminUiComponents.routes.js +30 -0
- package/src/routes/blogInternal.routes.js +14 -0
- package/src/routes/blogPublic.routes.js +9 -0
- package/src/routes/fileManager.routes.js +62 -0
- package/src/routes/fileManagerStoragePolicy.routes.js +9 -0
- package/src/routes/healthChecksPublic.routes.js +9 -0
- package/src/routes/log.routes.js +43 -60
- package/src/routes/metrics.routes.js +4 -2
- package/src/routes/orgAdmin.routes.js +6 -0
- package/src/routes/pages.routes.js +123 -0
- package/src/routes/proxy.routes.js +46 -0
- package/src/routes/rbac.routes.js +47 -0
- package/src/routes/uiComponentsPublic.routes.js +9 -0
- package/src/routes/webhook.routes.js +2 -1
- package/src/routes/workflows.routes.js +4 -0
- package/src/services/blockDefinitionsAi.service.js +247 -0
- package/src/services/blog.service.js +99 -0
- package/src/services/blogAutomation.service.js +978 -0
- package/src/services/blogCronsBootstrap.service.js +184 -0
- package/src/services/blogPublishing.service.js +58 -0
- package/src/services/cacheLayer.service.js +696 -0
- package/src/services/consoleManager.service.js +700 -0
- package/src/services/consoleOverride.service.js +6 -1
- package/src/services/cronScheduler.service.js +350 -0
- package/src/services/dbBrowser.service.js +536 -0
- package/src/services/ejsVirtual.service.js +102 -32
- package/src/services/fileManager.service.js +475 -0
- package/src/services/fileManagerStoragePolicy.service.js +285 -0
- package/src/services/headlessExternalModels.service.js +292 -0
- package/src/services/headlessModels.service.js +26 -6
- package/src/services/healthChecks.service.js +650 -0
- package/src/services/healthChecksBootstrap.service.js +109 -0
- package/src/services/healthChecksScheduler.service.js +106 -0
- package/src/services/llmDefaults.service.js +190 -0
- package/src/services/migrationAssets/s3.js +2 -2
- package/src/services/pages.service.js +602 -0
- package/src/services/pagesContext.service.js +331 -0
- package/src/services/pagesContextBlocksAi.service.js +349 -0
- package/src/services/proxy.service.js +535 -0
- package/src/services/rateLimiter.service.js +623 -0
- package/src/services/rbac.service.js +212 -0
- package/src/services/scriptsRunner.service.js +259 -0
- package/src/services/terminals.service.js +152 -0
- package/src/services/terminalsWs.service.js +100 -0
- package/src/services/uiComponentsAi.service.js +299 -0
- package/src/services/uiComponentsCrypto.service.js +39 -0
- package/src/services/workflow.service.js +23 -8
- package/src/utils/orgRoles.js +14 -0
- package/src/utils/rbac/engine.js +60 -0
- package/src/utils/rbac/rightsRegistry.js +29 -0
- package/views/admin-blog-automation.ejs +877 -0
- package/views/admin-blog-edit.ejs +542 -0
- package/views/admin-blog.ejs +399 -0
- package/views/admin-cache.ejs +681 -0
- package/views/admin-console-manager.ejs +680 -0
- package/views/admin-crons.ejs +645 -0
- package/views/admin-db-browser.ejs +445 -0
- package/views/admin-ejs-virtual.ejs +16 -10
- package/views/admin-file-manager.ejs +942 -0
- package/views/admin-headless.ejs +294 -24
- package/views/admin-health-checks.ejs +725 -0
- package/views/admin-i18n.ejs +59 -5
- package/views/admin-llm.ejs +99 -1
- package/views/admin-organizations.ejs +528 -10
- package/views/admin-pages.ejs +2424 -0
- package/views/admin-proxy.ejs +491 -0
- package/views/admin-rate-limiter.ejs +625 -0
- package/views/admin-rbac.ejs +1331 -0
- package/views/admin-scripts.ejs +497 -0
- package/views/admin-seo-config.ejs +61 -7
- package/views/admin-terminals.ejs +328 -0
- package/views/admin-ui-components.ejs +741 -0
- package/views/admin-users.ejs +261 -4
- package/views/admin-workflows.ejs +7 -7
- package/views/file-manager.ejs +866 -0
- package/views/pages/blocks/contact.ejs +27 -0
- package/views/pages/blocks/cta.ejs +18 -0
- package/views/pages/blocks/faq.ejs +20 -0
- package/views/pages/blocks/features.ejs +19 -0
- package/views/pages/blocks/hero.ejs +13 -0
- package/views/pages/blocks/html.ejs +5 -0
- package/views/pages/blocks/image.ejs +14 -0
- package/views/pages/blocks/testimonials.ejs +26 -0
- package/views/pages/blocks/text.ejs +10 -0
- package/views/pages/layouts/default.ejs +51 -0
- package/views/pages/layouts/minimal.ejs +42 -0
- package/views/pages/layouts/sidebar.ejs +54 -0
- package/views/pages/partials/footer.ejs +13 -0
- package/views/pages/partials/header.ejs +12 -0
- package/views/pages/partials/sidebar.ejs +8 -0
- package/views/pages/runtime/page.ejs +10 -0
- package/views/pages/templates/article.ejs +20 -0
- package/views/pages/templates/default.ejs +12 -0
- package/views/pages/templates/landing.ejs +14 -0
- package/views/pages/templates/listing.ejs +15 -0
- package/views/partials/admin-image-upload-modal.ejs +221 -0
- package/views/partials/dashboard/nav-items.ejs +14 -0
- package/views/partials/llm-provider-model-picker.ejs +183 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
const { WebSocketServer } = require('ws');
|
|
2
|
+
const url = require('url');
|
|
3
|
+
|
|
4
|
+
const { createSession, getSession, writeSession, resizeSession, touch } = require('./terminals.service');
|
|
5
|
+
|
|
6
|
+
function isBasicAuthValid(req, options) {
|
|
7
|
+
const authHeader = req.headers['authorization'] || '';
|
|
8
|
+
if (!String(authHeader).startsWith('Basic ')) return false;
|
|
9
|
+
|
|
10
|
+
const decoded = Buffer.from(String(authHeader).slice(6), 'base64').toString('utf-8');
|
|
11
|
+
const parts = decoded.split(':');
|
|
12
|
+
const username = parts[0] || '';
|
|
13
|
+
const password = parts.slice(1).join(':') || '';
|
|
14
|
+
|
|
15
|
+
const adminUsername = (options && options.adminUsername) || process.env.ADMIN_USERNAME || 'admin';
|
|
16
|
+
const adminPassword = (options && options.adminPassword) || process.env.ADMIN_PASSWORD || 'admin';
|
|
17
|
+
|
|
18
|
+
return username === adminUsername && password === adminPassword;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function attachTerminalWebsocketServer(server, options = {}) {
|
|
22
|
+
const wsPath = '/api/admin/terminals/ws';
|
|
23
|
+
|
|
24
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
25
|
+
|
|
26
|
+
console.log(`[Terminals] WebSocket upgrade path: ${wsPath}`);
|
|
27
|
+
|
|
28
|
+
server.on('upgrade', (req, socket, head) => {
|
|
29
|
+
const parsed = url.parse(req.url, true);
|
|
30
|
+
if (!parsed || parsed.pathname !== wsPath) return;
|
|
31
|
+
|
|
32
|
+
console.log(`[Terminals] WebSocket upgrade request for ${parsed.pathname}`);
|
|
33
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
34
|
+
wss.emit('connection', ws, req, parsed);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
wss.on('connection', (ws, req, parsed) => {
|
|
39
|
+
const q = parsed && parsed.query ? parsed.query : {};
|
|
40
|
+
let sessionId = q.sessionId ? String(q.sessionId) : null;
|
|
41
|
+
|
|
42
|
+
if (!sessionId) {
|
|
43
|
+
const created = createSession({ cols: 120, rows: 30 });
|
|
44
|
+
sessionId = created.sessionId;
|
|
45
|
+
ws.send(JSON.stringify({ type: 'session', sessionId }));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const s = getSession(sessionId);
|
|
49
|
+
if (!s || s.status !== 'running') {
|
|
50
|
+
ws.send(JSON.stringify({ type: 'error', error: 'Session not found' }));
|
|
51
|
+
ws.close();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
ws.send(JSON.stringify({ type: 'status', status: 'running', sessionId }));
|
|
56
|
+
|
|
57
|
+
const onData = (data) => {
|
|
58
|
+
try {
|
|
59
|
+
ws.send(JSON.stringify({ type: 'output', data: String(data || '') }));
|
|
60
|
+
} catch {}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
s.pty.onData(onData);
|
|
64
|
+
|
|
65
|
+
ws.on('message', (raw) => {
|
|
66
|
+
touch(sessionId);
|
|
67
|
+
let msg;
|
|
68
|
+
try {
|
|
69
|
+
msg = JSON.parse(String(raw || ''));
|
|
70
|
+
} catch {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (!msg || typeof msg !== 'object') return;
|
|
74
|
+
|
|
75
|
+
if (msg.type === 'input') {
|
|
76
|
+
writeSession(sessionId, msg.data);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (msg.type === 'resize') {
|
|
80
|
+
resizeSession(sessionId, msg.cols, msg.rows);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
ws.on('close', () => {
|
|
85
|
+
try {
|
|
86
|
+
s.pty.offData(onData);
|
|
87
|
+
} catch {}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
ws.on('error', () => {
|
|
91
|
+
try {
|
|
92
|
+
s.pty.offData(onData);
|
|
93
|
+
} catch {}
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return { wss, wsPath };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = { attachTerminalWebsocketServer };
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
const UiComponent = require('../models/UiComponent');
|
|
2
|
+
const llmService = require('./llm.service');
|
|
3
|
+
const { resolveLlmProviderModel } = require('./llmDefaults.service');
|
|
4
|
+
const { createAuditEvent } = require('./audit.service');
|
|
5
|
+
|
|
6
|
+
const ALLOWED_FIELDS = new Set(['html', 'css', 'js', 'usageMarkdown']);
|
|
7
|
+
|
|
8
|
+
function normalizeTargets(targets) {
|
|
9
|
+
const t = targets && typeof targets === 'object' ? targets : {};
|
|
10
|
+
const out = {};
|
|
11
|
+
for (const f of ALLOWED_FIELDS) out[f] = Boolean(t[f]);
|
|
12
|
+
if (!Object.values(out).some(Boolean)) {
|
|
13
|
+
// default: all
|
|
14
|
+
for (const f of ALLOWED_FIELDS) out[f] = true;
|
|
15
|
+
}
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function parseFieldPatches(raw) {
|
|
20
|
+
const text = String(raw || '');
|
|
21
|
+
const lines = text.split(/\r?\n/);
|
|
22
|
+
const result = [];
|
|
23
|
+
|
|
24
|
+
let current = null;
|
|
25
|
+
for (const line of lines) {
|
|
26
|
+
const m = line.match(/^FIELD:\s*(.+)$/);
|
|
27
|
+
if (m) {
|
|
28
|
+
if (current) result.push(current);
|
|
29
|
+
current = { field: String(m[1] || '').trim(), content: '' };
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (!current) continue;
|
|
33
|
+
current.content += (current.content ? '\n' : '') + line;
|
|
34
|
+
}
|
|
35
|
+
if (current) result.push(current);
|
|
36
|
+
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseDiffBlocks(patchText) {
|
|
41
|
+
const raw = String(patchText || '');
|
|
42
|
+
const blocks = [];
|
|
43
|
+
|
|
44
|
+
const re = /<<<<<<< SEARCH\n([\s\S]*?)\n=======\n([\s\S]*?)\n>>>>>>> REPLACE/g;
|
|
45
|
+
let m;
|
|
46
|
+
while ((m = re.exec(raw)) !== null) {
|
|
47
|
+
blocks.push({ search: m[1], replace: m[2] });
|
|
48
|
+
}
|
|
49
|
+
return blocks;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function applyBlocks(currentValue, blocks) {
|
|
53
|
+
let next = String(currentValue || '');
|
|
54
|
+
|
|
55
|
+
for (const b of blocks) {
|
|
56
|
+
const search = String(b.search);
|
|
57
|
+
const replace = String(b.replace);
|
|
58
|
+
|
|
59
|
+
if (search === '__FULL__') {
|
|
60
|
+
next = replace;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const idx = next.indexOf(search);
|
|
65
|
+
if (idx === -1) {
|
|
66
|
+
const err = new Error('SEARCH block did not match current content');
|
|
67
|
+
err.code = 'AI_INVALID';
|
|
68
|
+
err.meta = { searchPreview: search.slice(0, 120) };
|
|
69
|
+
throw err;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
next = next.replace(search, replace);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return next;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function computeWarnings(nextFields) {
|
|
79
|
+
const warnings = [];
|
|
80
|
+
const js = String(nextFields.js || '');
|
|
81
|
+
|
|
82
|
+
const checks = [
|
|
83
|
+
{ token: 'eval(', msg: 'JS contains eval( which is unsafe.' },
|
|
84
|
+
{ token: 'document.cookie', msg: 'JS references document.cookie.' },
|
|
85
|
+
{ token: 'Function(', msg: 'JS contains Function( which may indicate dynamic code execution.' },
|
|
86
|
+
{ token: 'fetch(', msg: 'JS uses fetch(. Consider origin allowlists and error handling.' },
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
for (const c of checks) {
|
|
90
|
+
if (js.includes(c.token)) warnings.push(c.msg);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return warnings;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function resolveLlmDefaults({ providerKey, model }) {
|
|
97
|
+
return resolveLlmProviderModel({
|
|
98
|
+
systemKey: 'uiComponents.proposeEdit',
|
|
99
|
+
providerKey,
|
|
100
|
+
model,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function buildSystemPrompt({ targets }) {
|
|
105
|
+
const allowed = Array.from(ALLOWED_FIELDS).filter((f) => targets[f]);
|
|
106
|
+
|
|
107
|
+
return [
|
|
108
|
+
'You are a code editor assistant modifying a UI component stored in a database.',
|
|
109
|
+
`You may edit ONLY these fields: ${allowed.join(', ')}.`,
|
|
110
|
+
'Return ONLY changes using FIELD-based SEARCH/REPLACE patches.',
|
|
111
|
+
'',
|
|
112
|
+
'Format:',
|
|
113
|
+
'FIELD: <fieldName>',
|
|
114
|
+
'<<<<<<< SEARCH',
|
|
115
|
+
'[exact text to find - must match character-by-character including whitespace]',
|
|
116
|
+
'=======',
|
|
117
|
+
'[replacement text]',
|
|
118
|
+
'>>>>>>> REPLACE',
|
|
119
|
+
'',
|
|
120
|
+
'Rules:',
|
|
121
|
+
'- You can include multiple FIELD sections.',
|
|
122
|
+
'- SEARCH must match exactly (whitespace matters).',
|
|
123
|
+
'- Include enough context (5-10 lines) for unique matching.',
|
|
124
|
+
'- Do not include any text outside FIELD sections and SEARCH/REPLACE blocks.',
|
|
125
|
+
"- If you cannot reliably match the existing text, use SEARCH content '__FULL__' to replace the entire field.",
|
|
126
|
+
'',
|
|
127
|
+
'JS contract:',
|
|
128
|
+
"- The component JS is executed as new Function('api','templateRootEl','props', js).",
|
|
129
|
+
'- Your JS must return an object with methods.',
|
|
130
|
+
'- Use templateRootEl for DOM queries (do not use document.querySelector without scoping).',
|
|
131
|
+
].join('\n');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function proposeComponentEdit({
|
|
135
|
+
code,
|
|
136
|
+
prompt,
|
|
137
|
+
providerKey,
|
|
138
|
+
model,
|
|
139
|
+
targets,
|
|
140
|
+
mode,
|
|
141
|
+
actor,
|
|
142
|
+
}) {
|
|
143
|
+
const componentCode = String(code || '').trim().toLowerCase();
|
|
144
|
+
if (!componentCode) {
|
|
145
|
+
const err = new Error('code is required');
|
|
146
|
+
err.code = 'VALIDATION';
|
|
147
|
+
throw err;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const instruction = String(prompt || '').trim();
|
|
151
|
+
if (!instruction) {
|
|
152
|
+
const err = new Error('prompt is required');
|
|
153
|
+
err.code = 'VALIDATION';
|
|
154
|
+
throw err;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const targetFlags = normalizeTargets(targets);
|
|
158
|
+
const allowedTargets = Object.entries(targetFlags)
|
|
159
|
+
.filter(([, v]) => v)
|
|
160
|
+
.map(([k]) => k);
|
|
161
|
+
|
|
162
|
+
const doc = await UiComponent.findOne({ code: componentCode });
|
|
163
|
+
if (!doc) {
|
|
164
|
+
const err = new Error('Component not found');
|
|
165
|
+
err.code = 'NOT_FOUND';
|
|
166
|
+
throw err;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const current = doc.toObject();
|
|
170
|
+
|
|
171
|
+
const llmDefaults = await resolveLlmDefaults({ providerKey, model });
|
|
172
|
+
|
|
173
|
+
const systemPrompt = buildSystemPrompt({ targets: targetFlags });
|
|
174
|
+
|
|
175
|
+
const context = {
|
|
176
|
+
code: current.code,
|
|
177
|
+
name: current.name,
|
|
178
|
+
version: current.version,
|
|
179
|
+
html: String(current.html || ''),
|
|
180
|
+
css: String(current.css || ''),
|
|
181
|
+
js: String(current.js || ''),
|
|
182
|
+
usageMarkdown: String(current.usageMarkdown || ''),
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const userContextLines = [
|
|
186
|
+
`Component code: ${context.code}`,
|
|
187
|
+
`Component name: ${context.name}`,
|
|
188
|
+
`Component version: ${context.version}`,
|
|
189
|
+
`Target fields: ${allowedTargets.join(', ')}`,
|
|
190
|
+
`Mode: ${String(mode || 'minimal')}`,
|
|
191
|
+
'',
|
|
192
|
+
'Current fields:',
|
|
193
|
+
'',
|
|
194
|
+
`FIELD: html\n${context.html}`,
|
|
195
|
+
'',
|
|
196
|
+
`FIELD: css\n${context.css}`,
|
|
197
|
+
'',
|
|
198
|
+
`FIELD: js\n${context.js}`,
|
|
199
|
+
'',
|
|
200
|
+
`FIELD: usageMarkdown\n${context.usageMarkdown}`,
|
|
201
|
+
];
|
|
202
|
+
|
|
203
|
+
const result = await llmService.callAdhoc(
|
|
204
|
+
{
|
|
205
|
+
providerKey: llmDefaults.providerKey,
|
|
206
|
+
model: llmDefaults.model,
|
|
207
|
+
messages: [
|
|
208
|
+
{ role: 'system', content: systemPrompt },
|
|
209
|
+
{ role: 'user', content: `Instruction:\n${instruction}` },
|
|
210
|
+
{ role: 'user', content: userContextLines.join('\n') },
|
|
211
|
+
],
|
|
212
|
+
promptKeyForAudit: 'uiComponents.ai.propose',
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
temperature: String(mode || '').toLowerCase() === 'rewrite' ? 0.6 : 0.3,
|
|
216
|
+
},
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const raw = String(result.content || '');
|
|
220
|
+
const fieldPatches = parseFieldPatches(raw);
|
|
221
|
+
|
|
222
|
+
const patchByField = new Map();
|
|
223
|
+
for (const fp of fieldPatches) {
|
|
224
|
+
patchByField.set(String(fp.field || '').trim(), fp.content);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const nextFields = {
|
|
228
|
+
html: context.html,
|
|
229
|
+
css: context.css,
|
|
230
|
+
js: context.js,
|
|
231
|
+
usageMarkdown: context.usageMarkdown,
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const appliedFields = [];
|
|
235
|
+
|
|
236
|
+
for (const field of ALLOWED_FIELDS) {
|
|
237
|
+
if (!targetFlags[field]) continue;
|
|
238
|
+
const patchText = patchByField.get(field);
|
|
239
|
+
if (!patchText) continue;
|
|
240
|
+
|
|
241
|
+
const blocks = parseDiffBlocks(patchText);
|
|
242
|
+
if (!blocks.length) {
|
|
243
|
+
const err = new Error(`No diff blocks found for field ${field}`);
|
|
244
|
+
err.code = 'AI_INVALID';
|
|
245
|
+
throw err;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
nextFields[field] = applyBlocks(nextFields[field], blocks);
|
|
249
|
+
appliedFields.push(field);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (!appliedFields.length) {
|
|
253
|
+
const err = new Error('No applicable field patches returned');
|
|
254
|
+
err.code = 'AI_INVALID';
|
|
255
|
+
throw err;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const warnings = computeWarnings(nextFields);
|
|
259
|
+
|
|
260
|
+
await createAuditEvent({
|
|
261
|
+
...(actor || { actorType: 'system', actorId: null }),
|
|
262
|
+
action: 'uiComponents.ai.propose',
|
|
263
|
+
entityType: 'UiComponent',
|
|
264
|
+
entityId: componentCode,
|
|
265
|
+
before: {
|
|
266
|
+
code: current.code,
|
|
267
|
+
version: current.version,
|
|
268
|
+
},
|
|
269
|
+
after: {
|
|
270
|
+
code: current.code,
|
|
271
|
+
version: current.version,
|
|
272
|
+
appliedFields,
|
|
273
|
+
},
|
|
274
|
+
meta: {
|
|
275
|
+
providerKey: llmDefaults.providerKey,
|
|
276
|
+
model: llmDefaults.model,
|
|
277
|
+
targets: targetFlags,
|
|
278
|
+
mode: String(mode || 'minimal'),
|
|
279
|
+
warnings,
|
|
280
|
+
patchPreview: raw.slice(0, 4000),
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
component: { code: current.code, version: current.version },
|
|
286
|
+
proposal: {
|
|
287
|
+
patch: raw,
|
|
288
|
+
fields: nextFields,
|
|
289
|
+
appliedFields,
|
|
290
|
+
warnings,
|
|
291
|
+
},
|
|
292
|
+
providerKey: llmDefaults.providerKey,
|
|
293
|
+
model: llmDefaults.model,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
module.exports = {
|
|
298
|
+
proposeComponentEdit,
|
|
299
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
|
|
3
|
+
function base64UrlEncode(buf) {
|
|
4
|
+
return buf
|
|
5
|
+
.toString('base64')
|
|
6
|
+
.replace(/\+/g, '-')
|
|
7
|
+
.replace(/\//g, '_')
|
|
8
|
+
.replace(/=+$/g, '');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function generateProjectApiKeyPlaintext() {
|
|
12
|
+
const raw = crypto.randomBytes(32);
|
|
13
|
+
return `uk_${base64UrlEncode(raw)}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function hashKey(plaintext) {
|
|
17
|
+
return crypto.createHash('sha256').update(String(plaintext)).digest('hex');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function timingSafeEqualHex(a, b) {
|
|
21
|
+
const aBuf = Buffer.from(String(a), 'hex');
|
|
22
|
+
const bBuf = Buffer.from(String(b), 'hex');
|
|
23
|
+
if (aBuf.length !== bBuf.length) return false;
|
|
24
|
+
return crypto.timingSafeEqual(aBuf, bBuf);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function verifyKey(plaintext, expectedHash) {
|
|
28
|
+
const provided = String(plaintext || '').trim();
|
|
29
|
+
if (!provided) return false;
|
|
30
|
+
const providedHash = hashKey(provided);
|
|
31
|
+
return timingSafeEqualHex(providedHash, expectedHash);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = {
|
|
35
|
+
generateProjectApiKeyPlaintext,
|
|
36
|
+
hashKey,
|
|
37
|
+
timingSafeEqualHex,
|
|
38
|
+
verifyKey,
|
|
39
|
+
};
|
|
@@ -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
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
|
package/src/utils/orgRoles.js
CHANGED
|
@@ -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,29 @@
|
|
|
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
|
+
'file_manager:*',
|
|
10
|
+
'file_manager:access',
|
|
11
|
+
'file_manager:drives:read',
|
|
12
|
+
'file_manager:files:read',
|
|
13
|
+
'file_manager:files:upload',
|
|
14
|
+
'file_manager:files:download',
|
|
15
|
+
'file_manager:files:update',
|
|
16
|
+
'file_manager:files:delete',
|
|
17
|
+
'file_manager:files:share',
|
|
18
|
+
'backoffice:*',
|
|
19
|
+
'backoffice:dashboard:access',
|
|
20
|
+
'*',
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
function listRights() {
|
|
24
|
+
return Array.from(new Set(DEFAULT_RIGHTS)).sort();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = {
|
|
28
|
+
listRights,
|
|
29
|
+
};
|