@intranefr/superbackend 1.4.3 → 1.5.0
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 +6 -1
- package/README.md +5 -5
- package/index.js +23 -5
- package/package.json +5 -2
- package/public/sdk/ui-components.iife.js +191 -0
- package/sdk/error-tracking/browser/package.json +4 -3
- package/sdk/error-tracking/browser/src/embed.js +29 -0
- package/sdk/ui-components/browser/src/index.js +228 -0
- package/src/controllers/admin.controller.js +139 -1
- package/src/controllers/adminHeadless.controller.js +82 -0
- package/src/controllers/adminMigration.controller.js +5 -1
- package/src/controllers/adminScripts.controller.js +229 -0
- 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/orgAdmin.controller.js +286 -0
- package/src/controllers/uiComponentsPublic.controller.js +118 -0
- package/src/middleware/auth.js +7 -0
- package/src/middleware.js +119 -0
- package/src/models/HeadlessModelDefinition.js +10 -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 +2 -0
- package/src/routes/adminHeadless.routes.js +6 -0
- package/src/routes/adminScripts.routes.js +21 -0
- package/src/routes/adminTerminals.routes.js +13 -0
- package/src/routes/adminUiComponents.routes.js +29 -0
- package/src/routes/llmUi.routes.js +26 -0
- package/src/routes/orgAdmin.routes.js +5 -0
- package/src/routes/uiComponentsPublic.routes.js +9 -0
- package/src/services/consoleOverride.service.js +291 -0
- package/src/services/email.service.js +17 -1
- package/src/services/headlessExternalModels.service.js +292 -0
- package/src/services/headlessModels.service.js +26 -6
- 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 +312 -0
- package/src/services/uiComponentsCrypto.service.js +39 -0
- package/src/services/webhook.service.js +2 -2
- package/src/services/workflow.service.js +1 -1
- package/src/utils/encryption.js +5 -3
- package/views/admin-coolify-deploy.ejs +1 -1
- package/views/admin-dashboard-home.ejs +1 -1
- package/views/admin-dashboard.ejs +1 -1
- package/views/admin-errors.ejs +2 -2
- package/views/admin-global-settings.ejs +3 -3
- package/views/admin-headless.ejs +294 -24
- package/views/admin-json-configs.ejs +8 -1
- package/views/admin-llm.ejs +2 -2
- package/views/admin-organizations.ejs +365 -9
- package/views/admin-scripts.ejs +497 -0
- package/views/admin-seo-config.ejs +1 -1
- package/views/admin-terminals.ejs +328 -0
- package/views/admin-test.ejs +3 -3
- package/views/admin-ui-components.ejs +709 -0
- package/views/admin-users.ejs +440 -4
- package/views/admin-webhooks.ejs +1 -1
- package/views/admin-workflows.ejs +1 -1
- package/views/partials/admin-assets-script.ejs +3 -3
- package/views/partials/dashboard/nav-items.ejs +3 -0
- package/views/partials/dashboard/palette.ejs +1 -1
package/.env.example
CHANGED
|
@@ -43,5 +43,10 @@ MAX_FILE_SIZE_HARD_CAP=10485760
|
|
|
43
43
|
# S3_REGION=us-east-1
|
|
44
44
|
# S3_ACCESS_KEY_ID=minioadmin
|
|
45
45
|
# S3_SECRET_ACCESS_KEY=minioadmin
|
|
46
|
-
# S3_BUCKET=
|
|
46
|
+
# S3_BUCKET=superbackend
|
|
47
|
+
# Legacy fallback: S3_BUCKET=saasbackend
|
|
47
48
|
# S3_FORCE_PATH_STYLE=true
|
|
49
|
+
|
|
50
|
+
# Encryption key for encrypted settings (new preferred name)
|
|
51
|
+
# SUPERBACKEND_ENCRYPTION_KEY=your-32-byte-encryption-key
|
|
52
|
+
# Legacy fallback: SAASBACKEND_ENCRYPTION_KEY=your-32-byte-encryption-key
|
package/README.md
CHANGED
|
@@ -29,13 +29,13 @@ Node.js middleware that gives your project backend superpowers. Handles authenti
|
|
|
29
29
|
## Installation
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
|
-
npm install superbackend
|
|
32
|
+
npm install @intranefr/superbackend
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
or
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
|
-
yarn add superbackend
|
|
38
|
+
yarn add @intranefr/superbackend
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
---
|
|
@@ -45,7 +45,7 @@ yarn add superbackend
|
|
|
45
45
|
```javascript
|
|
46
46
|
require('dotenv').config();
|
|
47
47
|
const express = require('express');
|
|
48
|
-
const { middleware } = require('
|
|
48
|
+
const { middleware } = require('@intranefr/superbackend');
|
|
49
49
|
|
|
50
50
|
const app = express();
|
|
51
51
|
|
|
@@ -101,8 +101,8 @@ Please read the [CONTRIBUTING.md](#) for guidelines.
|
|
|
101
101
|
<img src="https://img.shields.io/badge/Intrane-intranefr-blue?style=flat-square" alt="Intrane"/>
|
|
102
102
|
</a>
|
|
103
103
|
|
|
104
|
-
<a href="https://www.npmjs.com/package/superbackend" target="_blank">
|
|
105
|
-
<img src="https://img.shields.io/npm/v
|
|
104
|
+
<a href="https://www.npmjs.com/package/@intranefr/superbackend" target="_blank">
|
|
105
|
+
<img src="https://img.shields.io/npm/v/@intranefr%2Fsuperbackend?style=flat-square" alt="npm"/>
|
|
106
106
|
</a>
|
|
107
107
|
|
|
108
108
|
## License
|
package/index.js
CHANGED
|
@@ -2,7 +2,7 @@ require("dotenv").config({ path: process.env.ENV_FILE || ".env" });
|
|
|
2
2
|
const express = require("express");
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Creates the
|
|
5
|
+
* Creates the SuperBackend as Express middleware
|
|
6
6
|
* @param {Object} options - Configuration options
|
|
7
7
|
* @param {string} options.mongodbUri - MongoDB connection string
|
|
8
8
|
* @param {string} options.corsOrigin - CORS origin(s)
|
|
@@ -11,9 +11,10 @@ const express = require("express");
|
|
|
11
11
|
* @returns {express.Router} Configured Express router
|
|
12
12
|
*/
|
|
13
13
|
const middleware = require("./src/middleware");
|
|
14
|
+
const { attachTerminalWebsocketServer } = require('./src/services/terminalsWs.service');
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
|
-
* Creates and starts a standalone
|
|
17
|
+
* Creates and starts a standalone SuperBackend server
|
|
17
18
|
* @param {Object} options - Configuration options
|
|
18
19
|
* @param {number} options.port - Port to listen on
|
|
19
20
|
* @param {string} options.mongodbUri - MongoDB connection string
|
|
@@ -24,20 +25,36 @@ function startServer(options = {}) {
|
|
|
24
25
|
const app = express();
|
|
25
26
|
const PORT = options.port || process.env.PORT || 3000;
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
const router = module.exports.middleware(options);
|
|
29
|
+
app.use(router);
|
|
28
30
|
|
|
29
31
|
// Start server
|
|
30
32
|
const server = app.listen(PORT, () => {
|
|
31
|
-
console.log(`🚀
|
|
33
|
+
console.log(`🚀 SuperBackend standalone server running on http://localhost:${PORT}`);
|
|
32
34
|
});
|
|
33
35
|
|
|
36
|
+
// Attach WebSocket server via middleware helper or directly
|
|
37
|
+
console.log('[Index] Attaching WebSocket server...');
|
|
38
|
+
if (typeof router.attachWs === 'function') {
|
|
39
|
+
console.log('[Index] Using router.attachWs');
|
|
40
|
+
router.attachWs(server);
|
|
41
|
+
} else {
|
|
42
|
+
// Fallback: attach directly with admin path
|
|
43
|
+
const adminPath = router.adminPath || '/admin';
|
|
44
|
+
console.log('[Index] Using fallback attach with adminPath:', adminPath);
|
|
45
|
+
attachTerminalWebsocketServer(server, { basePathPrefix: adminPath });
|
|
46
|
+
}
|
|
47
|
+
|
|
34
48
|
return { app, server };
|
|
35
49
|
}
|
|
36
50
|
|
|
37
51
|
const saasbackend = {
|
|
38
52
|
server: startServer,
|
|
53
|
+
consoleOverride: require("./src/services/consoleOverride.service"),
|
|
39
54
|
middleware: (options = {}) => {
|
|
40
|
-
|
|
55
|
+
// Set both registries for backward compatibility
|
|
56
|
+
globalThis.superbackend = saasbackend;
|
|
57
|
+
globalThis.saasbackend = saasbackend; // Legacy support
|
|
41
58
|
return middleware(options);
|
|
42
59
|
},
|
|
43
60
|
services: {
|
|
@@ -88,6 +105,7 @@ const saasbackend = {
|
|
|
88
105
|
org: require("./src/middleware/org"),
|
|
89
106
|
i18n: require("./src/services/i18n.service"),
|
|
90
107
|
jsonConfigs: require("./src/services/jsonConfigs.service"),
|
|
108
|
+
terminals: require("./src/services/terminalsWs.service"),
|
|
91
109
|
},
|
|
92
110
|
};
|
|
93
111
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intranefr/superbackend",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Node.js middleware that gives your project backend superpowers",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"start:minio": "docker compose -f compose.standalone.yml --profile minio-only up -d minio",
|
|
10
10
|
"minio:envs": "node -e \"console.log(['S3_ENDPOINT=http://localhost:9000','S3_REGION=us-east-1','S3_ACCESS_KEY_ID=minioadmin','S3_SECRET_ACCESS_KEY=minioadmin','S3_BUCKET=saasbackend','S3_FORCE_PATH_STYLE=true'].join('\\n'))\"",
|
|
11
11
|
"build:sdk:error-tracking:browser": "esbuild sdk/error-tracking/browser/src/embed.js --bundle --format=iife --global-name=saasbackendErrorTrackingEmbed --outfile=sdk/error-tracking/browser/dist/embed.iife.js",
|
|
12
|
+
"build:sdk:ui-components:browser": "esbuild sdk/ui-components/browser/src/index.js --bundle --format=iife --outfile=public/sdk/ui-components.iife.js",
|
|
12
13
|
"test": "jest",
|
|
13
14
|
"test:watch": "jest --watch",
|
|
14
15
|
"test:coverage": "jest --coverage"
|
|
@@ -41,11 +42,13 @@
|
|
|
41
42
|
"jsonwebtoken": "^9.0.2",
|
|
42
43
|
"mongoose": "^8.0.0",
|
|
43
44
|
"multer": "^1.4.5-lts.1",
|
|
45
|
+
"node-pty": "^1.1.0",
|
|
44
46
|
"openai": "^4.0.0",
|
|
45
47
|
"resend": "^6.4.0",
|
|
46
48
|
"ssh2-sftp-client": "^12.0.1",
|
|
47
49
|
"stripe": "^14.0.0",
|
|
48
|
-
"vm2": "^3.10.0"
|
|
50
|
+
"vm2": "^3.10.0",
|
|
51
|
+
"ws": "^8.18.0"
|
|
49
52
|
},
|
|
50
53
|
"devDependencies": {
|
|
51
54
|
"esbuild": "^0.25.0",
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
// sdk/ui-components/browser/src/index.js
|
|
3
|
+
(function() {
|
|
4
|
+
function toStr(v) {
|
|
5
|
+
return v === void 0 || v === null ? "" : String(v);
|
|
6
|
+
}
|
|
7
|
+
function normalizeApiUrl(apiUrl) {
|
|
8
|
+
const u = toStr(apiUrl).trim();
|
|
9
|
+
if (!u) return "";
|
|
10
|
+
return u.replace(/\/$/, "");
|
|
11
|
+
}
|
|
12
|
+
function buildHeaders(apiKey) {
|
|
13
|
+
const headers = {};
|
|
14
|
+
const key = toStr(apiKey).trim();
|
|
15
|
+
if (key) headers["x-project-key"] = key;
|
|
16
|
+
return headers;
|
|
17
|
+
}
|
|
18
|
+
async function fetchJson(url, headers) {
|
|
19
|
+
const res = await fetch(url, { headers: headers || {} });
|
|
20
|
+
const text = await res.text();
|
|
21
|
+
let data = null;
|
|
22
|
+
try {
|
|
23
|
+
data = text ? JSON.parse(text) : null;
|
|
24
|
+
} catch {
|
|
25
|
+
data = null;
|
|
26
|
+
}
|
|
27
|
+
if (!res.ok) {
|
|
28
|
+
const msg = data && data.error ? data.error : "Request failed";
|
|
29
|
+
const err = new Error(msg);
|
|
30
|
+
err.status = res.status;
|
|
31
|
+
throw err;
|
|
32
|
+
}
|
|
33
|
+
return data;
|
|
34
|
+
}
|
|
35
|
+
function ensureTemplate(code, html) {
|
|
36
|
+
const id = "ui-cmp-" + code;
|
|
37
|
+
let tpl = document.getElementById(id);
|
|
38
|
+
if (!tpl) {
|
|
39
|
+
tpl = document.createElement("template");
|
|
40
|
+
tpl.id = id;
|
|
41
|
+
document.body.appendChild(tpl);
|
|
42
|
+
}
|
|
43
|
+
tpl.innerHTML = toStr(html);
|
|
44
|
+
return tpl;
|
|
45
|
+
}
|
|
46
|
+
function injectCssScoped(code, cssText) {
|
|
47
|
+
const id = "ui-cmp-style-" + code;
|
|
48
|
+
let el = document.getElementById(id);
|
|
49
|
+
if (!el) {
|
|
50
|
+
el = document.createElement("style");
|
|
51
|
+
el.id = id;
|
|
52
|
+
document.head.appendChild(el);
|
|
53
|
+
}
|
|
54
|
+
el.textContent = toStr(cssText);
|
|
55
|
+
}
|
|
56
|
+
function createShadowRootContainer() {
|
|
57
|
+
const host = document.createElement("div");
|
|
58
|
+
host.style.all = "initial";
|
|
59
|
+
const shadow = host.attachShadow({ mode: "open" });
|
|
60
|
+
return { host, shadow };
|
|
61
|
+
}
|
|
62
|
+
function defaultMountTarget() {
|
|
63
|
+
return document.body;
|
|
64
|
+
}
|
|
65
|
+
function compileComponentJs(jsCode) {
|
|
66
|
+
const code = toStr(jsCode);
|
|
67
|
+
if (!code.trim()) {
|
|
68
|
+
return function() {
|
|
69
|
+
return {};
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return new Function("api", "templateRootEl", "props", code);
|
|
73
|
+
}
|
|
74
|
+
const state = {
|
|
75
|
+
initialized: false,
|
|
76
|
+
projectId: null,
|
|
77
|
+
apiKey: null,
|
|
78
|
+
apiUrl: "",
|
|
79
|
+
cssIsolation: "scoped",
|
|
80
|
+
components: {}
|
|
81
|
+
};
|
|
82
|
+
function registerComponent(def) {
|
|
83
|
+
const code = toStr(def.code).trim().toLowerCase();
|
|
84
|
+
if (!code) return;
|
|
85
|
+
const version = def.version;
|
|
86
|
+
const html = def.html;
|
|
87
|
+
const js = def.js;
|
|
88
|
+
const css = def.css;
|
|
89
|
+
ensureTemplate(code, html);
|
|
90
|
+
const component = {
|
|
91
|
+
code,
|
|
92
|
+
version,
|
|
93
|
+
css,
|
|
94
|
+
js,
|
|
95
|
+
create: function(props, options) {
|
|
96
|
+
const opts = options || {};
|
|
97
|
+
const mountEl = opts.mountEl || defaultMountTarget();
|
|
98
|
+
const isolation = opts.cssIsolation || state.cssIsolation;
|
|
99
|
+
const tpl = ensureTemplate(code, html);
|
|
100
|
+
const fragment = tpl.content.cloneNode(true);
|
|
101
|
+
let templateRootEl;
|
|
102
|
+
let instanceRoot;
|
|
103
|
+
let shadow = null;
|
|
104
|
+
if (isolation === "shadow") {
|
|
105
|
+
const c = createShadowRootContainer();
|
|
106
|
+
instanceRoot = c.host;
|
|
107
|
+
shadow = c.shadow;
|
|
108
|
+
templateRootEl = shadow;
|
|
109
|
+
if (css) {
|
|
110
|
+
const style = document.createElement("style");
|
|
111
|
+
style.textContent = toStr(css);
|
|
112
|
+
shadow.appendChild(style);
|
|
113
|
+
}
|
|
114
|
+
shadow.appendChild(fragment);
|
|
115
|
+
} else {
|
|
116
|
+
instanceRoot = document.createElement("div");
|
|
117
|
+
templateRootEl = instanceRoot;
|
|
118
|
+
if (css) injectCssScoped(code, css);
|
|
119
|
+
instanceRoot.appendChild(fragment);
|
|
120
|
+
}
|
|
121
|
+
mountEl.appendChild(instanceRoot);
|
|
122
|
+
const api = {
|
|
123
|
+
unmount: function() {
|
|
124
|
+
try {
|
|
125
|
+
instanceRoot.remove();
|
|
126
|
+
} catch {
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
mountEl,
|
|
130
|
+
hostEl: instanceRoot,
|
|
131
|
+
shadowRoot: shadow
|
|
132
|
+
};
|
|
133
|
+
const fn = compileComponentJs(js);
|
|
134
|
+
let exported = {};
|
|
135
|
+
try {
|
|
136
|
+
exported = fn(api, templateRootEl, props || {}) || {};
|
|
137
|
+
} catch (e) {
|
|
138
|
+
exported = {
|
|
139
|
+
error: e
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return Object.assign({ api }, exported);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
state.components[code] = component;
|
|
146
|
+
uiCmp[code] = component;
|
|
147
|
+
uiComponents[code] = component;
|
|
148
|
+
}
|
|
149
|
+
async function init(options) {
|
|
150
|
+
const opts = options || {};
|
|
151
|
+
const projectId = toStr(opts.projectId).trim();
|
|
152
|
+
if (!projectId) throw new Error("projectId is required");
|
|
153
|
+
const apiUrl = normalizeApiUrl(opts.apiUrl);
|
|
154
|
+
const apiKey = opts.apiKey;
|
|
155
|
+
state.projectId = projectId;
|
|
156
|
+
state.apiKey = apiKey;
|
|
157
|
+
state.apiUrl = apiUrl;
|
|
158
|
+
const cssIsolation = toStr(opts.cssIsolation || "scoped").trim().toLowerCase();
|
|
159
|
+
state.cssIsolation = cssIsolation === "shadow" ? "shadow" : "scoped";
|
|
160
|
+
const base = state.apiUrl;
|
|
161
|
+
const url = base + "/api/ui-components/projects/" + encodeURIComponent(projectId) + "/manifest";
|
|
162
|
+
const data = await fetchJson(url, buildHeaders(apiKey));
|
|
163
|
+
const items = data && Array.isArray(data.components) ? data.components : [];
|
|
164
|
+
for (const def of items) {
|
|
165
|
+
registerComponent(def);
|
|
166
|
+
}
|
|
167
|
+
state.initialized = true;
|
|
168
|
+
return { project: data ? data.project : null, count: items.length };
|
|
169
|
+
}
|
|
170
|
+
async function load(code) {
|
|
171
|
+
const c = toStr(code).trim().toLowerCase();
|
|
172
|
+
if (!c) throw new Error("code is required");
|
|
173
|
+
if (!state.projectId) throw new Error("uiCmp not initialized");
|
|
174
|
+
if (state.components[c]) return state.components[c];
|
|
175
|
+
const base = state.apiUrl;
|
|
176
|
+
const url = base + "/api/ui-components/projects/" + encodeURIComponent(state.projectId) + "/components/" + encodeURIComponent(c);
|
|
177
|
+
const data = await fetchJson(url, buildHeaders(state.apiKey));
|
|
178
|
+
if (!data || !data.component) throw new Error("Component not found");
|
|
179
|
+
registerComponent(data.component);
|
|
180
|
+
return state.components[c];
|
|
181
|
+
}
|
|
182
|
+
const uiCmp = {
|
|
183
|
+
init,
|
|
184
|
+
load,
|
|
185
|
+
_state: state
|
|
186
|
+
};
|
|
187
|
+
const uiComponents = uiCmp;
|
|
188
|
+
window.uiCmp = uiCmp;
|
|
189
|
+
window.uiComponents = uiComponents;
|
|
190
|
+
})();
|
|
191
|
+
})();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "@
|
|
3
|
-
"version": "
|
|
2
|
+
"name": "@intranefr/superbackend-error-tracking-browser-sdk",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Error tracking SDK for SuperBackend browser applications.",
|
|
4
5
|
"type": "module",
|
|
5
6
|
"exports": {
|
|
6
7
|
".": {
|
|
@@ -8,7 +9,7 @@
|
|
|
8
9
|
}
|
|
9
10
|
},
|
|
10
11
|
"scripts": {
|
|
11
|
-
"build": "esbuild src/embed.js --bundle --format=iife --global-name=
|
|
12
|
+
"build": "esbuild src/embed.js --bundle --format=iife --global-name=superbackendErrorTrackingEmbed --outfile=dist/embed.iife.js --minify"
|
|
12
13
|
},
|
|
13
14
|
"devDependencies": {
|
|
14
15
|
"esbuild": "^0.27.2"
|
|
@@ -1,11 +1,39 @@
|
|
|
1
1
|
import { createErrorTrackingClient } from './core.js';
|
|
2
2
|
|
|
3
|
+
function attachToSuperbackendGlobal() {
|
|
4
|
+
const root = (typeof window !== 'undefined' ? window : undefined);
|
|
5
|
+
if (!root) return;
|
|
6
|
+
|
|
7
|
+
if (root.saasbackendErrorTrackingEmbed && !root.superbackendErrorTrackingEmbed) {
|
|
8
|
+
root.superbackendErrorTrackingEmbed = root.saasbackendErrorTrackingEmbed;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
root.superbackend = root.superbackend || {};
|
|
12
|
+
|
|
13
|
+
if (!root.superbackend.errorTracking) {
|
|
14
|
+
root.superbackend.errorTracking = createErrorTrackingClient();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (root.superbackend.errorTracking && typeof root.superbackend.errorTracking.init === 'function') {
|
|
18
|
+
root.superbackend.errorTracking.init();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
3
22
|
function attachToSaasbackendGlobal() {
|
|
4
23
|
const root = (typeof window !== 'undefined' ? window : undefined);
|
|
5
24
|
if (!root) return;
|
|
6
25
|
|
|
26
|
+
// Show deprecation warning in console
|
|
27
|
+
if (console.warn) {
|
|
28
|
+
console.warn('DEPRECATION: Global "window.saasbackend" is deprecated. Use "window.superbackend" instead.');
|
|
29
|
+
}
|
|
30
|
+
|
|
7
31
|
root.saasbackend = root.saasbackend || {};
|
|
8
32
|
|
|
33
|
+
if (root.superbackendErrorTrackingEmbed && !root.saasbackendErrorTrackingEmbed) {
|
|
34
|
+
root.saasbackendErrorTrackingEmbed = root.superbackendErrorTrackingEmbed;
|
|
35
|
+
}
|
|
36
|
+
|
|
9
37
|
if (!root.saasbackend.errorTracking) {
|
|
10
38
|
root.saasbackend.errorTracking = createErrorTrackingClient();
|
|
11
39
|
}
|
|
@@ -15,4 +43,5 @@ function attachToSaasbackendGlobal() {
|
|
|
15
43
|
}
|
|
16
44
|
}
|
|
17
45
|
|
|
46
|
+
attachToSuperbackendGlobal();
|
|
18
47
|
attachToSaasbackendGlobal();
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
function toStr(v) {
|
|
3
|
+
return v === undefined || v === null ? '' : String(v);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function normalizeApiUrl(apiUrl) {
|
|
7
|
+
const u = toStr(apiUrl).trim();
|
|
8
|
+
if (!u) return '';
|
|
9
|
+
return u.replace(/\/$/, '');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function buildHeaders(apiKey) {
|
|
13
|
+
const headers = {};
|
|
14
|
+
const key = toStr(apiKey).trim();
|
|
15
|
+
if (key) headers['x-project-key'] = key;
|
|
16
|
+
return headers;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function fetchJson(url, headers) {
|
|
20
|
+
const res = await fetch(url, { headers: headers || {} });
|
|
21
|
+
const text = await res.text();
|
|
22
|
+
let data = null;
|
|
23
|
+
try {
|
|
24
|
+
data = text ? JSON.parse(text) : null;
|
|
25
|
+
} catch {
|
|
26
|
+
data = null;
|
|
27
|
+
}
|
|
28
|
+
if (!res.ok) {
|
|
29
|
+
const msg = data && data.error ? data.error : 'Request failed';
|
|
30
|
+
const err = new Error(msg);
|
|
31
|
+
err.status = res.status;
|
|
32
|
+
throw err;
|
|
33
|
+
}
|
|
34
|
+
return data;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function ensureTemplate(code, html) {
|
|
38
|
+
const id = 'ui-cmp-' + code;
|
|
39
|
+
let tpl = document.getElementById(id);
|
|
40
|
+
if (!tpl) {
|
|
41
|
+
tpl = document.createElement('template');
|
|
42
|
+
tpl.id = id;
|
|
43
|
+
document.body.appendChild(tpl);
|
|
44
|
+
}
|
|
45
|
+
tpl.innerHTML = toStr(html);
|
|
46
|
+
return tpl;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function injectCssScoped(code, cssText) {
|
|
50
|
+
const id = 'ui-cmp-style-' + code;
|
|
51
|
+
let el = document.getElementById(id);
|
|
52
|
+
if (!el) {
|
|
53
|
+
el = document.createElement('style');
|
|
54
|
+
el.id = id;
|
|
55
|
+
document.head.appendChild(el);
|
|
56
|
+
}
|
|
57
|
+
el.textContent = toStr(cssText);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function createShadowRootContainer() {
|
|
61
|
+
const host = document.createElement('div');
|
|
62
|
+
host.style.all = 'initial';
|
|
63
|
+
const shadow = host.attachShadow({ mode: 'open' });
|
|
64
|
+
return { host, shadow };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function defaultMountTarget() {
|
|
68
|
+
return document.body;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function compileComponentJs(jsCode) {
|
|
72
|
+
const code = toStr(jsCode);
|
|
73
|
+
if (!code.trim()) {
|
|
74
|
+
return function () {
|
|
75
|
+
return {};
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return new Function('api', 'templateRootEl', 'props', code);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const state = {
|
|
82
|
+
initialized: false,
|
|
83
|
+
projectId: null,
|
|
84
|
+
apiKey: null,
|
|
85
|
+
apiUrl: '',
|
|
86
|
+
cssIsolation: 'scoped',
|
|
87
|
+
components: {},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
function registerComponent(def) {
|
|
91
|
+
const code = toStr(def.code).trim().toLowerCase();
|
|
92
|
+
if (!code) return;
|
|
93
|
+
|
|
94
|
+
const version = def.version;
|
|
95
|
+
const html = def.html;
|
|
96
|
+
const js = def.js;
|
|
97
|
+
const css = def.css;
|
|
98
|
+
|
|
99
|
+
ensureTemplate(code, html);
|
|
100
|
+
|
|
101
|
+
const component = {
|
|
102
|
+
code,
|
|
103
|
+
version,
|
|
104
|
+
css,
|
|
105
|
+
js,
|
|
106
|
+
create: function (props, options) {
|
|
107
|
+
const opts = options || {};
|
|
108
|
+
const mountEl = opts.mountEl || defaultMountTarget();
|
|
109
|
+
const isolation = opts.cssIsolation || state.cssIsolation;
|
|
110
|
+
|
|
111
|
+
const tpl = ensureTemplate(code, html);
|
|
112
|
+
const fragment = tpl.content.cloneNode(true);
|
|
113
|
+
|
|
114
|
+
let templateRootEl;
|
|
115
|
+
let instanceRoot;
|
|
116
|
+
let shadow = null;
|
|
117
|
+
|
|
118
|
+
if (isolation === 'shadow') {
|
|
119
|
+
const c = createShadowRootContainer();
|
|
120
|
+
instanceRoot = c.host;
|
|
121
|
+
shadow = c.shadow;
|
|
122
|
+
templateRootEl = shadow;
|
|
123
|
+
if (css) {
|
|
124
|
+
const style = document.createElement('style');
|
|
125
|
+
style.textContent = toStr(css);
|
|
126
|
+
shadow.appendChild(style);
|
|
127
|
+
}
|
|
128
|
+
shadow.appendChild(fragment);
|
|
129
|
+
} else {
|
|
130
|
+
instanceRoot = document.createElement('div');
|
|
131
|
+
templateRootEl = instanceRoot;
|
|
132
|
+
if (css) injectCssScoped(code, css);
|
|
133
|
+
instanceRoot.appendChild(fragment);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
mountEl.appendChild(instanceRoot);
|
|
137
|
+
|
|
138
|
+
const api = {
|
|
139
|
+
unmount: function () {
|
|
140
|
+
try {
|
|
141
|
+
instanceRoot.remove();
|
|
142
|
+
} catch {}
|
|
143
|
+
},
|
|
144
|
+
mountEl,
|
|
145
|
+
hostEl: instanceRoot,
|
|
146
|
+
shadowRoot: shadow,
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const fn = compileComponentJs(js);
|
|
150
|
+
let exported = {};
|
|
151
|
+
try {
|
|
152
|
+
exported = fn(api, templateRootEl, props || {}) || {};
|
|
153
|
+
} catch (e) {
|
|
154
|
+
exported = {
|
|
155
|
+
error: e,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return Object.assign({ api }, exported);
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
state.components[code] = component;
|
|
164
|
+
uiCmp[code] = component;
|
|
165
|
+
uiComponents[code] = component;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function init(options) {
|
|
169
|
+
const opts = options || {};
|
|
170
|
+
const projectId = toStr(opts.projectId).trim();
|
|
171
|
+
if (!projectId) throw new Error('projectId is required');
|
|
172
|
+
|
|
173
|
+
const apiUrl = normalizeApiUrl(opts.apiUrl);
|
|
174
|
+
const apiKey = opts.apiKey;
|
|
175
|
+
|
|
176
|
+
state.projectId = projectId;
|
|
177
|
+
state.apiKey = apiKey;
|
|
178
|
+
state.apiUrl = apiUrl;
|
|
179
|
+
|
|
180
|
+
const cssIsolation = toStr(opts.cssIsolation || 'scoped').trim().toLowerCase();
|
|
181
|
+
state.cssIsolation = cssIsolation === 'shadow' ? 'shadow' : 'scoped';
|
|
182
|
+
|
|
183
|
+
const base = state.apiUrl;
|
|
184
|
+
const url = base + '/api/ui-components/projects/' + encodeURIComponent(projectId) + '/manifest';
|
|
185
|
+
|
|
186
|
+
const data = await fetchJson(url, buildHeaders(apiKey));
|
|
187
|
+
const items = data && Array.isArray(data.components) ? data.components : [];
|
|
188
|
+
|
|
189
|
+
for (const def of items) {
|
|
190
|
+
registerComponent(def);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
state.initialized = true;
|
|
194
|
+
return { project: data ? data.project : null, count: items.length };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function load(code) {
|
|
198
|
+
const c = toStr(code).trim().toLowerCase();
|
|
199
|
+
if (!c) throw new Error('code is required');
|
|
200
|
+
if (!state.projectId) throw new Error('uiCmp not initialized');
|
|
201
|
+
if (state.components[c]) return state.components[c];
|
|
202
|
+
|
|
203
|
+
const base = state.apiUrl;
|
|
204
|
+
const url =
|
|
205
|
+
base +
|
|
206
|
+
'/api/ui-components/projects/' +
|
|
207
|
+
encodeURIComponent(state.projectId) +
|
|
208
|
+
'/components/' +
|
|
209
|
+
encodeURIComponent(c);
|
|
210
|
+
|
|
211
|
+
const data = await fetchJson(url, buildHeaders(state.apiKey));
|
|
212
|
+
if (!data || !data.component) throw new Error('Component not found');
|
|
213
|
+
|
|
214
|
+
registerComponent(data.component);
|
|
215
|
+
return state.components[c];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const uiCmp = {
|
|
219
|
+
init,
|
|
220
|
+
load,
|
|
221
|
+
_state: state,
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const uiComponents = uiCmp;
|
|
225
|
+
|
|
226
|
+
window.uiCmp = uiCmp;
|
|
227
|
+
window.uiComponents = uiComponents;
|
|
228
|
+
})();
|