@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,377 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const EventEmitter = require("events");
|
|
4
|
+
const http = require("http");
|
|
5
|
+
const https = require("https");
|
|
6
|
+
const os = require("os");
|
|
7
|
+
const {
|
|
8
|
+
CONTROL_STATUS_INTERVAL_MS,
|
|
9
|
+
DEFAULT_SERVER_URL,
|
|
10
|
+
LICENSE_STATES,
|
|
11
|
+
PRODUCT_CODE,
|
|
12
|
+
REQUEST_TIMEOUT_MS,
|
|
13
|
+
RETRY_DELAYS_MS,
|
|
14
|
+
SCHEDULER_INTERVAL_MS,
|
|
15
|
+
SERVER_STATE_MAP
|
|
16
|
+
} = require("./constants");
|
|
17
|
+
const { LicenseError, LicenseNetworkError } = require("./errors");
|
|
18
|
+
const { evaluateLicenseState, isLicensedState } = require("./license-state");
|
|
19
|
+
const { PUBLIC_KEYS } = require("./public-keys");
|
|
20
|
+
const { verifyLicenseToken } = require("./token-verifier");
|
|
21
|
+
const { licenseKeyFingerprint } = require("./storage");
|
|
22
|
+
|
|
23
|
+
const NETWORK_CODES = new Set(["ETIMEDOUT", "ECONNREFUSED", "ENOTFOUND", "EAI_AGAIN", "ECONNRESET", "EPIPE", "ERR_TLS_HANDSHAKE_TIMEOUT"]);
|
|
24
|
+
const AUTHORITATIVE_DENIAL_STATES = new Set([
|
|
25
|
+
LICENSE_STATES.REVOKED,
|
|
26
|
+
LICENSE_STATES.SUSPENDED,
|
|
27
|
+
LICENSE_STATES.DEVICE_BLOCKED,
|
|
28
|
+
LICENSE_STATES.DEVICE_REJECTED,
|
|
29
|
+
LICENSE_STATES.DEVICE_DEACTIVATED,
|
|
30
|
+
LICENSE_STATES.DEVICE_REMOVED
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
function sanitizeServerUrl(value) {
|
|
34
|
+
const candidate = String(value || DEFAULT_SERVER_URL).trim().replace(/\/+$/, "");
|
|
35
|
+
const parsed = new URL(candidate);
|
|
36
|
+
if (!/^https?:$/.test(parsed.protocol)) throw new LicenseError("SERVER_ERROR", "License server URL must use HTTP or HTTPS");
|
|
37
|
+
return parsed.toString().replace(/\/$/, "");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function requestJson(baseUrl, endpoint, body, timeoutMs) {
|
|
41
|
+
return new Promise(function (resolve, reject) {
|
|
42
|
+
const target = new URL(endpoint, baseUrl + "/");
|
|
43
|
+
const payload = Buffer.from(JSON.stringify(body || {}), "utf8");
|
|
44
|
+
const transport = target.protocol === "https:" ? https : http;
|
|
45
|
+
const request = transport.request(target, {
|
|
46
|
+
method: "POST",
|
|
47
|
+
headers: { "content-type": "application/json", "content-length": payload.length, accept: "application/json" },
|
|
48
|
+
timeout: timeoutMs || REQUEST_TIMEOUT_MS
|
|
49
|
+
}, function (response) {
|
|
50
|
+
const chunks = [];
|
|
51
|
+
let size = 0;
|
|
52
|
+
response.on("data", function (chunk) {
|
|
53
|
+
size += chunk.length;
|
|
54
|
+
if (size > 1024 * 1024) {
|
|
55
|
+
request.destroy(new Error("License response too large"));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
chunks.push(chunk);
|
|
59
|
+
});
|
|
60
|
+
response.on("end", function () {
|
|
61
|
+
const statusCode = Number(response.statusCode || 0);
|
|
62
|
+
const serverUnavailable = statusCode >= 500 && statusCode <= 599;
|
|
63
|
+
let data = {};
|
|
64
|
+
try { data = chunks.length ? JSON.parse(Buffer.concat(chunks).toString("utf8")) : {}; } catch (error) {
|
|
65
|
+
if (serverUnavailable) {
|
|
66
|
+
reject(new LicenseNetworkError(`License server unavailable (HTTP ${statusCode})`, error));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
reject(new LicenseError("SERVER_ERROR", "License server returned invalid JSON"));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const errorData = data && typeof data.error === "object" ? data.error : data;
|
|
73
|
+
if (serverUnavailable) {
|
|
74
|
+
reject(new LicenseNetworkError(errorData.message || `License server unavailable (HTTP ${statusCode})`));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (statusCode < 200 || statusCode >= 300 || data.success === false) {
|
|
78
|
+
const code = String(errorData.code || "SERVER_ERROR").toUpperCase();
|
|
79
|
+
reject(new LicenseError(code, errorData.message || "License server rejected the request", { statusCode }));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
resolve(data && data.success === true && data.data && typeof data.data === "object" ? data.data : data);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
request.on("timeout", function () { request.destroy(Object.assign(new Error("License request timed out"), { code: "ETIMEDOUT" })); });
|
|
86
|
+
request.on("error", function (error) {
|
|
87
|
+
if (NETWORK_CODES.has(error.code) || /network|socket|connect|timed out/i.test(error.message || "")) {
|
|
88
|
+
reject(new LicenseNetworkError(error.message, error));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
reject(new LicenseError("SERVER_ERROR", error.message));
|
|
92
|
+
});
|
|
93
|
+
request.end(payload);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
class LicenseClient extends EventEmitter {
|
|
98
|
+
constructor(options) {
|
|
99
|
+
super();
|
|
100
|
+
const settings = options || {};
|
|
101
|
+
this.storage = settings.storage;
|
|
102
|
+
this.identity = settings.identity;
|
|
103
|
+
this.product = settings.product || PRODUCT_CODE;
|
|
104
|
+
this.productName = settings.productName || this.product;
|
|
105
|
+
this.version = settings.version || "unknown";
|
|
106
|
+
this.nodeRedVersion = settings.nodeRedVersion || "unknown";
|
|
107
|
+
this.serverUrl = sanitizeServerUrl(settings.serverUrl || process.env.ATELIER_LICENSE_SERVER_URL || DEFAULT_SERVER_URL);
|
|
108
|
+
this.publicKeys = settings.publicKeys || PUBLIC_KEYS;
|
|
109
|
+
this.timeoutMs = settings.timeoutMs || REQUEST_TIMEOUT_MS;
|
|
110
|
+
this.request = settings.request || requestJson;
|
|
111
|
+
this.nodeUsageProvider = typeof settings.nodeUsageProvider === "function" ? settings.nodeUsageProvider : function () { return 0; };
|
|
112
|
+
this.state = LICENSE_STATES.UNKNOWN;
|
|
113
|
+
this.token = null;
|
|
114
|
+
this.claims = null;
|
|
115
|
+
this.lastCheck = null;
|
|
116
|
+
this.nextCheck = null;
|
|
117
|
+
this.lastMessage = "";
|
|
118
|
+
this.retryIndex = 0;
|
|
119
|
+
this.scheduler = null;
|
|
120
|
+
this.controlScheduler = null;
|
|
121
|
+
this.startupRefresh = null;
|
|
122
|
+
this.busy = null;
|
|
123
|
+
this.lastBackgroundError = null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
runBackground(operation, promise) {
|
|
127
|
+
Promise.resolve(promise).catch((error) => {
|
|
128
|
+
const code = String(error && error.code || "SERVER_ERROR");
|
|
129
|
+
const message = String(error && error.message || error || "Unknown licensing error");
|
|
130
|
+
const fingerprint = operation + ":" + code + ":" + message;
|
|
131
|
+
const now = Date.now();
|
|
132
|
+
if (!this.lastBackgroundError || this.lastBackgroundError.fingerprint !== fingerprint || now - this.lastBackgroundError.at >= 60000) {
|
|
133
|
+
this.lastBackgroundError = { fingerprint, at: now };
|
|
134
|
+
this.emit("background-error", { operation, code, message, at: new Date(now).toISOString() });
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
setState(state, message) {
|
|
140
|
+
const previous = this.state;
|
|
141
|
+
this.state = state;
|
|
142
|
+
this.lastMessage = message || "";
|
|
143
|
+
if (previous !== state) this.emit("state-changed", this.getStatus());
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
verifyAndApply(token, options) {
|
|
147
|
+
try {
|
|
148
|
+
const previousState = this.state;
|
|
149
|
+
const previousFeatures = JSON.stringify(this.claims && this.claims.features || {});
|
|
150
|
+
const verified = verifyLicenseToken(token, { publicKeys: this.publicKeys, deviceId: this.identity.deviceId, product: this.product });
|
|
151
|
+
this.token = token;
|
|
152
|
+
this.claims = verified.payload;
|
|
153
|
+
const state = evaluateLicenseState(this.claims, options);
|
|
154
|
+
this.setState(state);
|
|
155
|
+
if (previousState === state && previousFeatures !== JSON.stringify(this.claims.features || {})) {
|
|
156
|
+
this.emit("state-changed", this.getStatus());
|
|
157
|
+
}
|
|
158
|
+
this.nextCheck = this.claims.refreshAfter || null;
|
|
159
|
+
return verified.payload;
|
|
160
|
+
} catch (error) {
|
|
161
|
+
this.token = null;
|
|
162
|
+
this.claims = null;
|
|
163
|
+
this.setState(error.code === "INVALID_DEVICE" ? LICENSE_STATES.INVALID_DEVICE : error.code === "PRODUCT_NOT_LICENSED" ? LICENSE_STATES.PRODUCT_NOT_LICENSED : LICENSE_STATES.INVALID_TOKEN, error.message);
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async initialize() {
|
|
169
|
+
const stored = await this.storage.load();
|
|
170
|
+
if (!stored || (!stored.token && !stored.serverState)) {
|
|
171
|
+
this.setState(LICENSE_STATES.NOT_ACTIVATED);
|
|
172
|
+
} else if (!stored.token && stored.serverState) {
|
|
173
|
+
this.setState(stored.serverState);
|
|
174
|
+
} else {
|
|
175
|
+
try {
|
|
176
|
+
this.verifyAndApply(stored.token);
|
|
177
|
+
if (stored.serverState) this.setState(stored.serverState);
|
|
178
|
+
if (this.shouldRefresh()) setImmediate(() => this.runBackground("startup-refresh", this.refresh()));
|
|
179
|
+
} catch (error) {
|
|
180
|
+
// verifyAndApply already selected a fail-closed state.
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
this.scheduler = setInterval(() => {
|
|
184
|
+
if (this.shouldRefresh()) this.runBackground("scheduled-refresh", this.refresh());
|
|
185
|
+
}, SCHEDULER_INTERVAL_MS);
|
|
186
|
+
if (typeof this.scheduler.unref === "function") this.scheduler.unref();
|
|
187
|
+
this.controlScheduler = setInterval(() => {
|
|
188
|
+
if (this.token && !this.busy) this.runBackground("control-status", this.checkStatus());
|
|
189
|
+
}, CONTROL_STATUS_INTERVAL_MS);
|
|
190
|
+
if (typeof this.controlScheduler.unref === "function") this.controlScheduler.unref();
|
|
191
|
+
if (this.token) {
|
|
192
|
+
this.startupRefresh = setTimeout(() => {
|
|
193
|
+
this.startupRefresh = null;
|
|
194
|
+
this.runBackground("startup-refresh", this.refresh());
|
|
195
|
+
}, 1000);
|
|
196
|
+
if (typeof this.startupRefresh.unref === "function") this.startupRefresh.unref();
|
|
197
|
+
}
|
|
198
|
+
return this.getStatus();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
shouldRefresh(now) {
|
|
202
|
+
if (!this.token || !this.claims || this.busy) return false;
|
|
203
|
+
const refreshAt = Date.parse(this.claims.refreshAfter);
|
|
204
|
+
const retryAt = this.nextCheck ? Date.parse(this.nextCheck) : NaN;
|
|
205
|
+
return Number(now || Date.now()) >= (Number.isFinite(retryAt) ? retryAt : refreshAt);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
metadata() {
|
|
209
|
+
let activeNodeCount = 0;
|
|
210
|
+
try { activeNodeCount = Math.max(0, Number(this.nodeUsageProvider()) || 0); } catch (error) { activeNodeCount = 0; }
|
|
211
|
+
return {
|
|
212
|
+
activeNodeCount: Math.floor(activeNodeCount),
|
|
213
|
+
deviceId: this.identity.deviceId,
|
|
214
|
+
product: this.product,
|
|
215
|
+
version: this.version,
|
|
216
|
+
deviceName: os.hostname(),
|
|
217
|
+
deviceModel: this.identity.deviceModel || null,
|
|
218
|
+
nodeRedVersion: this.nodeRedVersion,
|
|
219
|
+
venusOsVersion: this.identity.venusOsVersion || null,
|
|
220
|
+
macAddress: this.identity.macAddress || null,
|
|
221
|
+
hardwareSerial: this.identity.hardwareSerial || null,
|
|
222
|
+
systemUuid: this.identity.systemUuid || null,
|
|
223
|
+
manufacturer: this.identity.manufacturer || null,
|
|
224
|
+
osPlatform: this.identity.osPlatform || null,
|
|
225
|
+
osRelease: this.identity.osRelease || null,
|
|
226
|
+
cpuArch: this.identity.cpuArch || null,
|
|
227
|
+
networkInterface: this.identity.networkInterface || null,
|
|
228
|
+
identitySource: this.identity.identitySource || this.identity.source || null
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async acceptResponse(response, options) {
|
|
233
|
+
const settings = options || {};
|
|
234
|
+
const code = String(response.code || response.status || "").toUpperCase();
|
|
235
|
+
if (SERVER_STATE_MAP[code]) {
|
|
236
|
+
this.setState(SERVER_STATE_MAP[code], response.message);
|
|
237
|
+
await this.storage.saveState(SERVER_STATE_MAP[code], this.token, this.identity.deviceId, this.product, settings.licenseKeyFingerprint);
|
|
238
|
+
return this.getStatus();
|
|
239
|
+
}
|
|
240
|
+
if (!response.token) throw new LicenseError("SERVER_ERROR", "License server did not return a token");
|
|
241
|
+
this.verifyAndApply(response.token);
|
|
242
|
+
await this.storage.save(response.token, this.identity.deviceId, this.product, settings.licenseKeyFingerprint);
|
|
243
|
+
this.lastCheck = new Date().toISOString();
|
|
244
|
+
this.retryIndex = 0;
|
|
245
|
+
return this.getStatus();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
runExclusive(operation) {
|
|
249
|
+
if (this.busy) return this.busy;
|
|
250
|
+
this.busy = Promise.resolve().then(operation).finally(() => { this.busy = null; });
|
|
251
|
+
return this.busy;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
activate(licenseKey) {
|
|
255
|
+
const key = String(licenseKey || "").trim();
|
|
256
|
+
if (!key) return Promise.reject(new LicenseError("NOT_ACTIVATED", "License Key is required"));
|
|
257
|
+
return this.runExclusive(async () => {
|
|
258
|
+
this.setState(LICENSE_STATES.ACTIVATING);
|
|
259
|
+
this.lastCheck = new Date().toISOString();
|
|
260
|
+
try {
|
|
261
|
+
return await this.acceptResponse(await this.request(this.serverUrl, "/api/v1/license/activate", Object.assign({ licenseKey: key }, this.metadata()), this.timeoutMs), { licenseKeyFingerprint: licenseKeyFingerprint(key) });
|
|
262
|
+
} catch (error) {
|
|
263
|
+
this.applyRequestError(error, false);
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
refresh() {
|
|
270
|
+
if (!this.token) return Promise.reject(new LicenseError("NOT_ACTIVATED", "No stored license token"));
|
|
271
|
+
return this.runExclusive(async () => {
|
|
272
|
+
this.lastCheck = new Date().toISOString();
|
|
273
|
+
try {
|
|
274
|
+
const body = this.metadata();
|
|
275
|
+
delete body.product;
|
|
276
|
+
body.token = this.token;
|
|
277
|
+
return await this.acceptResponse(await this.request(this.serverUrl, "/api/v1/license/refresh", body, this.timeoutMs));
|
|
278
|
+
} catch (error) {
|
|
279
|
+
this.applyRequestError(error, true);
|
|
280
|
+
throw error;
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
checkStatus() {
|
|
286
|
+
if (!this.token) return Promise.reject(new LicenseError("NOT_ACTIVATED", "No stored license token"));
|
|
287
|
+
return this.runExclusive(async () => {
|
|
288
|
+
this.lastCheck = new Date().toISOString();
|
|
289
|
+
try {
|
|
290
|
+
const response = await this.request(this.serverUrl, "/api/v1/license/status", { token: this.token, deviceId: this.identity.deviceId, activeNodeCount: this.metadata().activeNodeCount }, this.timeoutMs);
|
|
291
|
+
if (response.token) return await this.acceptResponse(response);
|
|
292
|
+
this.verifyAndApply(this.token, { remoteState: response.status || "ACTIVE" });
|
|
293
|
+
await this.storage.save(this.token, this.identity.deviceId, this.product);
|
|
294
|
+
this.retryIndex = 0;
|
|
295
|
+
return this.getStatus();
|
|
296
|
+
} catch (error) {
|
|
297
|
+
this.applyRequestError(error, true);
|
|
298
|
+
throw error;
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
applyRequestError(error, hadToken) {
|
|
304
|
+
if (["INVALID_TOKEN", "INVALID_DEVICE", "PRODUCT_NOT_LICENSED"].includes(error.code)) {
|
|
305
|
+
this.setState(LICENSE_STATES[error.code], error.message);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
const mapped = SERVER_STATE_MAP[String(error.code || "").toUpperCase()];
|
|
309
|
+
if (mapped) {
|
|
310
|
+
this.setState(mapped, error.message);
|
|
311
|
+
this.nextCheck = null;
|
|
312
|
+
if (this.storage && typeof this.storage.saveState === "function") {
|
|
313
|
+
this.runBackground("persist-server-state", this.storage.saveState(mapped, this.token, this.identity.deviceId, this.product));
|
|
314
|
+
}
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
if (error.isNetworkError && hadToken && this.token) {
|
|
318
|
+
// A server-side block/suspension remains authoritative until the
|
|
319
|
+
// server explicitly reports ACTIVE again. Never fall back to an
|
|
320
|
+
// older locally valid token after such a denial.
|
|
321
|
+
if (AUTHORITATIVE_DENIAL_STATES.has(this.state)) {
|
|
322
|
+
this.lastMessage = "License server unavailable; the last administrative restriction remains active.";
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
try { this.verifyAndApply(this.token, { networkUnavailable: true }); } catch (verificationError) { return; }
|
|
326
|
+
const delay = RETRY_DELAYS_MS[Math.min(this.retryIndex, RETRY_DELAYS_MS.length - 1)];
|
|
327
|
+
this.retryIndex += 1;
|
|
328
|
+
this.nextCheck = new Date(Date.now() + delay).toISOString();
|
|
329
|
+
this.lastMessage = "License server unavailable; using locally verified license policy.";
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
this.setState(LICENSE_STATES.SERVER_ERROR, error.message);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async clear() {
|
|
336
|
+
await this.storage.clear();
|
|
337
|
+
this.token = null;
|
|
338
|
+
this.claims = null;
|
|
339
|
+
this.lastCheck = new Date().toISOString();
|
|
340
|
+
this.nextCheck = null;
|
|
341
|
+
this.setState(LICENSE_STATES.NOT_ACTIVATED);
|
|
342
|
+
return this.getStatus();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
close() {
|
|
346
|
+
if (this.scheduler) clearInterval(this.scheduler);
|
|
347
|
+
if (this.controlScheduler) clearInterval(this.controlScheduler);
|
|
348
|
+
if (this.startupRefresh) clearTimeout(this.startupRefresh);
|
|
349
|
+
this.scheduler = null;
|
|
350
|
+
this.controlScheduler = null;
|
|
351
|
+
this.startupRefresh = null;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
isLicensed() { return isLicensedState(this.state); }
|
|
355
|
+
getState() { return this.state; }
|
|
356
|
+
getCompany() { return this.claims ? (this.claims.company || this.claims.companyName || this.claims.companyId) : null; }
|
|
357
|
+
getPlan() { return this.claims ? (this.claims.plan || null) : null; }
|
|
358
|
+
getFeatures() { return Object.assign({}, this.claims && this.claims.features || {}); }
|
|
359
|
+
hasFeature(name) { return !!(this.claims && this.claims.features && this.claims.features[name] === true); }
|
|
360
|
+
getDeviceId() { return this.identity.deviceId; }
|
|
361
|
+
getLicenseType() { return this.claims ? this.claims.licenseType : null; }
|
|
362
|
+
getExpiresAt() { return this.claims ? this.claims.expiresAt : null; }
|
|
363
|
+
getOfflineGraceUntil() { return this.claims ? this.claims.offlineGraceUntil : null; }
|
|
364
|
+
getLastCheck() { return this.lastCheck; }
|
|
365
|
+
getNextCheck() { return this.nextCheck || (this.claims && this.claims.refreshAfter) || null; }
|
|
366
|
+
getStatus() {
|
|
367
|
+
return {
|
|
368
|
+
company: this.getCompany(), deviceId: this.getDeviceId(), deviceSource: this.identity.source,
|
|
369
|
+
expiresAt: this.getExpiresAt(), features: this.getFeatures(), lastCheck: this.getLastCheck(),
|
|
370
|
+
licenseType: this.getLicenseType(), message: this.lastMessage, nextCheck: this.getNextCheck(),
|
|
371
|
+
offlineGraceUntil: this.getOfflineGraceUntil(), plan: this.getPlan(), product: this.product,
|
|
372
|
+
productName: this.productName, state: this.getState()
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
module.exports = { AUTHORITATIVE_DENIAL_STATES, LicenseClient, NETWORK_CODES, requestJson, sanitizeServerUrl };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { LICENSED_STATES, LICENSE_STATES, LICENSE_TYPES } = require("./constants");
|
|
4
|
+
|
|
5
|
+
function timeValue(value) {
|
|
6
|
+
if (value === null || value === undefined || value === "") return null;
|
|
7
|
+
const parsed = Date.parse(value);
|
|
8
|
+
return Number.isFinite(parsed) ? parsed : NaN;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function evaluateLicenseState(payload, options) {
|
|
12
|
+
const settings = options || {};
|
|
13
|
+
const now = settings.now instanceof Date ? settings.now.getTime() : Number(settings.now || Date.now());
|
|
14
|
+
const networkUnavailable = !!settings.networkUnavailable;
|
|
15
|
+
const remoteState = String(settings.remoteState || payload.status || "").toUpperCase();
|
|
16
|
+
|
|
17
|
+
if (["REVOKED", "LICENSE_REVOKED"].includes(remoteState)) return LICENSE_STATES.REVOKED;
|
|
18
|
+
if (["SUSPENDED", "LICENSE_SUSPENDED", "COMPANY_SUSPENDED"].includes(remoteState)) return LICENSE_STATES.SUSPENDED;
|
|
19
|
+
if (remoteState === "DEVICE_BLOCKED") return LICENSE_STATES.DEVICE_BLOCKED;
|
|
20
|
+
if (remoteState === "DEVICE_REJECTED") return LICENSE_STATES.DEVICE_REJECTED;
|
|
21
|
+
if (remoteState === "DEVICE_DEACTIVATED") return LICENSE_STATES.DEVICE_DEACTIVATED;
|
|
22
|
+
if (remoteState === "PENDING_APPROVAL" || remoteState === "DEVICE_APPROVAL_REQUIRED") return LICENSE_STATES.PENDING_APPROVAL;
|
|
23
|
+
|
|
24
|
+
const licenseType = String(payload.licenseType || "").toUpperCase();
|
|
25
|
+
const expiresAt = timeValue(payload.expiresAt);
|
|
26
|
+
const offlineUntil = timeValue(payload.offlineGraceUntil);
|
|
27
|
+
|
|
28
|
+
if (licenseType === LICENSE_TYPES.SUBSCRIPTION && (!Number.isFinite(expiresAt) || now > expiresAt)) {
|
|
29
|
+
return LICENSE_STATES.EXPIRED;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!networkUnavailable) return LICENSE_STATES.ACTIVE;
|
|
33
|
+
|
|
34
|
+
// A null offlineGraceUntil represents an unlimited offline policy for
|
|
35
|
+
// both permanent and subscription licenses. Subscriptions are still
|
|
36
|
+
// bounded by expiresAt, which is checked above.
|
|
37
|
+
if (payload.offlineGraceUntil === null) {
|
|
38
|
+
return LICENSE_STATES.OFFLINE;
|
|
39
|
+
}
|
|
40
|
+
if (Number.isFinite(offlineUntil) && now <= offlineUntil) {
|
|
41
|
+
return LICENSE_STATES.GRACE_PERIOD;
|
|
42
|
+
}
|
|
43
|
+
return LICENSE_STATES.EXPIRED;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isLicensedState(state) {
|
|
47
|
+
return LICENSED_STATES.includes(state);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = { evaluateLicenseState, isLicensedState, timeValue };
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { STATUS_PRESENTATION } = require("./constants");
|
|
4
|
+
const { getPackageIntegrity } = require("./package-integrity");
|
|
5
|
+
|
|
6
|
+
function statusForLicense(license) {
|
|
7
|
+
if (!license) return { fill: "red", shape: "ring", text: "license missing" };
|
|
8
|
+
const state = typeof license.getState === "function" ? license.getState() : "UNKNOWN";
|
|
9
|
+
return STATUS_PRESENTATION[state] || STATUS_PRESENTATION.UNKNOWN;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function nodeFeatureKey(node) {
|
|
13
|
+
return "node:" + String(node && node.type || "").trim();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isNodeLicensed(license, node) {
|
|
17
|
+
if (!license || typeof license.hasFeature !== "function") return false;
|
|
18
|
+
return license.hasFeature("node:*") || license.hasFeature(nodeFeatureKey(node));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function runCloseHandler(handler) {
|
|
22
|
+
return new Promise(function (resolve) {
|
|
23
|
+
let complete = false;
|
|
24
|
+
let timeout = null;
|
|
25
|
+
function done() {
|
|
26
|
+
if (!complete) {
|
|
27
|
+
complete = true;
|
|
28
|
+
if (timeout) clearTimeout(timeout);
|
|
29
|
+
resolve();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const result = handler.length >= 2 ? handler(false, done) : handler(done);
|
|
34
|
+
if (result && typeof result.then === "function") result.then(done, done);
|
|
35
|
+
else if (handler.length === 0) done();
|
|
36
|
+
} catch (error) { done(); }
|
|
37
|
+
if (!complete) timeout = setTimeout(done, 5000);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function runLicensedNode(node, RED, config, startRuntime) {
|
|
42
|
+
const license = RED.nodes.getNode(config.license);
|
|
43
|
+
const integrity = getPackageIntegrity();
|
|
44
|
+
const originalOn = node.on.bind(node);
|
|
45
|
+
const originalRemoveListener = node.removeListener ? node.removeListener.bind(node) : null;
|
|
46
|
+
let running = false;
|
|
47
|
+
let closed = false;
|
|
48
|
+
let transition = Promise.resolve();
|
|
49
|
+
let runtimeListeners = [];
|
|
50
|
+
let closeHandlers = [];
|
|
51
|
+
let entitlementRefreshRequested = false;
|
|
52
|
+
|
|
53
|
+
function presentLicenseStatus() {
|
|
54
|
+
if (!integrity.isValid()) {
|
|
55
|
+
node.status(STATUS_PRESENTATION.PACKAGE_TAMPERED);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (license && license.isLicensed() && !isNodeLicensed(license, node)) {
|
|
59
|
+
node.status({ fill: "red", shape: "ring", text: "node not licensed" });
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
node.status(statusForLicense(license));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function start() {
|
|
66
|
+
if (closed || running || !integrity.isValid() || !license || !license.isLicensed() || !isNodeLicensed(license, node)) {
|
|
67
|
+
presentLicenseStatus();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
running = true;
|
|
71
|
+
const publicOn = node.on;
|
|
72
|
+
node.on = function (eventName, handler) {
|
|
73
|
+
if (eventName === "close") {
|
|
74
|
+
closeHandlers.push(handler);
|
|
75
|
+
return node;
|
|
76
|
+
}
|
|
77
|
+
const guarded = eventName === "input" ? function () {
|
|
78
|
+
if (!integrity.isValid() || !license.isLicensed() || !isNodeLicensed(license, node)) {
|
|
79
|
+
presentLicenseStatus();
|
|
80
|
+
const done = arguments.length > 2 && arguments[2];
|
|
81
|
+
if (typeof done === "function") done();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
return handler.apply(node, arguments);
|
|
85
|
+
} : handler;
|
|
86
|
+
runtimeListeners.push({ eventName, handler: guarded });
|
|
87
|
+
originalOn(eventName, guarded);
|
|
88
|
+
return node;
|
|
89
|
+
};
|
|
90
|
+
try {
|
|
91
|
+
startRuntime();
|
|
92
|
+
} catch (error) {
|
|
93
|
+
running = false;
|
|
94
|
+
node.error("Licensed runtime startup failed: " + (error.message || error));
|
|
95
|
+
} finally {
|
|
96
|
+
node.on = publicOn;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function stop() {
|
|
101
|
+
if (!running) return;
|
|
102
|
+
running = false;
|
|
103
|
+
if (originalRemoveListener) runtimeListeners.forEach(function (listener) {
|
|
104
|
+
originalRemoveListener(listener.eventName, listener.handler);
|
|
105
|
+
});
|
|
106
|
+
runtimeListeners = [];
|
|
107
|
+
const handlers = closeHandlers.splice(0);
|
|
108
|
+
for (const handler of handlers.reverse()) await runCloseHandler(handler);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function reconcile() {
|
|
112
|
+
transition = transition.then(async function () {
|
|
113
|
+
if (closed) return;
|
|
114
|
+
if (integrity.isValid() && license && license.isLicensed() && isNodeLicensed(license, node)) start();
|
|
115
|
+
else { await stop(); presentLicenseStatus(); }
|
|
116
|
+
}).catch(function (error) { node.error(error); });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function stateChanged() { reconcile(); refreshMissingEntitlementOnce(); }
|
|
120
|
+
if (license && typeof license.on === "function") license.on("state-changed", stateChanged);
|
|
121
|
+
integrity.on("state-changed", stateChanged);
|
|
122
|
+
|
|
123
|
+
function refreshMissingEntitlementOnce() {
|
|
124
|
+
if (entitlementRefreshRequested || !integrity.isValid() || !license || typeof license.refresh !== "function" || !license.isLicensed() || isNodeLicensed(license, node)) return;
|
|
125
|
+
entitlementRefreshRequested = true;
|
|
126
|
+
Promise.resolve(license.refresh()).then(reconcile).catch(function () { presentLicenseStatus(); });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
originalOn("close", function (removed, done) {
|
|
130
|
+
if (typeof removed === "function" && done === undefined) {
|
|
131
|
+
done = removed;
|
|
132
|
+
removed = false;
|
|
133
|
+
}
|
|
134
|
+
closed = true;
|
|
135
|
+
if (license && typeof license.removeListener === "function") license.removeListener("state-changed", stateChanged);
|
|
136
|
+
integrity.removeListener("state-changed", stateChanged);
|
|
137
|
+
transition.then(stop).finally(function () { if (typeof done === "function") done(); });
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
if (integrity.isValid() && license && license.isLicensed() && isNodeLicensed(license, node)) start();
|
|
141
|
+
else {
|
|
142
|
+
presentLicenseStatus();
|
|
143
|
+
// A newly delivered node may not exist in the previously issued token.
|
|
144
|
+
// Refresh once at deployment so its new entitlement becomes effective
|
|
145
|
+
// without opening the license configuration node manually.
|
|
146
|
+
refreshMissingEntitlementOnce();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
module.exports = { runLicensedNode, statusForLicense, isNodeLicensed };
|