iobroker.eos-admin 7.9.97 → 7.9.99

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/{INSTALL_TEST_V7.9.97.md → INSTALL_TEST_V7.9.99.md} +2 -2
  2. package/{MERGE_UPDATE_README_V7.9.97.md → MERGE_UPDATE_README_V7.9.99.md} +1 -1
  3. package/NEXOWATT_EOS_BUILD_INFO.json +21 -20
  4. package/{PUBLISH_STABLE_V7.9.97.md → PUBLISH_STABLE_V7.9.99.md} +2 -2
  5. package/{README_STABILITY_V7.9.97.md → README_STABILITY_V7.9.99.md} +1 -1
  6. package/{RELEASE_ACCEPTANCE_V7.9.97.md → RELEASE_ACCEPTANCE_V7.9.99.md} +1 -1
  7. package/RELEASE_NOTES_V7.9.99.md +9 -0
  8. package/adminWww/assets/Intro-DkwRiz1n-v84.js +2 -2
  9. package/adminWww/css/eos-ems-overview.css +3 -5
  10. package/adminWww/css/nexowatt-native-shell.css +32 -1
  11. package/adminWww/index.html +19 -18
  12. package/adminWww/js/eos-account-management.js +13 -9
  13. package/adminWww/js/eos-assistant.js +1 -1
  14. package/adminWww/js/eos-ems-overview.js +2 -3
  15. package/adminWww/js/eos-release-watch.js +77 -0
  16. package/adminWww/js/eos-role-bootstrap.js +7 -4
  17. package/adminWww/js/eos-role-ui.js +2 -2
  18. package/adminWww/js/nexowatt-native-shell.js +1 -1
  19. package/adminWww/static/js/nexowatt-stable-login.js +3 -3
  20. package/adminWww/static/js/{nexowatt-stable-v97.js → nexowatt-stable-v99.js} +2 -2
  21. package/build/lib/eosPassword.js +62 -0
  22. package/build/lib/eosRequestSecurity.js +31 -0
  23. package/build/lib/web.js +99 -71
  24. package/build/main.js +2 -1
  25. package/io-package.json +5 -5
  26. package/package.json +18 -12
  27. package/tools/nexowatt-account-management-selftest.cjs +20 -57
  28. package/tools/nexowatt-apply-ems-overview.cjs +2 -4
  29. package/tools/nexowatt-cache-update-selftest.cjs +11 -0
  30. package/tools/nexowatt-clean-legacy-runtime.cjs +1 -1
  31. package/tools/nexowatt-ems-overview-selftest.cjs +1 -1
  32. package/tools/nexowatt-login-layout-selftest.cjs +11 -11
  33. package/tools/nexowatt-merge-update-selftest.cjs +1 -1
  34. package/tools/nexowatt-modern-ui-selftest.cjs +1 -1
  35. package/tools/nexowatt-password-write-selftest.cjs +46 -0
  36. package/tools/nexowatt-request-security-selftest.cjs +11 -0
  37. package/tools/nexowatt-runtime-cleanup-selftest.cjs +6 -6
  38. package/tools/nexowatt-stable-v99-selftest.cjs +25 -0
  39. package/tools/nexowatt-ui-overview-runtime-selftest.cjs +3 -1
  40. package/tools/nexowatt-validate-package.cjs +23 -14
  41. package/RELEASE_NOTES_V7.9.97.md +0 -19
  42. package/tools/nexowatt-stable-v97-selftest.cjs +0 -16
package/build/lib/web.js CHANGED
@@ -21,6 +21,8 @@ const session = require("express-session");
21
21
  const bodyParser = require("body-parser");
22
22
  const cookieParser = require("cookie-parser");
23
23
  const iobroker_mcp_1 = require("iobroker.mcp");
24
+ const eosPassword_1 = require("./eosPassword");
25
+ const eosRequestSecurity_1 = require("./eosRequestSecurity");
24
26
  let AdapterStore;
25
27
  /** Content of a socket-io file */
26
28
  let socketIoFile;
@@ -714,22 +716,9 @@ class Web {
714
716
  return { valid: true, password: value };
715
717
  }
716
718
  async setEosUserPassword(userId, password) {
717
- // The authenticated route is strictly self-service: the target user comes from the current
718
- // session and cannot be supplied by the browser. Use the trusted EOS service context for the
719
- // actual password write so installer/end-user groups do not need global users.write rights.
720
- const options = { user: EOS_PASSWORD_SERVICE_USER };
721
- if (typeof this.adapter.setPasswordAsync === 'function') {
722
- await this.adapter.setPasswordAsync(userId, password, options);
723
- return;
724
- }
725
- if (typeof this.adapter.setPassword === 'function') {
726
- await new Promise((resolve, reject) => {
727
- this.adapter.setPassword?.(userId, password, options, error => (error ? reject(error) : resolve()));
728
- });
729
- return;
730
- }
731
- throw new Error('passwordApiUnavailable');
719
+ await (0, eosPassword_1.setEosUserPasswordWithVerification)(this.adapter, userId, password, EOS_PASSWORD_SERVICE_USER);
732
720
  }
