@atelieruldetehnologii/node-red-custome-solutions 0.62.22
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/NOTICE +56 -0
- package/PROPRIETARY-LICENSE.txt +157 -0
- package/README.md +105 -0
- package/TERMS-AND-CONDITIONS.md +311 -0
- package/advantech-read/advantech-read.html +775 -0
- package/advantech-read/advantech-read.js +449 -0
- package/advantech-write/advantech-write.html +413 -0
- package/advantech-write/advantech-write.js +338 -0
- package/bess-control/bess-control.html +569 -0
- package/bess-control/bess-control.js +541 -0
- package/bess-control/lib/allocation.js +149 -0
- package/bess-control/lib/batteries.js +86 -0
- package/bess-control/lib/helpers.js +157 -0
- package/bess-control/lib/messages.js +131 -0
- package/bess-control/lib/runtime.js +156 -0
- package/carlo-gavazzi/carlo-gavazzi.html +4327 -0
- package/carlo-gavazzi/carlo-gavazzi.js +3081 -0
- package/control-export/control-export.html +3644 -0
- package/control-export/control-export.js +2192 -0
- package/control-reactive/control-reactive.html +3136 -0
- package/control-reactive/control-reactive.js +2124 -0
- package/fronius-emulator/README.md +258 -0
- package/fronius-emulator/fronius-emulator.html +170 -0
- package/fronius-emulator/fronius-emulator.js +520 -0
- package/fronius-emulator/lib/binary-codec.js +65 -0
- package/fronius-emulator/lib/fronius-register-map.js +185 -0
- package/fronius-emulator/lib/modbus-parser.js +61 -0
- package/fronius-emulator/lib/modbus-tcp-server.js +224 -0
- package/fronius-emulator/lib/validation.js +206 -0
- package/fronius-emulator/lib/virtual-meter.js +132 -0
- package/fronius-read/fronius-read.html +3944 -0
- package/fronius-read/fronius-read.js +1466 -0
- package/fronius-read/lib/inverter-profiles.js +711 -0
- package/fronius-read/lib/sunspec-core.js +303 -0
- package/fronius-read/lib/sunspec-model-parsers.js +845 -0
- package/fronius-write/fronius-write.html +1789 -0
- package/fronius-write/fronius-write.js +2531 -0
- package/influxdb/influxdb.html +1077 -0
- package/influxdb/influxdb.js +1487 -0
- package/janitza/janitza.html +2562 -0
- package/janitza/janitza.js +1500 -0
- package/janitza/lib/janitza-profiles.js +371 -0
- package/lib/a3-test-server.js +199 -0
- package/lib/adam-6000-profiles.js +173 -0
- package/lib/licensing/constants.js +103 -0
- package/lib/licensing/device-id.js +251 -0
- package/lib/licensing/errors.js +21 -0
- package/lib/licensing/license-client.js +377 -0
- package/lib/licensing/license-state.js +50 -0
- package/lib/licensing/node-guard.js +150 -0
- package/lib/licensing/package-integrity.js +167 -0
- package/lib/licensing/package-integrity.json +513 -0
- package/lib/licensing/public-key.pem +3 -0
- package/lib/licensing/public-key.pem.example +2 -0
- package/lib/licensing/public-keys.js +18 -0
- package/lib/licensing/storage.js +63 -0
- package/lib/licensing/token-verifier.js +84 -0
- package/lib/numbers.js +26 -0
- package/license/atelier-license.html +88 -0
- package/license/atelier-license.js +170 -0
- package/modbus-tcp-discover/modbus-tcp-discover.html +190 -0
- package/modbus-tcp-discover/modbus-tcp-discover.js +706 -0
- package/multi-battery/multi-battery.html +649 -0
- package/multi-battery/multi-battery.js +668 -0
- package/multi-grid/multi-grid.html +745 -0
- package/multi-grid/multi-grid.js +1392 -0
- package/multi-inverter/lib/engine-core.js +237 -0
- package/multi-inverter/multi-inverter.html +2674 -0
- package/multi-inverter/multi-inverter.js +1393 -0
- package/package.json +109 -0
- package/phoenix-contact/lib/eem-ma370-advanced.js +175 -0
- package/phoenix-contact/lib/eem-ma370.js +138 -0
- package/phoenix-contact/lib/integrations.js +149 -0
- package/phoenix-contact/lib/phoenix-contact-profile.js +35030 -0
- package/phoenix-contact/phoenix-contact.html +840 -0
- package/phoenix-contact/phoenix-contact.js +679 -0
- package/pv-generator/pv-generator.html +284 -0
- package/pv-generator/pv-generator.js +387 -0
- package/pylontech-read/pylontech-read.html +763 -0
- package/pylontech-read/pylontech-read.js +518 -0
- package/pylontech-write/pylontech-write.html +405 -0
- package/pylontech-write/pylontech-write.js +546 -0
- package/scada-aggregator/lib/aggregate.js +72 -0
- package/scada-aggregator/lib/groups.js +39 -0
- package/scada-aggregator/scada-aggregator.html +108 -0
- package/scada-aggregator/scada-aggregator.js +117 -0
- package/scada-server/lib/aggregator-map.js +58 -0
- package/scada-server/lib/encoding.js +235 -0
- package/scada-server/lib/modbus-server.js +492 -0
- package/scada-server/lib/object-path.js +18 -0
- package/scada-server/lib/register-map.js +288 -0
- package/scada-server/scada-server.html +1419 -0
- package/scada-server/scada-server.js +704 -0
- package/scripts/check-syntax.js +66 -0
- package/scripts/package-integrity.js +46 -0
- package/scripts/package-release.js +116 -0
- package/scripts/run-tests.js +2178 -0
- package/scripts/tgz-integrity.js +74 -0
- package/sma-read/lib/sunspec-core.js +83 -0
- package/sma-read/lib/sunspec-model-parsers.js +853 -0
- package/sma-read/sma-read.html +659 -0
- package/sma-read/sma-read.js +458 -0
- package/sma-write/sma-write.html +105 -0
- package/sma-write/sma-write.js +270 -0
- package/victron-read/lib/mqtt-state.js +176 -0
- package/victron-read/victron-read.html +1074 -0
- package/victron-read/victron-read.js +1222 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
|
+
function loadPublicKeys() {
|
|
7
|
+
const keys = {};
|
|
8
|
+
const pemPath = path.join(__dirname, "public-key.pem");
|
|
9
|
+
try {
|
|
10
|
+
const pem = fs.readFileSync(pemPath, "utf8").trim();
|
|
11
|
+
if (pem.includes("BEGIN PUBLIC KEY")) keys.v1 = pem;
|
|
12
|
+
} catch (error) {
|
|
13
|
+
// A release without a vendor key fails closed in token-verifier.
|
|
14
|
+
}
|
|
15
|
+
return Object.freeze(keys);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = { PUBLIC_KEYS: loadPublicKeys(), loadPublicKeys };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const crypto = require("crypto");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
|
|
7
|
+
async function ensurePrivateDirectory(directory) {
|
|
8
|
+
await fs.promises.mkdir(directory, { recursive: true, mode: 0o700 });
|
|
9
|
+
try { await fs.promises.chmod(directory, 0o700); } catch (error) { /* Windows/limited filesystems */ }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function tokenFileName(configId, product) {
|
|
13
|
+
const digest = crypto.createHash("sha256").update(String(configId) + "\0" + String(product)).digest("hex");
|
|
14
|
+
return "license-" + digest.slice(0, 32) + ".json";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function licenseKeyFingerprint(value) {
|
|
18
|
+
const key = String(value || "").trim();
|
|
19
|
+
return key ? crypto.createHash("sha256").update(key, "utf8").digest("hex") : null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
class LicenseStorage {
|
|
23
|
+
constructor(userDir, configId, product) {
|
|
24
|
+
this.directory = path.join(userDir, "atelier-license");
|
|
25
|
+
this.filePath = path.join(this.directory, tokenFileName(configId, product));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async load() {
|
|
29
|
+
try {
|
|
30
|
+
const raw = await fs.promises.readFile(this.filePath, "utf8");
|
|
31
|
+
const stored = JSON.parse(raw);
|
|
32
|
+
return stored && (typeof stored.token === "string" || typeof stored.serverState === "string") ? stored : null;
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (error.code === "ENOENT") return null;
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async save(token, deviceId, product, keyFingerprint) {
|
|
40
|
+
return this.saveState(null, token, deviceId, product, keyFingerprint);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async saveState(serverState, token, deviceId, product, keyFingerprint) {
|
|
44
|
+
let fingerprint = keyFingerprint;
|
|
45
|
+
if (fingerprint === undefined) {
|
|
46
|
+
const current = await this.load();
|
|
47
|
+
fingerprint = current && current.licenseKeyFingerprint || null;
|
|
48
|
+
}
|
|
49
|
+
await ensurePrivateDirectory(this.directory);
|
|
50
|
+
const temporary = this.filePath + ".tmp-" + process.pid;
|
|
51
|
+
const body = JSON.stringify({ token: token || null, serverState: serverState || null, deviceId, product, licenseKeyFingerprint: fingerprint || null, savedAt: new Date().toISOString() }, null, 2) + "\n";
|
|
52
|
+
await fs.promises.writeFile(temporary, body, { encoding: "utf8", mode: 0o600 });
|
|
53
|
+
try { await fs.promises.chmod(temporary, 0o600); } catch (error) { /* Windows/limited filesystems */ }
|
|
54
|
+
await fs.promises.rename(temporary, this.filePath);
|
|
55
|
+
try { await fs.promises.chmod(this.filePath, 0o600); } catch (error) { /* Windows/limited filesystems */ }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async clear() {
|
|
59
|
+
try { await fs.promises.unlink(this.filePath); } catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = { LicenseStorage, ensurePrivateDirectory, licenseKeyFingerprint, tokenFileName };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const crypto = require("crypto");
|
|
4
|
+
const { LICENSE_TYPES } = require("./constants");
|
|
5
|
+
const { LicenseError } = require("./errors");
|
|
6
|
+
|
|
7
|
+
function decodeJsonPart(value, label) {
|
|
8
|
+
try {
|
|
9
|
+
return JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
|
|
10
|
+
} catch (error) {
|
|
11
|
+
throw new LicenseError("INVALID_TOKEN", "Invalid " + label + " in license token");
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function requireText(payload, field) {
|
|
16
|
+
if (typeof payload[field] !== "string" || !payload[field].trim()) {
|
|
17
|
+
throw new LicenseError("INVALID_TOKEN", "License token is missing " + field);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function requireTime(payload, field, nullable) {
|
|
22
|
+
const value = payload[field];
|
|
23
|
+
if (nullable && value === null) return;
|
|
24
|
+
if (typeof value !== "string" || !Number.isFinite(Date.parse(value))) {
|
|
25
|
+
throw new LicenseError("INVALID_TOKEN", "License token has invalid " + field);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function validateClaims(payload) {
|
|
30
|
+
if (!Number.isInteger(payload.tokenVersion) || payload.tokenVersion < 1) {
|
|
31
|
+
throw new LicenseError("INVALID_TOKEN", "Unsupported license token version");
|
|
32
|
+
}
|
|
33
|
+
["deviceId", "companyId", "licenseId", "product", "licenseType", "status"].forEach(function (field) {
|
|
34
|
+
requireText(payload, field);
|
|
35
|
+
});
|
|
36
|
+
["issuedAt", "refreshAfter"].forEach(function (field) { requireTime(payload, field, false); });
|
|
37
|
+
const licenseType = payload.licenseType.toUpperCase();
|
|
38
|
+
if (!Object.prototype.hasOwnProperty.call(LICENSE_TYPES, licenseType)) {
|
|
39
|
+
throw new LicenseError("INVALID_TOKEN", "Unsupported license type");
|
|
40
|
+
}
|
|
41
|
+
requireTime(payload, "expiresAt", licenseType === LICENSE_TYPES.PERMANENT);
|
|
42
|
+
requireTime(payload, "offlineGraceUntil", licenseType === LICENSE_TYPES.PERMANENT);
|
|
43
|
+
if (payload.features === undefined) payload.features = {};
|
|
44
|
+
if (!payload.features || typeof payload.features !== "object" || Array.isArray(payload.features)) {
|
|
45
|
+
throw new LicenseError("INVALID_TOKEN", "License token has invalid features");
|
|
46
|
+
}
|
|
47
|
+
return payload;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function verifyLicenseToken(token, options) {
|
|
51
|
+
const settings = options || {};
|
|
52
|
+
if (typeof token !== "string" || token.length > 65536) {
|
|
53
|
+
throw new LicenseError("INVALID_TOKEN", "Invalid license token format");
|
|
54
|
+
}
|
|
55
|
+
const parts = token.split(".");
|
|
56
|
+
if (parts.length !== 3 || parts.some(function (part) { return !part; })) {
|
|
57
|
+
throw new LicenseError("INVALID_TOKEN", "License token must be compact JWS");
|
|
58
|
+
}
|
|
59
|
+
const header = decodeJsonPart(parts[0], "header");
|
|
60
|
+
const payload = decodeJsonPart(parts[1], "payload");
|
|
61
|
+
if (header.alg !== "EdDSA") throw new LicenseError("INVALID_TOKEN", "License token algorithm must be EdDSA");
|
|
62
|
+
const keyId = String(header.kid || payload.keyId || "");
|
|
63
|
+
if (!keyId) throw new LicenseError("INVALID_TOKEN", "License token is missing keyId");
|
|
64
|
+
const publicKeys = settings.publicKeys || {};
|
|
65
|
+
const publicKey = publicKeys[keyId];
|
|
66
|
+
if (!publicKey) throw new LicenseError("INVALID_TOKEN", "Unknown license signing key: " + keyId);
|
|
67
|
+
let valid = false;
|
|
68
|
+
try {
|
|
69
|
+
valid = crypto.verify(null, Buffer.from(parts[0] + "." + parts[1], "ascii"), publicKey, Buffer.from(parts[2], "base64url"));
|
|
70
|
+
} catch (error) {
|
|
71
|
+
throw new LicenseError("INVALID_TOKEN", "Invalid license signing key");
|
|
72
|
+
}
|
|
73
|
+
if (!valid) throw new LicenseError("INVALID_TOKEN", "Invalid license token signature");
|
|
74
|
+
validateClaims(payload);
|
|
75
|
+
if (settings.deviceId && payload.deviceId !== settings.deviceId) {
|
|
76
|
+
throw new LicenseError("INVALID_DEVICE", "License token belongs to another device");
|
|
77
|
+
}
|
|
78
|
+
if (settings.product && payload.product !== settings.product) {
|
|
79
|
+
throw new LicenseError("PRODUCT_NOT_LICENSED", "License token belongs to another product");
|
|
80
|
+
}
|
|
81
|
+
return { header, payload, keyId };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = { validateClaims, verifyLicenseToken };
|
package/lib/numbers.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
function toFiniteNumber(value, fallback) {
|
|
4
|
+
const numeric = Number(value);
|
|
5
|
+
return Number.isFinite(numeric) ? numeric : fallback;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function atLeast(value, min, fallback) {
|
|
9
|
+
return Math.max(min, toFiniteNumber(value, fallback));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function clampNumber(value, min, max, fallback) {
|
|
13
|
+
const numeric = toFiniteNumber(value, fallback);
|
|
14
|
+
return Math.min(Math.max(numeric, min), max);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function roundedAtLeast(value, min, fallback) {
|
|
18
|
+
return Math.max(min, Math.round(toFiniteNumber(value, fallback)));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = {
|
|
22
|
+
toFiniteNumber,
|
|
23
|
+
atLeast,
|
|
24
|
+
clampNumber,
|
|
25
|
+
roundedAtLeast
|
|
26
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
(function () {
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
// Multiple individually installed Atelier packages ship the same config node.
|
|
6
|
+
// Reuse the first editor definition instead of registering the type twice.
|
|
7
|
+
const marker = "__atelierLicenseEditorRegistered";
|
|
8
|
+
const alreadyRegistered = window[marker] || (RED.nodes && typeof RED.nodes.getType === "function" && RED.nodes.getType("atelier-license"));
|
|
9
|
+
window[marker] = true;
|
|
10
|
+
if (alreadyRegistered) return;
|
|
11
|
+
|
|
12
|
+
const LABELS = {
|
|
13
|
+
UNKNOWN: "Checking", NOT_ACTIVATED: "Not Activated", ACTIVATING: "Activating", ACTIVE: "Active",
|
|
14
|
+
OFFLINE: "Licensed Offline", GRACE_PERIOD: "Grace Period", PENDING_APPROVAL: "Pending Approval",
|
|
15
|
+
DEVICE_LIMIT_REACHED: "Device Limit Reached", EXPIRED: "Expired", REVOKED: "Revoked",
|
|
16
|
+
SUSPENDED: "Suspended", DEVICE_BLOCKED: "Device Blocked", DEVICE_REJECTED: "Device Rejected",
|
|
17
|
+
DEVICE_DEACTIVATED: "Device Deactivated", DEVICE_REMOVED: "Device Removed", INVALID_TOKEN: "Invalid Token", INVALID_DEVICE: "Invalid Device",
|
|
18
|
+
PRODUCT_NOT_LICENSED: "Product Not Licensed", SERVER_ERROR: "Server Error"
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function text(value) { return value === null || value === undefined || value === "" ? "—" : String(value); }
|
|
22
|
+
function render(status) {
|
|
23
|
+
const enabledFeatures = Object.keys(status.features || {}).filter(function (key) { return status.features[key]; });
|
|
24
|
+
const genericFeatures = enabledFeatures.filter(function (key) { return !key.startsWith("node:"); });
|
|
25
|
+
const selectedNodes = enabledFeatures.filter(function (key) { return key.startsWith("node:") && key !== "node:*"; }).map(function (key) { return key.slice(5); });
|
|
26
|
+
const displayedFeatures = genericFeatures.concat(status.allNodes ? ["Toate nodurile"] : selectedNodes);
|
|
27
|
+
const fields = {
|
|
28
|
+
status: LABELS[status.state] || status.state, company: status.company, device: status.deviceId,
|
|
29
|
+
source: status.deviceSource, type: status.licenseType, plan: status.plan, valid: status.expiresAt,
|
|
30
|
+
offline: status.offlineGraceUntil, last: status.lastCheck, next: status.nextCheck,
|
|
31
|
+
product: status.productName || status.product, features: displayedFeatures.join(", ")
|
|
32
|
+
};
|
|
33
|
+
Object.keys(fields).forEach(function (key) { $("#atelier-license-" + key).text(text(fields[key])); });
|
|
34
|
+
let message = status.message || "";
|
|
35
|
+
if (status.state === "PENDING_APPROVAL") message = "This device is waiting for approval from Atelierul de Tehnologii.";
|
|
36
|
+
if (status.state === "DEVICE_LIMIT_REACHED") message = "The maximum number of licensed devices for this company has been reached.";
|
|
37
|
+
if (status.state === "DEVICE_REMOVED") message = "This device was removed. Deploy the license configuration again to request a new activation.";
|
|
38
|
+
$("#atelier-license-message").text(message);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function request(node, operation) {
|
|
42
|
+
if (!node.id || node.id.startsWith("_") || !RED.nodes.node(node.id)) {
|
|
43
|
+
RED.notify("Deploy the license configuration before using this action.", "warning");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
$(".atelier-license-action").prop("disabled", true);
|
|
47
|
+
$.ajax({ url: "atelier-license/" + encodeURIComponent(node.id) + "/" + operation, method: operation === "status" ? "GET" : "POST", contentType: "application/json" })
|
|
48
|
+
.done(function (response) { render(response.status || {}); if (operation !== "status") RED.notify("License operation completed.", "success"); })
|
|
49
|
+
.fail(function (xhr) { const result = xhr.responseJSON || {}; RED.notify(result.message || "License operation failed.", "error"); })
|
|
50
|
+
.always(function () { $(".atelier-license-action").prop("disabled", false); });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
RED.nodes.registerType("atelier-license", {
|
|
54
|
+
category: "config",
|
|
55
|
+
defaults: {
|
|
56
|
+
name: { value: "Atelierul de Tehnologii License" },
|
|
57
|
+
product: { value: "node-red-custome-solutions", required: true }
|
|
58
|
+
},
|
|
59
|
+
credentials: { licenseKey: { type: "password" } },
|
|
60
|
+
label: function () { return this.name || "Atelierul de Tehnologii License"; },
|
|
61
|
+
oneditprepare: function () {
|
|
62
|
+
const node = this;
|
|
63
|
+
$("#node-config-input-product").val("node-red-custome-solutions");
|
|
64
|
+
$("#atelier-license-refresh").on("click", function () { request(node, "refresh"); });
|
|
65
|
+
request(node, "status");
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}());
|
|
69
|
+
</script>
|
|
70
|
+
|
|
71
|
+
<script type="text/html" data-template-name="atelier-license">
|
|
72
|
+
<div class="form-row"><label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label><input type="text" id="node-config-input-name"></div>
|
|
73
|
+
<div class="form-row"><label for="node-config-input-licenseKey"><i class="fa fa-key"></i> License Key</label><input type="password" id="node-config-input-licenseKey" autocomplete="new-password"></div>
|
|
74
|
+
<input type="hidden" id="node-config-input-product">
|
|
75
|
+
<div class="form-row"><button type="button" id="atelier-license-refresh" class="red-ui-button atelier-license-action"><i class="fa fa-refresh"></i> Refresh License</button></div>
|
|
76
|
+
<div class="form-tips">License activation is automatic after Deploy. To change the license, enter the new key and Deploy again.</div>
|
|
77
|
+
<div class="form-row" style="border:1px solid #ddd;border-radius:4px;padding:10px;line-height:1.7">
|
|
78
|
+
<strong>Status: <span id="atelier-license-status">Checking</span></strong><br>
|
|
79
|
+
Company: <span id="atelier-license-company">—</span><br>Device ID: <span id="atelier-license-device">—</span><br>Device source: <span id="atelier-license-source">—</span><br>
|
|
80
|
+
License Type: <span id="atelier-license-type">—</span><br>Plan: <span id="atelier-license-plan">—</span><br>Valid Until: <span id="atelier-license-valid">—</span><br>
|
|
81
|
+
Offline Until: <span id="atelier-license-offline">—</span><br>Last Check: <span id="atelier-license-last">—</span><br>Next Check: <span id="atelier-license-next">—</span><br>
|
|
82
|
+
Product: <span id="atelier-license-product">—</span><br>Features: <span id="atelier-license-features">—</span><br><em id="atelier-license-message"></em>
|
|
83
|
+
</div>
|
|
84
|
+
</script>
|
|
85
|
+
|
|
86
|
+
<script type="text/html" data-help-name="atelier-license">
|
|
87
|
+
<p>Central commercial license configuration for Atelierul de Tehnologii nodes. The License Key is stored as a Node-RED credential; signed tokens are stored under the Node-RED user directory.</p>
|
|
88
|
+
</script>
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { LicenseClient } = require("../lib/licensing/license-client");
|
|
6
|
+
const { PRODUCT_CODE, PRODUCT_NAME, STATUS_PRESENTATION } = require("../lib/licensing/constants");
|
|
7
|
+
const { getDeviceIdentity } = require("../lib/licensing/device-id");
|
|
8
|
+
const { PUBLIC_KEYS } = require("../lib/licensing/public-keys");
|
|
9
|
+
const { LicenseStorage, licenseKeyFingerprint } = require("../lib/licensing/storage");
|
|
10
|
+
const { getPackageIntegrity } = require("../lib/licensing/package-integrity");
|
|
11
|
+
|
|
12
|
+
module.exports = function (RED) {
|
|
13
|
+
// Every selectively delivered package contains this shared config node. Node-RED
|
|
14
|
+
// loads installed packages sequentially, so only the first copy must register it.
|
|
15
|
+
// The remaining packages reuse the existing type and its runtime/admin routes.
|
|
16
|
+
if (RED.nodes && typeof RED.nodes.getType === "function" && RED.nodes.getType("atelier-license")) return;
|
|
17
|
+
|
|
18
|
+
const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8").replace(/^\uFEFF/, ""));
|
|
19
|
+
const commercialNodeTypes = new Set(Object.keys(packageJson["node-red"].nodes || {}).filter(function (type) { return type !== "atelier-license"; }));
|
|
20
|
+
const packageContainsAllNodes = !packageJson.atelierDelivery || packageJson.atelierDelivery.allNodes === true;
|
|
21
|
+
const runtimeNodes = new Map();
|
|
22
|
+
const integrity = getPackageIntegrity();
|
|
23
|
+
|
|
24
|
+
function publicError(error) {
|
|
25
|
+
return { ok: false, code: error.code || "SERVER_ERROR", message: error.message || "License operation failed" };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function AtelierLicenseNode(config) {
|
|
29
|
+
RED.nodes.createNode(this, config);
|
|
30
|
+
const node = this;
|
|
31
|
+
node.name = String(config.name || "Atelierul de Tehnologii License").trim();
|
|
32
|
+
node.product = PRODUCT_CODE;
|
|
33
|
+
node.client = null;
|
|
34
|
+
node.initialization = null;
|
|
35
|
+
node.approvalRetry = null;
|
|
36
|
+
|
|
37
|
+
function clientMethod(name, fallback) {
|
|
38
|
+
return function () {
|
|
39
|
+
if (!node.client || typeof node.client[name] !== "function") return fallback;
|
|
40
|
+
return node.client[name].apply(node.client, arguments);
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const clientIsLicensed = clientMethod("isLicensed", false);
|
|
45
|
+
const clientGetState = clientMethod("getState", "UNKNOWN");
|
|
46
|
+
node.isLicensed = function () { return integrity.isValid() && clientIsLicensed(); };
|
|
47
|
+
node.getState = function () { return integrity.isValid() ? clientGetState() : "PACKAGE_TAMPERED"; };
|
|
48
|
+
node.getCompany = clientMethod("getCompany", null);
|
|
49
|
+
node.getPlan = clientMethod("getPlan", null);
|
|
50
|
+
node.getFeatures = clientMethod("getFeatures", {});
|
|
51
|
+
node.hasFeature = clientMethod("hasFeature", false);
|
|
52
|
+
node.getDeviceId = clientMethod("getDeviceId", null);
|
|
53
|
+
node.getLicenseType = clientMethod("getLicenseType", null);
|
|
54
|
+
node.getExpiresAt = clientMethod("getExpiresAt", null);
|
|
55
|
+
node.getOfflineGraceUntil = clientMethod("getOfflineGraceUntil", null);
|
|
56
|
+
node.getLastCheck = clientMethod("getLastCheck", null);
|
|
57
|
+
node.getNextCheck = clientMethod("getNextCheck", null);
|
|
58
|
+
node.getLicenseStatus = clientMethod("getStatus", { state: "UNKNOWN" });
|
|
59
|
+
node.getDisplayLicenseStatus = function () {
|
|
60
|
+
const status = node.getLicenseStatus();
|
|
61
|
+
const features = status && status.features || {};
|
|
62
|
+
const everyInstalledNodeLicensed = commercialNodeTypes.size > 0 && [...commercialNodeTypes].every(function (type) { return features["node:" + type] === true; });
|
|
63
|
+
return Object.assign({}, status, { state: integrity.isValid() ? status.state : "PACKAGE_TAMPERED", integrity: integrity.getStatus(), allNodes: features["node:*"] === true || packageContainsAllNodes && everyInstalledNodeLicensed });
|
|
64
|
+
};
|
|
65
|
+
node.refresh = function () { return node.initialization.then(function () { return node.client.refresh(); }).then(function () { return node.getDisplayLicenseStatus(); }); };
|
|
66
|
+
|
|
67
|
+
function updateStatus(status) {
|
|
68
|
+
const effective = integrity.isValid() ? status : Object.assign({}, status, { state: "PACKAGE_TAMPERED", integrity: integrity.getStatus() });
|
|
69
|
+
const presentation = STATUS_PRESENTATION[effective.state] || STATUS_PRESENTATION.UNKNOWN;
|
|
70
|
+
node.status(presentation);
|
|
71
|
+
node.emit("state-changed", effective);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function integrityChanged() { updateStatus(node.getLicenseStatus()); }
|
|
75
|
+
integrity.on("state-changed", integrityChanged);
|
|
76
|
+
|
|
77
|
+
const storage = new LicenseStorage(RED.settings.userDir, node.id, node.product);
|
|
78
|
+
node.initialization = storage.load().catch(function () { return null; }).then(async function (stored) {
|
|
79
|
+
const identity = await getDeviceIdentity(RED.settings.userDir, { preferredDeviceId: stored && stored.deviceId });
|
|
80
|
+
node.client = new LicenseClient({
|
|
81
|
+
identity,
|
|
82
|
+
nodeRedVersion: (typeof RED.version === "function" ? RED.version() : RED.version) || (RED.settings && RED.settings.version) || "unknown",
|
|
83
|
+
product: node.product,
|
|
84
|
+
productName: PRODUCT_NAME,
|
|
85
|
+
publicKeys: PUBLIC_KEYS,
|
|
86
|
+
nodeUsageProvider: function () {
|
|
87
|
+
let count = 0;
|
|
88
|
+
if (RED.nodes && typeof RED.nodes.eachNode === "function") {
|
|
89
|
+
RED.nodes.eachNode(function (flowNode) {
|
|
90
|
+
if (flowNode && flowNode.license === node.id && commercialNodeTypes.has(flowNode.type)) count += 1;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return count;
|
|
94
|
+
},
|
|
95
|
+
serverUrl: process.env.ATELIER_LICENSE_SERVER_URL,
|
|
96
|
+
storage,
|
|
97
|
+
version: packageJson.version
|
|
98
|
+
});
|
|
99
|
+
node.client.on("state-changed", updateStatus);
|
|
100
|
+
node.client.on("background-error", function (event) {
|
|
101
|
+
node.warn("License " + event.operation + " failed [" + event.code + "]: " + event.message + " at " + event.at);
|
|
102
|
+
});
|
|
103
|
+
let status = await node.client.initialize();
|
|
104
|
+
const configuredKey = String(node.credentials && node.credentials.licenseKey || "").trim();
|
|
105
|
+
const configuredFingerprint = licenseKeyFingerprint(configuredKey);
|
|
106
|
+
const keyChanged = Boolean(configuredFingerprint && (!stored || stored.licenseKeyFingerprint !== configuredFingerprint));
|
|
107
|
+
const needsActivation = ["NOT_ACTIVATED", "INVALID_TOKEN", "INVALID_DEVICE", "PRODUCT_NOT_LICENSED", "PENDING_APPROVAL", "DEVICE_LIMIT_REACHED", "DEVICE_REMOVED"].includes(status.state);
|
|
108
|
+
if (configuredKey && (keyChanged || needsActivation)) {
|
|
109
|
+
try {
|
|
110
|
+
status = await node.client.activate(configuredKey);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
status = node.client.getStatus();
|
|
113
|
+
node.warn("Automatic license activation failed: " + error.message);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (configuredKey) {
|
|
117
|
+
node.approvalRetry = setInterval(function () {
|
|
118
|
+
if (!node.client || !["PENDING_APPROVAL", "DEVICE_LIMIT_REACHED"].includes(node.client.getState())) return;
|
|
119
|
+
node.client.runBackground("approval-retry", node.client.activate(configuredKey));
|
|
120
|
+
}, 5000);
|
|
121
|
+
if (typeof node.approvalRetry.unref === "function") node.approvalRetry.unref();
|
|
122
|
+
}
|
|
123
|
+
updateStatus(status);
|
|
124
|
+
return status;
|
|
125
|
+
}).catch(function (error) {
|
|
126
|
+
node.status(STATUS_PRESENTATION.SERVER_ERROR);
|
|
127
|
+
node.error("License initialization failed: " + error.message);
|
|
128
|
+
throw error;
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
runtimeNodes.set(node.id, node);
|
|
132
|
+
node.on("close", function (removed, done) {
|
|
133
|
+
if (typeof removed === "function" && done === undefined) done = removed;
|
|
134
|
+
runtimeNodes.delete(node.id);
|
|
135
|
+
integrity.removeListener("state-changed", integrityChanged);
|
|
136
|
+
if (node.approvalRetry) clearInterval(node.approvalRetry);
|
|
137
|
+
node.approvalRetry = null;
|
|
138
|
+
if (node.client) node.client.close();
|
|
139
|
+
if (typeof done === "function") done();
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
RED.nodes.registerType("atelier-license", AtelierLicenseNode, {
|
|
144
|
+
credentials: { licenseKey: { type: "password" } }
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
function permission(name) {
|
|
148
|
+
return RED.auth && typeof RED.auth.needsPermission === "function" ? RED.auth.needsPermission(name) : function (req, res, next) { next(); };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function withNode(req, res, action) {
|
|
152
|
+
const node = runtimeNodes.get(req.params.id);
|
|
153
|
+
if (!node) { res.status(404).json({ ok: false, code: "NOT_FOUND", message: "License configuration is not deployed." }); return; }
|
|
154
|
+
Promise.resolve().then(() => action(node)).then(function (status) {
|
|
155
|
+
res.json({ ok: true, status });
|
|
156
|
+
}).catch(function (error) {
|
|
157
|
+
const response = publicError(error);
|
|
158
|
+
res.status(error.isNetworkError ? 503 : 400).json(response);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (RED.httpAdmin) {
|
|
163
|
+
RED.httpAdmin.get("/atelier-license/:id/status", permission("atelier-license.read"), function (req, res) {
|
|
164
|
+
withNode(req, res, async function (node) { await node.initialization; return node.getDisplayLicenseStatus(); });
|
|
165
|
+
});
|
|
166
|
+
RED.httpAdmin.post("/atelier-license/:id/refresh", permission("atelier-license.write"), function (req, res) {
|
|
167
|
+
withNode(req, res, function (node) { return node.refresh(); });
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
};
|