721
+
733
722
  async destroyEosRequestSessions(req) {
734
723
  const token = this.readAccessTokenFromRequest(req);
735
724
  if (!token || typeof this.adapter.destroySession !== 'function') {
@@ -758,7 +747,11 @@ class Web {
758
747
  res.status(403).json({ error: 'passwordSetupNotAllowed' });
759
748
  return;
760
749
  }
761
- if (!this.isEosSameOriginWrite(req) || req.headers['x-nexowatt-eos-first-login'] !== '1') {
750
+ if (req.headers['x-nexowatt-eos-first-login'] !== '1') {
751
+ res.status(403).json({ error: 'invalidRequest' });
752
+ return;
753
+ }
754
+ if (!this.isEosSameOriginWrite(req)) {
762
755
  res.status(403).json({ error: 'invalidRequestOrigin' });
763
756
  return;
764
757
  }
@@ -781,8 +774,8 @@ class Web {
781
774
  if (!userObject) {
782
775
  throw new Error('userObjectUnavailableAfterPasswordChange');
783
776
  }
784
- const native = (userObject.native ||= {});
785
- const account = (native.nexowattEosAccount ||= {});
777
+ const native = { ...(userObject.native || {}) };
778
+ const account = { ...(native.nexowattEosAccount || {}) };
786
779
  const now = new Date().toISOString();
787
780
  account.passwordInitialized = true;
788
781
  account.passwordInitializedAt = Date.now();
@@ -793,13 +786,18 @@ class Web {
793
786
  account.firstLoginCompletedAt = now;
794
787
  account.forcePasswordChange = false;
795
788
  account.passwordlessFirstLoginAllowed = false;
789
+ native.nexowattEosAccount = account;
796
790
  native.nexowattPasswordChangeRequired = false;
797
791
  native.eosPasswordChangeRequired = false;
798
792
  native.nexowattFirstLoginPending = false;
799
793
  native.eosFirstLoginRequired = false;
800
794
  native.nexowattInitialPasswordApplied = false;
801
795
  native.nexowattPasswordChangedAt = now;
802
- await this.adapter.setForeignObjectAsync(access.userId, userObject);
796
+ await this.adapter.extendForeignObjectAsync(access.userId, { native }, { user: EOS_PASSWORD_SERVICE_USER });
797
+ const persistedState = await this.getEosFirstLoginPasswordState(access.userId, access.role);
798
+ if (persistedState.required || !persistedState.initialized) {
799
+ throw new Error('passwordMetadataNotPersisted');
800
+ }
803
801
  await this.destroyEosRequestSessions(req);
804
802
  for (const cookie of ['access_token', 'refresh_token', 'connect.sid']) {
805
803
  res.clearCookie(cookie);
@@ -1027,8 +1025,8 @@ class Web {
1027
1025
  if (!user) {
1028
1026
  throw new Error('userObjectUnavailableAfterPasswordChange');
1029
1027
  }
1030
- const native = (user.native ||= {});
1031
- const account = (native.nexowattEosAccount ||= {});
1028
+ const native = { ...(user.native || {}) };
1029
+ const account = { ...(native.nexowattEosAccount || {}) };
1032
1030
  const now = new Date().toISOString();
1033
1031
  account.passwordInitialized = true;
1034
1032
  account.passwordInitializedAt = Date.now();
@@ -1040,7 +1038,12 @@ class Web {
1040
1038
  account.forcePasswordChange = false;
1041
1039
  account.passwordlessFirstLoginAllowed = false;
1042
1040
  account.passwordlessClaimCompletedAt = now;
1043
- await this.adapter.setForeignObjectAsync(claim.userId, user);
1041
+ native.nexowattEosAccount = account;
1042
+ native.nexowattPasswordChangeRequired = false;
1043
+ native.eosPasswordChangeRequired = false;
1044
+ native.nexowattFirstLoginPending = false;
1045
+ native.eosFirstLoginRequired = false;
1046
+ await this.adapter.extendForeignObjectAsync(claim.userId, { native }, { user: EOS_PASSWORD_SERVICE_USER });
1044
1047
  this.invalidateEosPasswordClaimsForUser(claim.userId);
1045
1048
  res.clearCookie('nexowatt_eos_first_login', { path: '/nexowatt/account' });
1046
1049
  this.adapter.log.info(`EOS passwordless first activation completed for ${claim.userId}`);
@@ -1106,11 +1109,16 @@ class Web {
1106
1109
  async resetEosAccountPassword(req, res) {
1107
1110
  res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
1108
1111
  const access = await this.getEosRequestAccess(req);
1109
- if (!access.userId
1110
- || (access.role !== 'admin' && access.role !== 'installer')
1111
- || !this.isEosSameOriginWrite(req)
1112
- || req.headers['x-nexowatt-eos-account-reset'] !== '1') {
1113
- res.status(403).json({ error: 'permissionError' });
1112
+ if (!access.userId || (access.role !== 'admin' && access.role !== 'installer')) {
1113
+ res.status(403).json({ error: 'permissionError', role: access.role });
1114
+ return;
1115
+ }
1116
+ if (req.headers['x-nexowatt-eos-account-reset'] !== '1') {
1117
+ res.status(403).json({ error: 'invalidRequest' });
1118
+ return;
1119
+ }
1120
+ if (!this.isEosSameOriginWrite(req)) {
1121
+ res.status(403).json({ error: 'invalidRequestOrigin' });
1114
1122
  return;
1115
1123
  }
1116
1124
  const targetUserId = this.normalizeEosUserId(req.body?.user);
@@ -1118,19 +1126,12 @@ class Web {
1118
1126
  res.status(400).json({ error: 'invalidTarget' });
1119
1127
  return;
1120
1128
  }
1121
- const targetAccess = await this.getEosAccessForUserId(targetUserId);
1122
- const explicitInstaller = targetAccess.groups.some(group => this.getEosInstallerGroups().includes(group));
1123
- const explicitEndUser = targetAccess.groups.some(group => this.getEosEndUserGroups().includes(group));
1124
- const explicitlyManagedRole = explicitInstaller
1125
- ? 'installer'
1126
- : explicitEndUser
1127
- ? 'enduser'
1128
- : null;
1129
- if (targetAccess.role === 'admin'
1130
- || !explicitlyManagedRole
1131
- || (access.role === 'installer' && explicitlyManagedRole !== 'enduser')) {
1132
- // Never rely on the secure-default "enduser" classification for a password reset. The
1133
- // target must be an explicit member of a managed EOS installer/end-user group.
1129
+ const managedAccounts = await this.getEosManagedAccounts(access.role);
1130
+ const managedTarget = managedAccounts.find(account => account.id === targetUserId);
1131
+ const explicitlyManagedRole = managedTarget?.role === 'installer' || managedTarget?.role === 'enduser'
1132
+ ? managedTarget.role
1133
+ : null;
1134
+ if (!managedTarget || !explicitlyManagedRole) {
1134
1135
  res.status(403).json({ error: 'permissionError' });
1135
1136
  return;
1136
1137
  }
@@ -1148,8 +1149,8 @@ class Web {
1148
1149
  if (!user) {
1149
1150
  throw new Error('userObjectUnavailableAfterPasswordReset');
1150
1151
  }
1151
- const native = (user.native ||= {});
1152
- const account = (native.nexowattEosAccount ||= {});
1152
+ const native = { ...(user.native || {}) };
1153
+ const account = { ...(native.nexowattEosAccount || {}) };
1153
1154
  const now = new Date().toISOString();
1154
1155
  account.passwordInitialized = false;
1155
1156
  account.passwordSetupVersion = 0;
@@ -1159,6 +1160,7 @@ class Web {
1159
1160
  account.passwordResetAt = now;
1160
1161
  account.passwordResetBy = access.userId;
1161
1162
  account.passwordResetMode = 'initial-password';
1163
+ native.nexowattEosAccount = account;
1162
1164
  native.nexowattPasswordChangeRequired = true;
1163
1165
  native.eosPasswordChangeRequired = true;
1164
1166
  native.nexowattFirstLoginPending = true;
@@ -1168,7 +1170,11 @@ class Web {
1168
1170
  native.nexowattStableInitialCredentialVersion = 1;
1169
1171
  delete account.passwordSetAt;
1170
1172
  delete account.firstLoginCompletedAt;
1171
- await this.adapter.setForeignObjectAsync(targetUserId, user);
1173
+ await this.adapter.extendForeignObjectAsync(targetUserId, { native }, { user: EOS_PASSWORD_SERVICE_USER });
1174
+ const resetState = await this.getEosFirstLoginPasswordState(targetUserId, explicitlyManagedRole);
1175
+ if (!resetState.required || resetState.initialized) {
1176
+ throw new Error('passwordMetadataNotPersisted');
1177
+ }
1172
1178
  this.invalidateEosPasswordClaimsForUser(targetUserId);
1173
1179
  this.adapter.log.warn(`EOS account ${targetUserId} was reset to the mandatory initial-password flow by ${access.userId}`);
1174
1180
  res.status(200).json({
@@ -1245,15 +1251,9 @@ class Web {
1245
1251
  });
1246
1252
  }
1247
1253
  isEosSameOriginWrite(req) {
1248
- const origin = String(req.headers.origin || '').trim();
1249
- if (!origin) {
1250
- return true;
1251
- }
1252
- const forwardedProto = String(req.headers['x-forwarded-proto'] || '').split(',')[0].trim();
1253
- const protocol = forwardedProto || (this.settings.secure ? 'https' : 'http');
1254
- const host = String(req.headers.host || '').trim();
1255
- return !!host && origin === `${protocol}://${host}`;
1254
+ return (0, eosRequestSecurity_1.isEosSameOriginRequest)(req.headers);
1256
1255
  }
1256
+
1257
1257
  async saveEosBasicSettings(req, res) {
1258
1258
  res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
1259
1259
  const access = await this.getEosRequestAccess(req);
@@ -1484,6 +1484,9 @@ class Web {
1484
1484
  next();
1485
1485
  });*/
1486
1486
  this.server.app.get('/version', (_req, res) => {
1487
+ res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
1488
+ res.setHeader('Pragma', 'no-cache');
1489
+ res.setHeader('Expires', '0');
1487
1490
  res.status(200).send(this.adapter.version);
1488
1491
  });
1489
1492
  // v36: Repair stale/broken URLs generated by older EOS hard-logout builds.
@@ -1762,8 +1765,13 @@ class Web {
1762
1765
  });
1763
1766
  this.server.app.post('/nexowatt/account/first-password', (req, res) => {
1764
1767
  void this.saveEosFirstLoginPassword(req, res).catch(e => {
1765
- this.adapter.log.warn(`Cannot initialize EOS first-login password: ${e instanceof Error ? e.message : e}`);
1766
- res.status(500).json({ error: 'passwordSetupFailed' });
1768
+ const detail = e instanceof Error ? e.message : String(e || '');
1769
+ this.adapter.log.warn(`Cannot initialize EOS first-login password: ${detail}`);
1770
+ const known = new Set([
1771
+ 'passwordApiUnavailable', 'passwordNotPersisted', 'passwordVerificationFailed',
1772
+ 'passwordMetadataNotPersisted', 'userObjectUnavailableAfterPasswordChange',
1773
+ ]);
1774
+ res.status(500).json({ error: known.has(detail) ? detail : 'passwordSetupFailed' });
1767
1775
  });
1768
1776
  });
1769
1777
  this.server.app.get('/nexowatt/account/manage', (req, res) => {
@@ -1774,8 +1782,13 @@ class Web {
1774
1782
  });
1775
1783
  this.server.app.post('/nexowatt/account/reset', (req, res) => {
1776
1784
  void this.resetEosAccountPassword(req, res).catch(e => {
1777
- this.adapter.log.warn(`Cannot reset EOS account: ${e instanceof Error ? e.message : e}`);
1778
- res.status(500).json({ error: 'accountResetFailed' });
1785
+ const detail = e instanceof Error ? e.message : String(e || '');
1786
+ this.adapter.log.warn(`Cannot reset EOS account: ${detail}`);
1787
+ const known = new Set([
1788
+ 'passwordApiUnavailable', 'passwordNotPersisted', 'passwordVerificationFailed',
1789
+ 'passwordMetadataNotPersisted', 'userObjectUnavailableAfterPasswordReset',
1790
+ ]);
1791
+ res.status(500).json({ error: known.has(detail) ? detail : 'accountResetFailed' });
1779
1792
  });
1780
1793
  });
1781
1794
  this.server.app.get('/iobroker_check.html', (_req, res) => {
@@ -1902,7 +1915,29 @@ class Web {
1902
1915
  res.status(404).send(get404Page(`File ${escapeHtml(fileName)} not found`));
1903
1916
  }
1904
1917
  });
1905
- const appOptions = {};
1918
+ const isEosCurrentAsset = (pathName) => pathName === '/'
1919
+ || pathName === '/index.html'
1920
+ || pathName === '/manifest.json'
1921
+ || pathName === '/mf-manifest.json'
1922
+ || pathName === '/version'
1923
+ || /^\/(?:js|css)\/(?:eos-|nexowatt-)/.test(pathName)
1924
+ || /^\/static\/js\/nexowatt-stable-v\d+\.js$/.test(pathName)
1925
+ || /^\/remoteEntry(?:-v\d+)?\.js$/.test(pathName)
1926
+ || /^\/assets\/(?:bootstrap|hostInit|Intro|index-CQZugZ1z|index-D2ymscJA|iobroker_admin__mf_v__runtimeInit__mf_v__)-[^/]+\.js$/.test(pathName);
1927
+ const applyNoStoreHeaders = (res) => {
1928
+ res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
1929
+ res.setHeader('Pragma', 'no-cache');
1930
+ res.setHeader('Expires', '0');
1931
+ res.setHeader('Surrogate-Control', 'no-store');
1932
+ };
1933
+ const appOptions = {
1934
+ setHeaders: (res, filePath) => {
1935
+ const raw = filePath.startsWith(this.wwwDir) ? filePath.slice(this.wwwDir.length) : filePath;
1936
+ const relativePath = `/${raw.replace(/\\/g, '/').replace(/^\/+/, '')}`;
1937
+ if (isEosCurrentAsset(relativePath))
1938
+ applyNoStoreHeaders(res);
1939
+ },
1940
+ };
1906
1941
  if (this.settings.cache) {
1907
1942
  appOptions.maxAge = 30_758_400_000;
1908
1943
  }
@@ -2000,31 +2035,24 @@ class Web {
2000
2035
  this.server.app.get('/empty.html', (_req, res) => {
2001
2036
  res.status(200).send('');
2002
2037
  });
2003
- // Stability release: entrypoints and module-federation manifests must always
2004
- // revalidate. Hashed leaf assets remain cacheable through express.static.
2038
+ // EOS entrypoints, overlays and patched module bundles must never be reused
2039
+ // from a previous release. Hashed untouched leaf assets may remain cacheable.
2005
2040
  this.server.app.use((req, res, next) => {
2006
2041
  const pathName = req.path || req.url.split('?')[0];
2007
- if (pathName === '/'
2008
- || pathName === '/index.html'
2009
- || pathName === '/mf-manifest.json'
2010
- || /^\/remoteEntry(?:-v\d+)?\.js$/.test(pathName)
2011
- || /^\/assets\/(?:hostInit|index-CQZugZ1z)-[^/]+\.js$/.test(pathName)) {
2012
- res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
2013
- res.setHeader('Pragma', 'no-cache');
2014
- res.setHeader('Expires', '0');
2015
- }
2042
+ if (isEosCurrentAsset(pathName))
2043
+ applyNoStoreHeaders(res);
2016
2044
  next();
2017
2045
  });
2018
2046
  this.server.app.get('/index.html', async (_req, res) => {
2019
- this.indexHTML ||= await this.prepareIndex('/index.html');
2047
+ this.indexHTML = await this.prepareIndex('/index.html');
2020
2048
  res.header('Content-Type', 'text/html');
2021
- res.header('Cache-Control', 'no-cache');
2049
+ applyNoStoreHeaders(res);
2022
2050
  res.status(200).send(this.indexHTML);
2023
2051
  });
2024
2052
  this.server.app.get('/', async (_req, res) => {
2025
- this.indexHTML ||= await this.prepareIndex('/index.html');
2053
+ this.indexHTML = await this.prepareIndex('/index.html');
2026
2054
  res.header('Content-Type', 'text/html');
2027
- res.header('Cache-Control', 'no-cache');
2055
+ applyNoStoreHeaders(res);
2028
2056
  res.status(200).send(this.indexHTML);
2029
2057
  });
2030
2058
  this.server.app.use('/', express.static(this.wwwDir, appOptions));
package/build/main.js CHANGED
@@ -20,6 +20,7 @@ const socket_classes_1 = require("@iobroker/socket-classes");
20
20
  const ws_server_1 = require("@iobroker/ws-server");
21
21
  const translations_1 = require("./lib/translations");
22
22
  const web_1 = require("./lib/web");
23
+ const eosPassword_1 = require("./lib/eosPassword");
23
24
  const checkLinuxPass_1 = require("./lib/checkLinuxPass");
24
25
  const plugin_docker_1 = require("@iobroker/plugin-docker");
25
26
  const objectFixes_1 = require("./lib/objectFixes");
@@ -1152,7 +1153,7 @@ class Admin extends adapter_core_1.Adapter {
1152
1153
  }
1153
1154
  if (String(user.common.password || '') === '') {
1154
1155
  const bootstrapSecret = `${(0, node_crypto_1.randomBytes)(48).toString('base64url')}!aA1`;
1155
- await this.setPasswordAsync(userId, bootstrapSecret, { user: ADMIN_OWNER_USER });
1156
+ await (0, eosPassword_1.setEosUserPasswordWithVerification)(this, userId, bootstrapSecret, ADMIN_OWNER_USER);
1156
1157
  user = (await this.getForeignObjectAsync(userId));
1157
1158
  if (!user) {
1158
1159
  return;
package/io-package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "eos-admin",
4
- "version": "7.9.97",
4
+ "version": "7.9.99",
5
5
  "titleLang": {
6
6
  "en": "NexoWatt EOS Admin",
7
7
  "de": "NexoWatt EOS Admin",
@@ -329,7 +329,7 @@
329
329
  "icon": "admin.svg",
330
330
  "messagebox": true,
331
331
  "enabled": true,
332
- "extIcon": "https://unpkg.com/iobroker.eos-admin@7.9.97/admin/admin.svg",
332
+ "extIcon": "https://unpkg.com/iobroker.eos-admin@7.9.99/admin/admin.svg",
333
333
  "keywords": [
334
334
  "NexoWatt",
335
335
  "EOS",
@@ -340,7 +340,7 @@
340
340
  "licensed"
341
341
  ],
342
342
  "compact": true,
343
- "readme": "https://unpkg.com/iobroker.eos-admin@7.9.97/README.md",
343
+ "readme": "https://unpkg.com/iobroker.eos-admin@7.9.99/README.md",
344
344
  "authors": [
345
345
  "NexoWatt"
346
346
  ],
@@ -408,7 +408,7 @@
408
408
  "nondeletable": false,
409
409
  "allowAdapterUpdate": true,
410
410
  "allowAdapterDelete": false,
411
- "meta": "https://unpkg.com/iobroker.eos-admin@7.9.97/io-package.json",
411
+ "meta": "https://unpkg.com/iobroker.eos-admin@7.9.99/io-package.json",
412
412
  "npmPackage": "iobroker.eos-admin",
413
413
  "packetName": "iobroker.eos-admin",
414
414
  "publisher": "NexoWatt"
@@ -1117,5 +1117,5 @@
1117
1117
  }
1118
1118
  }
1119
1119
  },
1120
- "version": "7.9.97"
1120
+ "version": "7.9.99"
1121
1121
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "iobroker.eos-admin",
3
- "description": "NexoWatt EOS Admin – stable sales release with orderly login, role-safe overview and live EMS diagnosis.",
4
- "version": "7.9.97",
3
+ "description": "NexoWatt EOS Admin – stable candidate with truthful EMS overview, verified user passwords and deterministic browser updates.",
4
+ "version": "7.9.99",
5
5
  "contributors": [
6
6
  "bluefox <dogafox@gmail.com>",
7
7
  "apollon77",
@@ -118,19 +118,22 @@
118
118
  "tools/nexowatt-version-sync-selftest.cjs",
119
119
  "tools/nexowatt-merge-update-selftest.cjs",
120
120
  "tools/nexowatt-ems-overview-selftest.cjs",
121
- "README_STABILITY_V7.9.97.md",
122
- "RELEASE_NOTES_V7.9.97.md",
123
- "PUBLISH_STABLE_V7.9.97.md",
124
- "INSTALL_TEST_V7.9.97.md",
125
- "RELEASE_ACCEPTANCE_V7.9.97.md",
126
- "MERGE_UPDATE_README_V7.9.97.md",
121
+ "README_STABILITY_V7.9.99.md",
122
+ "RELEASE_NOTES_V7.9.99.md",
123
+ "PUBLISH_STABLE_V7.9.99.md",
124
+ "INSTALL_TEST_V7.9.99.md",
125
+ "RELEASE_ACCEPTANCE_V7.9.99.md",
126
+ "MERGE_UPDATE_README_V7.9.99.md",
127
127
  "README_EMS_OVERVIEW_MERGE.md",
128
128
  "RC73_EOS_ADMIN_MERGE_VALIDATION.md",
129
129
  "VALIDATION_EMS_OVERVIEW_MERGE.md",
130
130
  "NexoWatt_EOS_Admin_EMS_OVERVIEW_MERGE_VALIDATION.md",
131
131
  "tools/nexowatt-apply-ems-overview.cjs",
132
132
  "tools/nexowatt-ui-overview-runtime-selftest.cjs",
133
- "tools/nexowatt-stable-v97-selftest.cjs"
133
+ "tools/nexowatt-stable-v99-selftest.cjs",
134
+ "tools/nexowatt-password-write-selftest.cjs",
135
+ "tools/nexowatt-request-security-selftest.cjs",
136
+ "tools/nexowatt-cache-update-selftest.cjs"
134
137
  ],
135
138
  "scripts": {
136
139
  "npm": "npm i -f && cd src-admin && npm i -f",
@@ -165,7 +168,7 @@
165
168
  "nexowatt:repo-entry": "node tools/nexowatt-generate-repo-entry.cjs",
166
169
  "nexowatt:patch-repo": "node tools/nexowatt-patch-repo.cjs",
167
170
  "nexowatt:repair-update": "node tools/nexowatt-repair-eos-admin-update.cjs",
168
- "check:eos-stability": "node tools/nexowatt-stability-check.cjs && node tools/nexowatt-esm-syntax-selftest.cjs && node tools/nexowatt-import-integrity-selftest.cjs && node tools/nexowatt-entrypoint-smoke-selftest.cjs && node tools/nexowatt-frontend-graph-check.cjs && node tools/nexowatt-policy-selftest.cjs && node tools/nexowatt-publish-channel-selftest.cjs && node tools/nexowatt-release-defaults-selftest.cjs && node tools/nexowatt-role-access-selftest.cjs && node tools/nexowatt-internal-reserve-selftest.cjs && node tools/nexowatt-first-login-selftest.cjs && node tools/nexowatt-account-management-selftest.cjs && node tools/nexowatt-modern-ui-selftest.cjs && node tools/nexowatt-login-layout-selftest.cjs && node tools/nexowatt-branding-selftest.cjs && node tools/nexowatt-default-port-selftest.cjs && node tools/nexowatt-module-popup-selftest.cjs && node tools/nexowatt-datapoint-selftest.cjs && node tools/nexowatt-update-selftest.cjs && node tools/nexowatt-datapoint-interaction-selftest.cjs && node tools/nexowatt-manual-write-policy-selftest.cjs && node tools/nexowatt-value-column-selftest.cjs && node tools/nexowatt-native-shell-selftest.cjs && node tools/nexowatt-assistant-separation-selftest.cjs && node tools/nexowatt-runtime-cleanup-selftest.cjs && node tools/nexowatt-version-sync-selftest.cjs && node tools/nexowatt-merge-update-selftest.cjs && npm run check:eos-stable-v97 && node tools/nexowatt-ems-overview-selftest.cjs && node tools/nexowatt-ui-overview-runtime-selftest.cjs",
171
+ "check:eos-stability": "node tools/nexowatt-stability-check.cjs && node tools/nexowatt-esm-syntax-selftest.cjs && node tools/nexowatt-import-integrity-selftest.cjs && node tools/nexowatt-entrypoint-smoke-selftest.cjs && node tools/nexowatt-frontend-graph-check.cjs && node tools/nexowatt-policy-selftest.cjs && node tools/nexowatt-publish-channel-selftest.cjs && node tools/nexowatt-release-defaults-selftest.cjs && node tools/nexowatt-role-access-selftest.cjs && node tools/nexowatt-internal-reserve-selftest.cjs && node tools/nexowatt-first-login-selftest.cjs && node tools/nexowatt-account-management-selftest.cjs && node tools/nexowatt-cache-update-selftest.cjs && node tools/nexowatt-request-security-selftest.cjs && node tools/nexowatt-password-write-selftest.cjs && node tools/nexowatt-modern-ui-selftest.cjs && node tools/nexowatt-login-layout-selftest.cjs && node tools/nexowatt-branding-selftest.cjs && node tools/nexowatt-default-port-selftest.cjs && node tools/nexowatt-module-popup-selftest.cjs && node tools/nexowatt-datapoint-selftest.cjs && node tools/nexowatt-update-selftest.cjs && node tools/nexowatt-datapoint-interaction-selftest.cjs && node tools/nexowatt-manual-write-policy-selftest.cjs && node tools/nexowatt-value-column-selftest.cjs && node tools/nexowatt-native-shell-selftest.cjs && node tools/nexowatt-assistant-separation-selftest.cjs && node tools/nexowatt-runtime-cleanup-selftest.cjs && node tools/nexowatt-version-sync-selftest.cjs && node tools/nexowatt-merge-update-selftest.cjs && npm run check:eos-stable-v99 && node tools/nexowatt-ems-overview-selftest.cjs && node tools/nexowatt-ui-overview-runtime-selftest.cjs",
169
172
  "nexowatt:patch-built-frontend": "node tools/nexowatt-patch-built-frontend.cjs",
170
173
  "clean:eos-runtime": "node tools/nexowatt-clean-legacy-runtime.cjs",
171
174
  "precheck:eos-package": "npm run sync:eos-version && npm run prepare:eos-release-defaults && npm run clean:eos-runtime",
@@ -189,7 +192,10 @@
189
192
  "sync:eos-version": "node tools/nexowatt-sync-release-version.cjs",
190
193
  "test:eos-version-sync": "node tools/nexowatt-version-sync-selftest.cjs",
191
194
  "verify:eos-merge": "node tools/nexowatt-merge-update-selftest.cjs",
192
- "check:eos-stable-v97": "node tools/nexowatt-stable-v97-selftest.cjs"
195
+ "check:eos-stable-v99": "node tools/nexowatt-stable-v99-selftest.cjs",
196
+ "test:eos-password-write": "node tools/nexowatt-password-write-selftest.cjs",
197
+ "test:eos-request-security": "node tools/nexowatt-request-security-selftest.cjs",
198
+ "test:eos-cache-update": "node tools/nexowatt-cache-update-selftest.cjs"
193
199
  },
194
200
  "author": "NexoWatt",
195
201
  "private": false,
@@ -200,7 +206,7 @@
200
206
  },
201
207
  "nexowattReleasePolicy": {
202
208
  "distTag": "latest",
203
- "purpose": "accepted stable NexoWatt EOS sales-product release 7.9.97"
209
+ "purpose": "accepted stable NexoWatt EOS sales-product release 7.9.99"
204
210
  },
205
211
  "publisher": "NexoWatt"
206
212
  }
@@ -1,60 +1,23 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
- const fs = require('fs');
4
- const path = require('path');
5
- const root = path.resolve(__dirname, '..');
6
- const read = rel => fs.readFileSync(path.join(root, rel), 'utf8');
7
- const json = rel => JSON.parse(read(rel));
8
- let bad = false;
9
- const fail = msg => { console.error(`[NexoWatt EOS account management] ${msg}`); bad = true; };
10
-
11
- const mainSource = read('src/main.ts');
12
- const mainBuilt = read('build/main.js');
13
- const webSource = read('src/lib/web.ts');
14
- const webBuilt = read('build/lib/web.js');
15
- const sourceUi = read('src-admin/public/js/eos-account-management.js');
16
- const builtUi = read('adminWww/js/eos-account-management.js');
17
- const roleUi = read('adminWww/js/eos-role-ui.js');
18
- const css = read('adminWww/css/nexowatt-native-shell.css');
19
- const index = read('adminWww/index.html');
20
- const info = json('NEXOWATT_EOS_BUILD_INFO.json');
21
- const shellTag = String(info.shellCacheTag || info.shellCacheVersion);
22
-
23
- for (const code of [mainSource, mainBuilt]) {
24
- for (const marker of [
25
- 'system.user.installer', 'system.user.guest', 'ensureEosDefaultRoleUsers', 'unknown random bootstrap secret',
26
- 'eosPasswordSetupRequired', 'personal password setup is required',
27
- ]) if (!code.includes(marker)) fail(`main account marker missing: ${marker}`);
3
+ const fs=require('node:fs');const path=require('node:path');const root=path.resolve(__dirname,'..');const read=rel=>fs.readFileSync(path.join(root,rel),'utf8');const json=rel=>JSON.parse(read(rel));
4
+ const fail=m=>{console.error(`[NexoWatt EOS account management] ${m}`);process.exit(1)};const must=(c,m)=>{if(!c)fail(m)};
5
+ const mainSource=read('src/main.ts'),mainBuilt=read('build/main.js'),webSource=read('src/lib/web.ts'),webBuilt=read('build/lib/web.js'),passwordSource=read('src/lib/eosPassword.ts');
6
+ const sourceUi=read('src-admin/public/js/eos-account-management.js'),builtUi=read('adminWww/js/eos-account-management.js'),roleUi=read('adminWww/js/eos-role-ui.js'),css=read('adminWww/css/nexowatt-native-shell.css'),index=read('adminWww/index.html'),info=json('NEXOWATT_EOS_BUILD_INFO.json'),shellTag=String(info.shellCacheTag||info.shellCacheVersion);
7
+ for(const code of [mainSource,mainBuilt])for(const marker of ['system.user.installer','system.user.guest','ensureEosDefaultRoleUsers','unknown random bootstrap secret','personal password setup is required'])must(code.includes(marker),`main marker missing: ${marker}`);
8
+ for(const code of [webSource,webBuilt]){
9
+ for(const marker of ['getEosManagedAccounts','sendEosAccountManagement','resetEosAccountPassword','/nexowatt/account/manage','/nexowatt/account/reset','x-nexowatt-eos-account-reset','canResetInstaller','canResetEndUser',"targetUserId === 'system.user.admin'",'targetUserId === access.userId','const managedAccounts = await this.getEosManagedAccounts(access.role)','const managedTarget = managedAccounts.find(account => account.id === targetUserId)',"managedTarget?.role === 'installer'","managedTarget?.role === 'enduser'",'passwordResetAt','passwordResetBy',"passwordResetMode = 'initial-password'","setEosUserPassword(targetUserId, 'nexowatt')",'passwordlessFirstLoginAllowed = false','initialPasswordRequired: true','accountDisabled','passwordMetadataNotPersisted']) must(code.includes(marker),`web marker missing: ${marker}`);
10
+ must(!code.includes('const explicitInstaller = targetAccess.groups.some'),'obsolete explicit installer re-classifier remains');
11
+ must(!code.includes('const explicitEndUser = targetAccess.groups.some'),'obsolete explicit end-user re-classifier remains');
12
+ must(!code.includes('setForeignObjectAsync(targetUserId, user)'),'reset still writes stale full user object');
28
13
  }
29
- for (const code of [webSource, webBuilt]) {
30
- for (const marker of [
31
- 'getEosManagedAccounts', 'sendEosAccountManagement', 'resetEosAccountPassword',
32
- '/nexowatt/account/manage', '/nexowatt/account/reset', 'x-nexowatt-eos-account-reset',
33
- 'canResetInstaller', 'canResetEndUser', "targetUserId === 'system.user.admin'", 'targetUserId === access.userId',
34
- "access.role === 'installer' && explicitlyManagedRole !== 'enduser'",
35
- 'explicit member of a managed EOS installer/end-user group', 'passwordResetAt', 'passwordResetBy',
36
- "passwordResetMode = 'initial-password'", "setEosUserPassword(targetUserId, 'nexowatt')", 'passwordlessFirstLoginAllowed = false', 'initialPasswordRequired: true', 'accountDisabled',
37
- ]) if (!code.includes(marker)) fail(`web account marker missing: ${marker}`);
38
- if (!code.includes('const explicitInstaller = targetAccess.groups.some')) fail('reset target is not restricted to explicit installer membership');
39
- if (!code.includes('const explicitEndUser = targetAccess.groups.some')) fail('reset target is not restricted to explicit end-user membership');
40
- }
41
-
42
- for (const marker of [
43
- 'v91-account-management-under-access-rights', "['admin', 'installer']", 'nexowatt/account/manage',
44
- 'nexowatt/account/reset', 'X-NexoWatt-EOS-Account-Reset', 'data.canResetInstaller === true',
45
- "account.role === 'enduser'", 'Admin/Service', 'Installateure', 'NEXOWATT_EOS_ACCOUNT_MANAGEMENT',
46
- 'ensureEntrySurface', "currentRoute() !== 'tab-users'", 'eos-account-management-entry',
47
- ]) if (!sourceUi.includes(marker)) fail(`account UI marker missing: ${marker}`);
48
- if (sourceUi.includes("launcher.className = 'eos-account-management-launcher'")) fail('standalone account launcher is still created');
49
- if (sourceUi.includes('new MutationObserver')) fail('account management adds a second broad MutationObserver');
50
- if (sourceUi !== builtUi) fail('account management source/build drift');
51
- if (!roleUi.includes("route === 'tab-users'")) fail('installer cannot reach Zugänge & Rechte');
52
- if (roleUi.includes('NEXOWATT_EOS_ACCOUNT_MANAGEMENT.open')) fail('role overview opens account management outside Zugänge & Rechte');
53
- if (!index.includes(`eos-account-management.js?v=${shellTag}`)) fail('account management asset is not active with the current cache tag');
54
- for (const marker of [
55
- '.eos-account-management-entry', '.eos-account-management-overlay', '.eos-account-management-dialog',
56
- '.eos-account-row', '.eos-account-reset', 'html.eos-account-page-installer',
57
- ]) if (!css.includes(marker)) fail(`account management CSS marker missing: ${marker}`);
58
-
59
- if (bad) process.exit(1);
60
- console.log('[NexoWatt EOS account management] OK');
14
+ for(const marker of ['setPasswordAsync(userName','checkPasswordAsync(userName','passwordNotPersisted','passwordVerificationFailed'])must(passwordSource.includes(marker),`password helper marker missing: ${marker}`);
15
+ for(const marker of ["VERSION = 'v99-account-management-password-write'","['admin', 'installer']",'nexowatt/account/manage','nexowatt/account/reset','X-NexoWatt-EOS-Account-Reset','data.canResetInstaller === true',"account.role === 'enduser'",'Admin/Service','Installateure','NEXOWATT_EOS_ACCOUNT_MANAGEMENT','ensureEntrySurface',"currentRoute() !== 'tab-users'",'eos-account-management-entry','Startpasswort „nexowatt“','passwordVerificationFailed'])must(sourceUi.includes(marker),`UI marker missing: ${marker}`);
16
+ must(sourceUi===builtUi,'account management source/build drift');
17
+ must(!sourceUi.includes("launcher.className = 'eos-account-management-launcher'"),'standalone account launcher remains');
18
+ must(!sourceUi.includes('new MutationObserver'),'account management adds a broad observer');
19
+ must(roleUi.includes("route === 'tab-users'"),'installer cannot reach Zugänge & Rechte');
20
+ must(!roleUi.includes('NEXOWATT_EOS_ACCOUNT_MANAGEMENT.open'),'overview opens account management outside Zugänge & Rechte');
21
+ must(index.includes(`eos-account-management.js?v=${shellTag}`),'account management cache tag stale');
22
+ for(const marker of ['.eos-account-management-entry','.eos-account-management-overlay','.eos-account-management-dialog','.eos-account-row','.eos-account-reset','html.eos-account-page-installer'])must(css.includes(marker),`CSS marker missing: ${marker}`);
23
+ console.log('[NexoWatt EOS account management] OK (managed-list authorization and real password reset)');
@@ -52,7 +52,7 @@ if (!css.includes(cssMarker)) {
52
52
  overflow: hidden;
53
53
  }
54
54
  .eos-ems-overview-card * { box-sizing: border-box; }
55
- .eos-ems-overview-header { display:flex; align-items:flex-start; justify-content:space-between; gap:20px; }
55
+ .eos-ems-overview-header { display:flex; align-items:flex-start; justify-content:flex-start; gap:20px; }
56
56
  .eos-ems-overview-header h2 { margin:5px 0 6px; font-size:clamp(1.2rem,2vw,1.65rem); color:#f5fbff; }
57
57
  .eos-ems-overview-header p { margin:0; max-width:760px; color:#9eb2c2; line-height:1.45; }
58
58
  .eos-ems-overview-eyebrow { color:#49e39e; font-size:.72rem; font-weight:900; letter-spacing:.13em; text-transform:uppercase; }
@@ -83,9 +83,7 @@ if (!css.includes(cssMarker)) {
83
83
  .eos-ems-overview-event time { color:#6f8799; font-size:.68rem; font-variant-numeric:tabular-nums; }
84
84
  .eos-ems-overview-event div { display:grid; gap:2px; min-width:0; }
85
85
  .eos-ems-overview-empty { color:#718797; font-size:.76rem; }
86
- .eos-ems-overview-footer { display:flex; justify-content:space-between; gap:12px; margin-top:12px; color:#60798a; font-size:.67rem; }
87
- .eos-ems-overview-footer a { color:#5ee0c2; font-weight:800; text-decoration:none; }
88
- .eos-ems-overview-footer a:hover,.eos-ems-overview-footer a:focus-visible { color:#9af2dd; text-decoration:underline; }
86
+ .eos-ems-overview-footer { display:flex; justify-content:flex-start; gap:12px; margin-top:12px; color:#60798a; font-size:.67rem; }
89
87
  @media (max-width: 1100px) {
90
88
  .eos-ems-overview-card { flex-basis:100%; max-width:none; }
91
89
  }
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ const fs=require('node:fs');const path=require('node:path');const root=path.resolve(__dirname,'..');
4
+ const read=rel=>fs.readFileSync(path.join(root,rel),'utf8');const fail=m=>{console.error(`[NexoWatt EOS cache update] ${m}`);process.exit(1)};const must=(c,m)=>{if(!c)fail(m)};
5
+ const source=read('src/lib/web.ts'),built=read('build/lib/web.js'),index=read('adminWww/index.html'),dev=read('src-admin/index.html'),watch=read('adminWww/js/eos-release-watch.js');
6
+ for(const code of [source,built])for(const marker of ['isEosCurrentAsset','applyNoStoreHeaders','Surrogate-Control','this.indexHTML = await this.prepareIndex'])must(code.includes(marker),`server marker missing: ${marker}`);
7
+ for(const marker of ["const RELEASE = '7.9.99'",'nexowatt.eos.loadedRelease','cache: \'no-store\'','window.location.replace','navigator.serviceWorker?.getRegistrations'])must(watch.includes(marker),`watcher marker missing: ${marker}`);
8
+ for(const html of [index,dev])for(const marker of ['eos-release-watch.js?v=99','nexowatt-stable-v99.js?v=99','nexowatt-native-shell.css?v=99'])must(html.includes(marker),`index marker missing: ${marker}`);
9
+ must(index.includes('hostInit-v84.js?v=99')&&index.includes('index-CQZugZ1z-v84.js?v=99'),'productive module entry cache tags missing');
10
+ must(!/\?v=(?:97|98)(?:["'])/.test(index),'stale product cache tag remains');
11
+ console.log('[NexoWatt EOS cache update] OK (no-store entrypoints and release watcher)');
@@ -74,7 +74,7 @@ function isLegacyVersionedRuntime(file, activeRuntime, activeShell) {
74
74
  // Stable product overlays use the product release number in their filename,
75
75
  // while the compiled React runtime can intentionally remain on v84. Keep
76
76
  // only the active stable overlay and remove stale release assets left behind
77
- // by a flat ZIP update (for example v96 when installing v97).
77
+ // by a flat ZIP update (for example v98 when installing v99).
78
78
  const stableMatch = name.match(/^nexowatt-stable-v(\d+)\.js$/i);
79
79
  if (stableMatch) return Number(stableMatch[1]) !== activeShell;
80
80
  const versionMatch = name.match(/-v(\d+)\.(?:js|js\.map|css|css\.map)$/i);
@@ -19,11 +19,11 @@ for (const marker of [
19
19
  'document.visibilityState',
20
20
  'window.setInterval(() => void load(), 5_000)',
21
21
  'Diagnose ist rein lesend',
22
- 'Vollständige EMS-Diagnose öffnen',
23
22
  'system.adapter.nexowatt-ui.*.alive',
24
23
  ]) if (!component.includes(marker)) fail(`Komponenten-Vertrag fehlt: ${marker}`);
25
24
  if ((component.match(/NexoWatt EMS · Live-Diagnose/g) || []).length !== 1) fail('EMS-Überschrift ist doppelt oder fehlt.');
26
25
  if (!css.includes('/* nexowatt-ems-overview-v1 */')) fail('Responsive EMS-CSS fehlt.');
26
+ if (component.includes('Vollständige EMS-Diagnose öffnen')) fail('Nicht vorhandener Diagnose-Link darf nicht angeboten werden.');
27
27
  if (/setState\(|setForeignState\(|sendToHost\(/.test(component)) fail('EMS-Overview darf keine ioBroker-Schreiboperation enthalten.');
28
28
  const assetDir = path.join(root, 'admin', 'assets');
29
29
  if (fs.existsSync(assetDir)) {