iobroker.eos-admin 7.9.99 → 7.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/{INSTALL_TEST_V7.9.99.md → INSTALL_TEST_V7.10.0.md} +2 -2
  2. package/{MERGE_UPDATE_README_V7.9.99.md → MERGE_UPDATE_README_V7.10.0.md} +1 -1
  3. package/NEXOWATT_EOS_BUILD_INFO.json +21 -21
  4. package/{PUBLISH_STABLE_V7.9.99.md → PUBLISH_STABLE_V7.10.0.md} +2 -2
  5. package/README_STABILITY_V7.10.0.md +3 -0
  6. package/{RELEASE_ACCEPTANCE_V7.9.99.md → RELEASE_ACCEPTANCE_V7.10.0.md} +1 -1
  7. package/RELEASE_NOTES_V7.10.0.md +17 -0
  8. package/adminWww/assets/bootstrap-COulQZax-v84.js +1 -1
  9. package/adminWww/css/eos-auto-update.css +4 -0
  10. package/adminWww/css/eos-ems-overview.css +1 -1
  11. package/adminWww/css/nexowatt-native-shell.css +432 -2
  12. package/adminWww/index.html +21 -19
  13. package/adminWww/js/eos-account-management.js +1 -1
  14. package/adminWww/js/eos-assistant.js +1 -1
  15. package/adminWww/js/eos-auto-update.js +78 -0
  16. package/adminWww/js/eos-ems-overview.js +1 -1
  17. package/adminWww/js/eos-release-watch.js +1 -1
  18. package/adminWww/js/eos-role-bootstrap.js +69 -12
  19. package/adminWww/js/eos-role-ui.js +2 -2
  20. package/adminWww/js/nexowatt-native-shell.js +1 -1
  21. package/adminWww/static/js/nexowatt-stable-login.js +70 -43
  22. package/adminWww/static/js/nexowatt-stable-v7100.js +111 -0
  23. package/build/lib/eosAutoUpdate.js +43 -0
  24. package/build/lib/eosPassword.js +49 -39
  25. package/build/lib/web.js +142 -19
  26. package/build/main.js +1 -1
  27. package/io-package.json +7 -6
  28. package/package.json +20 -14
  29. package/src/lib/eosAutoUpdate.ts +320 -0
  30. package/tools/nexowatt-account-management-selftest.cjs +2 -2
  31. package/tools/nexowatt-auto-update-selftest.cjs +34 -0
  32. package/tools/nexowatt-cache-update-selftest.cjs +6 -5
  33. package/tools/nexowatt-login-layout-selftest.cjs +11 -10
  34. package/tools/nexowatt-merge-update-selftest.cjs +1 -1
  35. package/tools/nexowatt-modern-ui-selftest.cjs +1 -1
  36. package/tools/nexowatt-password-write-selftest.cjs +13 -41
  37. package/tools/nexowatt-runtime-cleanup-selftest.cjs +6 -4
  38. package/tools/nexowatt-stable-v7100-selftest.cjs +17 -0
  39. package/tools/nexowatt-sync-release-version.cjs +4 -1
  40. package/tools/nexowatt-ui-overview-runtime-selftest.cjs +1 -1
  41. package/tools/nexowatt-validate-package.cjs +11 -11
  42. package/README_STABILITY_V7.9.99.md +0 -18
  43. package/RELEASE_NOTES_V7.9.99.md +0 -9
  44. package/adminWww/static/js/nexowatt-stable-v99.js +0 -33
  45. package/tools/nexowatt-stable-v99-selftest.cjs +0 -25
@@ -1,7 +1,7 @@
1
1
  (() => {
2
2
  'use strict';
3
3
 
4
- const VERSION = 'v99-first-login-password-persisted';
4
+ const VERSION = 'v7100-first-login-password-persisted';
5
5
  const script = document.currentScript || document.querySelector('script[src*="eos-role-bootstrap.js"]');
6
6
  const entry = script?.dataset?.eosEntry || '';
7
7
  let launched = false;
@@ -80,7 +80,7 @@
80
80
  required: 'Bitte beide Passwortfelder ausfüllen.', mismatch: 'Die Passwörter stimmen nicht überein.',
81
81
  length: min => `Das Passwort muss mindestens ${min} Zeichen lang sein.`, complexity: 'Groß-/Kleinbuchstabe, Zahl und Sonderzeichen sind erforderlich.',
82
82
  easy: 'Das Passwort ist zu leicht oder enthält den Kontonamen.', generic: 'Das Passwort konnte nicht gespeichert werden.', success: 'Passwort gespeichert. Die Anmeldung wird neu gestartet …',
83
- backendErrors: { invalidRequest: 'Die Sicherheitsbestätigung fehlt. Bitte erneut anmelden.', invalidRequestOrigin: 'Die Sitzung oder aufgerufene Adresse hat sich geändert. Bitte erneut anmelden.', passwordApiUnavailable: 'Die ioBroker-Passwortverwaltung ist nicht verfügbar.', passwordNotPersisted: 'Das Passwort wurde nicht im ioBroker-Benutzerkonto gespeichert.', passwordVerificationFailed: 'Das neue Passwort wurde geschrieben, aber nicht als gültige Anmeldung bestätigt.', passwordMetadataNotPersisted: 'Das Passwort wurde gespeichert, aber der Erstanmeldungsstatus konnte nicht abgeschlossen werden.', passwordSetupFailed: 'Das Passwort konnte serverseitig nicht gespeichert werden. Bitte das EOS-Systemlog prüfen.' },
83
+ backendErrors: { invalidRequest: 'Die Sicherheitsbestätigung fehlt. Bitte erneut anmelden.', invalidRequestOrigin: 'Die Sitzung oder aufgerufene Adresse hat sich geändert. Bitte erneut anmelden.', passwordApiUnavailable: 'Die ioBroker-Passwortverwaltung ist nicht verfügbar.', passwordNotPersisted: 'Das Passwort wurde vom ioBroker-Controller nicht gespeichert. Bitte Systemlog prüfen.', passwordVerificationFailed: 'Das neue Passwort wurde geschrieben, aber nicht als gültige Anmeldung bestätigt.', passwordMetadataNotPersisted: 'Das Passwort wurde gespeichert, aber der Erstanmeldungsstatus konnte nicht abgeschlossen werden.', passwordSetupFailed: 'Das Passwort konnte serverseitig nicht gespeichert werden. Bitte das EOS-Systemlog prüfen.', passwordSetupTokenExpired: 'Die sichere Passwortfreigabe ist abgelaufen. Bitte erneut anmelden.', passwordSetupTokenMismatch: 'Die Passwortfreigabe gehört nicht zu dieser Sitzung. Bitte erneut anmelden.' },
84
84
  securityTitle: 'Zugriff wird geprüft', securityIntro: 'Die sichere Anmeldung wird gerade geprüft. Die Oberfläche bleibt gesperrt, bis die Berechtigungen eindeutig geladen wurden.',
85
85
  securityRetry: 'Jetzt erneut prüfen', securityLogout: 'Zur Anmeldung', securityAuto: seconds => `Automatischer neuer Versuch in ${seconds} Sekunden …`,
86
86
  claimLauncher: 'Erstanmeldung', claimTitle: 'Konto erstmals aktivieren',
@@ -100,7 +100,7 @@
100
100
  required: 'Please complete both password fields.', mismatch: 'The passwords do not match.',
101
101
  length: min => `The password must contain at least ${min} characters.`, complexity: 'Upper/lower case, a number and a special character are required.',
102
102
  easy: 'The password is too easy or contains the account name.', generic: 'The password could not be saved.', success: 'Password saved. Restarting sign-in …',
103
- backendErrors: { invalidRequest: 'The security confirmation is missing. Sign in again.', invalidRequestOrigin: 'The session or address changed. Sign in again.', passwordApiUnavailable: 'The ioBroker password service is unavailable.', passwordNotPersisted: 'The password was not persisted in the ioBroker user account.', passwordVerificationFailed: 'The new password was written but could not be verified as a valid login.', passwordMetadataNotPersisted: 'The password was saved, but the first-login status could not be completed.', passwordSetupFailed: 'The password could not be saved on the server. Check the EOS system log.' },
103
+ backendErrors: { invalidRequest: 'The security confirmation is missing. Sign in again.', invalidRequestOrigin: 'The session or address changed. Sign in again.', passwordApiUnavailable: 'The ioBroker password service is unavailable.', passwordNotPersisted: 'The ioBroker controller did not persist the password. Check the system log.', passwordVerificationFailed: 'The new password was written but could not be verified as a valid login.', passwordMetadataNotPersisted: 'The password was saved, but the first-login status could not be completed.', passwordSetupFailed: 'The password could not be saved on the server. Check the EOS system log.', passwordSetupTokenExpired: 'The secure password setup ticket expired. Sign in again.', passwordSetupTokenMismatch: 'The password setup ticket does not belong to this session. Sign in again.' },
104
104
  securityTitle: 'Checking access', securityIntro: 'The secure sign-in is being verified. The interface stays locked until the permissions are loaded unambiguously.',
105
105
  securityRetry: 'Check again now', securityLogout: 'Return to sign-in', securityAuto: seconds => `Retrying automatically in ${seconds} seconds …`,
106
106
  claimLauncher: 'First sign-in', claimTitle: 'Activate account for the first time',
@@ -120,7 +120,7 @@
120
120
  required: 'Vul beide wachtwoordvelden in.', mismatch: 'De wachtwoorden zijn niet gelijk.',
121
121
  length: min => `Het wachtwoord moet minimaal ${min} tekens bevatten.`, complexity: 'Hoofdletter, kleine letter, cijfer en speciaal teken zijn verplicht.',
122
122
  easy: 'Het wachtwoord is te eenvoudig of bevat de accountnaam.', generic: 'Het wachtwoord kon niet worden opgeslagen.', success: 'Wachtwoord opgeslagen. De aanmelding wordt opnieuw gestart …',
123
- backendErrors: { invalidRequest: 'De beveiligingsbevestiging ontbreekt. Meld opnieuw aan.', invalidRequestOrigin: 'De sessie of het adres is gewijzigd. Meld opnieuw aan.', passwordApiUnavailable: 'De ioBroker-wachtwoordservice is niet beschikbaar.', passwordNotPersisted: 'Het wachtwoord is niet in het ioBroker-gebruikersaccount opgeslagen.', passwordVerificationFailed: 'Het nieuwe wachtwoord is geschreven maar kon niet als geldige aanmelding worden bevestigd.', passwordMetadataNotPersisted: 'Het wachtwoord is opgeslagen, maar de eerste-aanmeldstatus kon niet worden afgerond.', passwordSetupFailed: 'Het wachtwoord kon server-side niet worden opgeslagen. Controleer het EOS-systeemlogboek.' },
123
+ backendErrors: { invalidRequest: 'De beveiligingsbevestiging ontbreekt. Meld opnieuw aan.', invalidRequestOrigin: 'De sessie of het adres is gewijzigd. Meld opnieuw aan.', passwordApiUnavailable: 'De ioBroker-wachtwoordservice is niet beschikbaar.', passwordNotPersisted: 'De ioBroker-controller heeft het wachtwoord niet opgeslagen. Controleer het systeemlogboek.', passwordVerificationFailed: 'Het nieuwe wachtwoord is geschreven maar kon niet als geldige aanmelding worden bevestigd.', passwordMetadataNotPersisted: 'Het wachtwoord is opgeslagen, maar de eerste-aanmeldstatus kon niet worden afgerond.', passwordSetupFailed: 'Het wachtwoord kon server-side niet worden opgeslagen. Controleer het EOS-systeemlogboek.', passwordSetupTokenExpired: 'De beveiligde wachtwoordtoestemming is verlopen. Meld opnieuw aan.', passwordSetupTokenMismatch: 'De wachtwoordtoestemming hoort niet bij deze sessie. Meld opnieuw aan.' },
124
124
  securityTitle: 'Toegang wordt gecontroleerd', securityIntro: 'De veilige aanmelding wordt gecontroleerd. De interface blijft vergrendeld totdat de rechten eenduidig zijn geladen.',
125
125
  securityRetry: 'Nu opnieuw controleren', securityLogout: 'Terug naar aanmelden', securityAuto: seconds => `Automatisch opnieuw proberen over ${seconds} seconden …`,
126
126
  claimLauncher: 'Eerste aanmelding', claimTitle: 'Account voor het eerst activeren',
@@ -133,11 +133,32 @@
133
133
  },
134
134
  };
135
135
 
136
+ const waitForPasswordSocket = async (timeoutMs = 10000) => {
137
+ const started = Date.now();
138
+ while (Date.now() - started < timeoutMs) {
139
+ const socket = window.NEXOWATT_EOS_ADMIN_SOCKET || window.NEXOWATT_EOS_SOCKET;
140
+ if (socket && typeof socket.changePassword === 'function') return socket;
141
+ await new Promise(resolve => window.setTimeout(resolve, 120));
142
+ }
143
+ return null;
144
+ };
145
+
146
+ const postWithTimeout = async (url, options, timeoutMs = 25000) => {
147
+ const controller = new AbortController();
148
+ const timer = window.setTimeout(() => controller.abort(), timeoutMs);
149
+ try {
150
+ return await fetch(url, { ...options, signal: controller.signal });
151
+ } finally {
152
+ window.clearTimeout(timer);
153
+ }
154
+ };
155
+
136
156
  const showFirstLoginPassword = (policy, base) => {
137
157
  const t = translations[language()];
138
158
  const passwordSetup = policy?.passwordSetup || {};
139
159
  const minLength = Number(passwordSetup.minLength || 10);
140
160
  const userName = String(passwordSetup.userName || policy?.user || '').replace(/^system\.user\./, '') || 'user';
161
+ const setupToken = String(passwordSetup.setupToken || '');
141
162
  const endpoint = new URL('nexowatt/account/first-password', base).href;
142
163
  const logoutUrl = new URL('logout', base).href;
143
164
  window.NEXOWATT_EOS_PASSWORD_SETUP_ACTIVE = true;
@@ -194,10 +215,31 @@
194
215
  if (!/[a-z]/.test(password.value) || !/[A-Z]/.test(password.value) || !/\d/.test(password.value) || !/[^A-Za-z0-9]/.test(password.value)) return setStatus(t.complexity, 'error');
195
216
  submit.disabled = true; submit.textContent = t.saving; setStatus('', '');
196
217
  try {
197
- const response = await fetch(endpoint, {
218
+ const socket = await waitForPasswordSocket();
219
+ let passwordAlreadyWritten = false;
220
+ if (socket) {
221
+ try {
222
+ // Use the same native path as the working ioBroker user editor.
223
+ await socket.changePassword(`system.user.${userName}`, password.value);
224
+ passwordAlreadyWritten = true;
225
+ } catch (socketError) {
226
+ console.warn('[NexoWatt EOS] Native password write failed; using server fallback:', socketError);
227
+ }
228
+ }
229
+ const response = await postWithTimeout(endpoint, {
198
230
  method: 'POST', credentials: 'same-origin', cache: 'no-store',
199
- headers: { 'Content-Type': 'application/json', Accept: 'application/json', 'X-NexoWatt-EOS-First-Login': '1' },
200
- body: JSON.stringify({ password: password.value, passwordRepeat: repeat.value }),
231
+ headers: {
232
+ 'Content-Type': 'application/json',
233
+ Accept: 'application/json',
234
+ 'X-NexoWatt-EOS-First-Login': '1',
235
+ ...(setupToken ? { 'X-NexoWatt-EOS-Password-Setup-Token': setupToken } : {}),
236
+ },
237
+ body: JSON.stringify({
238
+ password: password.value,
239
+ passwordRepeat: repeat.value,
240
+ setupToken,
241
+ passwordAlreadyWritten,
242
+ }),
201
243
  });
202
244
  const data = await response.json().catch(() => ({}));
203
245
  if (!response.ok || data.error) {
@@ -208,7 +250,14 @@
208
250
  window.setTimeout(() => window.location.replace(logoutUrl), 650);
209
251
  } catch (error) {
210
252
  submit.disabled = false; submit.textContent = t.submit;
211
- setStatus(error?.message || t.generic, 'error');
253
+ const message = error?.name === 'AbortError'
254
+ ? (language() === 'de'
255
+ ? 'Das Speichern hat zu lange gedauert. Bitte erneut versuchen.'
256
+ : language() === 'nl'
257
+ ? 'Opslaan duurde te lang. Probeer opnieuw.'
258
+ : 'Saving timed out. Please try again.')
259
+ : error?.message || t.generic;
260
+ setStatus(message, 'error');
212
261
  }
213
262
  });
214
263
  window.setTimeout(() => password.focus(), 0);
@@ -318,9 +367,17 @@
318
367
  const loginElements = () => {
319
368
  const username = document.querySelector('input#username,input[name="username"]');
320
369
  const password = document.querySelector('input#password,input[name="password"]');
321
- const card = username?.closest?.('.MuiPaper-root,form,main,section') || password?.closest?.('.MuiPaper-root,form,main,section');
322
- const submit = card?.querySelector?.('button[type="submit"]') || Array.from(card?.querySelectorAll?.('button') || []).find(button => /anmelden|login|sign in|aanmelden/.test(normalize(button.textContent || '')));
323
- return { username, password, card, submit };
370
+ const form = username?.closest?.('form') || password?.closest?.('form');
371
+ const card = form?.closest?.('.MuiPaper-root')
372
+ || username?.closest?.('.MuiPaper-root')
373
+ || password?.closest?.('.MuiPaper-root')
374
+ || form
375
+ || username?.closest?.('main,section')
376
+ || password?.closest?.('main,section');
377
+ const submit = form?.querySelector?.('button[type="submit"]')
378
+ || card?.querySelector?.('button[type="submit"]')
379
+ || Array.from(card?.querySelectorAll?.('button') || []).find(button => /anmelden|login|sign in|aanmelden/.test(normalize(button.textContent || '')));
380
+ return { username, password, form, card, submit };
324
381
  };
325
382
  const statusNode = card => card?.querySelector?.('.eos-login-first-status');
326
383
  const directChildWithin = (ancestor, node) => {
@@ -503,7 +560,7 @@
503
560
  const { username, password, card, submit } = loginElements();
504
561
  if (!username || !password || !card) return;
505
562
  card.classList.add('eos-login-card-modern');
506
- // v99 normal-login first activation: do not add role buttons or a second panel.
563
+ // v7100 normal-login first activation: do not add role buttons or a second panel.
507
564
  // The account is entered in the existing Loginname field; a blank password is accepted only
508
565
  // after the backend confirms that this exact managed account is eligible.
509
566
  card.querySelectorAll('.eos-login-role-selector').forEach(element => element.remove());
@@ -1,7 +1,7 @@
1
1
  (() => {
2
2
  'use strict';
3
3
 
4
- const VERSION = 'v99-native-overview-ems-live-reserve-filter';
4
+ const VERSION = 'v7100-native-overview-ems-live-reserve-filter';
5
5
  window.NEXOWATT_EOS_ROLE_UI_VERSION = VERSION;
6
6
 
7
7
  const state = {
@@ -278,7 +278,7 @@
278
278
  });
279
279
  };
280
280
  const ensureSafeOverview = () => safe(() => {
281
- // 7.9.99: never cover the real Admin Intro with a second tile surface.
281
+ // 7.10.0: never cover the real Admin Intro with a second tile surface.
282
282
  // Installer and end-user use the native card component; security remains
283
283
  // enforced by route/instance filters and backend permissions.
284
284
  removeSafeOverview();
@@ -1,7 +1,7 @@
1
1
  (() => {
2
2
  'use strict';
3
3
 
4
- const VERSION = 'v99-nexowatt-native-shell-native-overview';
4
+ const VERSION = 'v7100-nexowatt-native-shell-native-overview';
5
5
  const previous = window.NEXOWATT_NATIVE_SHELL;
6
6
  if (previous?.version === VERSION) return;
7
7
  previous?.destroy?.();
@@ -1,45 +1,72 @@
1
- /* NexoWatt EOS stable runtime v99 */
1
+ /* NexoWatt EOS stable login runtime 7.10.0 */
2
2
  (() => {
3
- 'use strict';
4
- const MANAGED_USERS = new Set(['installer', 'guest', 'user']);
5
- const ASSIST_TEXT = /EOS\s*Assist/i;
6
- const PASSWORDLESS_TEXT = /Erstanmeldung\s+ohne\s+Passwort|first\s+sign-?in\s+without\s+password/i;
7
- const isLogin = () => /(?:\?|&)login(?:[=&]|$)/i.test(location.search) || /\/login(?:[/?#]|$)/i.test(location.pathname + location.hash);
8
- const visible = element => !!element && element.getClientRects().length > 0;
9
- const findInput = kind => {
10
- const inputs = [...document.querySelectorAll('input')].filter(visible);
11
- if (kind === 'password') return inputs.find(input => input.type === 'password');
12
- return inputs.find(input => input.type !== 'password' && /user|login|name/i.test(`${input.name} ${input.id} ${input.autocomplete} ${input.getAttribute('aria-label') || ''}`)) || inputs.find(input => input.type !== 'password');
13
- };
14
- const findSubmit = () => [...document.querySelectorAll('button,input[type="submit"]')].find(element => visible(element) && (/anmelden|login|sign in/i.test((element.textContent || element.value || '').trim()) || element.type === 'submit'));
15
- const sync = () => {
16
- if (!isLogin()) return;
17
- document.documentElement.classList.add('nw-eos-login-v99');
18
- document.body?.classList.add('nw-eos-login-v99');
19
- const username = findInput('user');
20
- const password = findInput('password');
21
- const submit = findSubmit();
22
- const account = String(username?.value || '').trim().toLowerCase().replace(/^system\.user\./, '');
23
- if (submit && MANAGED_USERS.has(account) && !String(password?.value || '')) {
24
- submit.disabled = true;
25
- submit.setAttribute('aria-disabled', 'true');
26
- }
27
- };
28
- const hideLegacyUi = () => {
29
- for (const element of document.querySelectorAll('button,a,[role="button"],div,span')) {
30
- const value = (element.textContent || '').trim();
31
- if (!value || value.length > 100) continue;
32
- if (ASSIST_TEXT.test(value) || PASSWORDLESS_TEXT.test(value)) {
33
- const target = element.closest('button,a,[role="button"],[data-eos-assist],.eos-assist') || element;
34
- target.style.setProperty('display', 'none', 'important');
35
- target.setAttribute('aria-hidden', 'true');
36
- }
37
- }
38
- };
39
- const apply = () => { sync(); hideLegacyUi(); };
40
- const observer = new MutationObserver(() => requestAnimationFrame(apply));
41
- observer.observe(document.documentElement, { subtree: true, childList: true });
42
- document.addEventListener('input', apply, true);
43
- document.addEventListener('change', apply, true);
44
- if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', apply, { once: true }); else apply();
3
+ 'use strict';
4
+ const VERSION = 'v7100-clean-login';
5
+ const MANAGED_USERS = new Set(['installer', 'guest', 'user']);
6
+ const ASSIST_TEXT = /EOS\s*Assist/i;
7
+ const PASSWORDLESS_TEXT = /Erstanmeldung\s+ohne\s+Passwort|first\s+sign-?in\s+without\s+password/i;
8
+ const isLogin = () => /(?:\?|&)login(?:[=&]|$)/i.test(location.search) || /\/login(?:[/?#]|$)/i.test(location.pathname + location.hash);
9
+ const visible = element => !!element && element.getClientRects().length > 0;
10
+ const findInput = kind => {
11
+ const inputs = [...document.querySelectorAll('input')].filter(visible);
12
+ if (kind === 'password') return inputs.find(input => input.type === 'password');
13
+ return inputs.find(input => input.type !== 'password' && /user|login|name/i.test(`${input.name} ${input.id} ${input.autocomplete} ${input.getAttribute('aria-label') || ''}`)) || inputs.find(input => input.type !== 'password');
14
+ };
15
+ const findSubmit = () => [...document.querySelectorAll('button,input[type="submit"]')].find(element => visible(element) && (/anmelden|login|sign in/i.test((element.textContent || element.value || '').trim()) || element.type === 'submit'));
16
+ const normalizeUser = value => String(value || '').trim().toLowerCase().replace(/^system\.user\./, '');
17
+ const normalizeGeometry = () => {
18
+ document.querySelectorAll('[data-nw-login-card],[data-nw-login-shell]').forEach(element => {
19
+ if (!element.classList.contains('MuiPaper-root') && !element.classList.contains('eos-login-card-modern')) {
20
+ element.removeAttribute('data-nw-login-card');
21
+ element.removeAttribute('data-nw-login-shell');
22
+ }
23
+ });
24
+ for (const input of [findInput('user'), findInput('password')].filter(Boolean)) {
25
+ const control = input.closest('.MuiFormControl-root');
26
+ const label = control?.querySelector('.MuiInputLabel-root');
27
+ if (control && label) {
28
+ const localized = String(label.textContent || input.getAttribute('aria-label') || '').trim();
29
+ if (localized) control.dataset.eosLoginLabel = localized;
30
+ label.classList.add('MuiInputLabel-shrink');
31
+ label.dataset.shrink = 'true';
32
+ label.removeAttribute('aria-hidden');
33
+ }
34
+ }
35
+ };
36
+ const sync = () => {
37
+ if (!isLogin() && !document.querySelector('input#username,input[name="username"]')) return;
38
+ document.documentElement.classList.add('nw-eos-login-v7100');
39
+ document.body?.classList.add('nw-eos-login-v7100');
40
+ normalizeGeometry();
41
+ const username = findInput('user');
42
+ const password = findInput('password');
43
+ const submit = findSubmit();
44
+ if (submit && MANAGED_USERS.has(normalizeUser(username?.value)) && !String(password?.value || '')) {
45
+ submit.disabled = true;
46
+ submit.setAttribute('aria-disabled', 'true');
47
+ }
48
+ };
49
+ const hideLegacyUi = () => {
50
+ for (const element of document.querySelectorAll('button,a,[role="button"],div,span')) {
51
+ const value = (element.textContent || '').trim();
52
+ if (!value || value.length > 100) continue;
53
+ if (ASSIST_TEXT.test(value) || PASSWORDLESS_TEXT.test(value)) {
54
+ const target = element.closest('button,a,[role="button"],[data-eos-assist],.eos-assist') || element;
55
+ target.style.setProperty('display', 'none', 'important');
56
+ target.setAttribute('aria-hidden', 'true');
57
+ }
58
+ }
59
+ };
60
+ const apply = () => { sync(); hideLegacyUi(); };
61
+ let scheduled = false;
62
+ const schedule = () => {
63
+ if (scheduled) return;
64
+ scheduled = true;
65
+ requestAnimationFrame(() => { scheduled = false; apply(); });
66
+ };
67
+ new MutationObserver(schedule).observe(document.documentElement, { subtree: true, childList: true });
68
+ document.addEventListener('input', schedule, true);
69
+ document.addEventListener('change', schedule, true);
70
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', apply, { once: true }); else apply();
71
+ window.NEXOWATT_EOS_STABLE_LOGIN = Object.freeze({ version: VERSION, refresh: apply });
45
72
  })();
@@ -0,0 +1,111 @@
1
+ (() => {
2
+ 'use strict';
3
+ const VERSION = 'v7100-scroll-login-auto-update';
4
+ const MANAGED_USERS = new Set(['installer', 'guest', 'user']);
5
+ const normalizeUser = value => String(value || '').trim().toLowerCase().replace(/^system\.user\./, '');
6
+ const INTRO_STYLE_PROPERTIES = [
7
+ 'position', 'top', 'right', 'bottom', 'left', 'width', 'height', 'min-height', 'max-height',
8
+ 'margin', 'overflow-x', 'overflow-y', 'overscroll-behavior', 'scrollbar-gutter',
9
+ ];
10
+ const isIntroRoute = () => {
11
+ const hash = decodeURIComponent(window.location.hash || '').toLowerCase();
12
+ const explicitTab = hash.match(/(?:^|[#/])(tab-[a-z0-9_-]+)(?:[/?&]|$)/)?.[1] || '';
13
+ if (explicitTab) return explicitTab === 'tab-intro';
14
+ return document.documentElement.classList.contains('eos-route-intro')
15
+ || !hash
16
+ || hash === '#'
17
+ || hash === '#/';
18
+ };
19
+ const bindScrollInput = paper => {
20
+ if (paper.dataset.nexowattScrollBound === 'true') return;
21
+ paper.dataset.nexowattScrollBound = 'true';
22
+ paper.addEventListener('wheel', event => {
23
+ if (!event.deltaY || paper.scrollHeight <= paper.clientHeight + 1) return;
24
+ const before = paper.scrollTop;
25
+ paper.scrollTop += event.deltaY;
26
+ if (paper.scrollTop !== before) event.preventDefault();
27
+ }, { passive: false });
28
+ paper.addEventListener('keydown', event => {
29
+ if (paper.scrollHeight <= paper.clientHeight + 1) return;
30
+ const step = Math.max(120, Math.round(paper.clientHeight * .78));
31
+ if (event.key === 'PageDown') paper.scrollTop += step;
32
+ else if (event.key === 'PageUp') paper.scrollTop -= step;
33
+ else if (event.key === 'Home' && event.ctrlKey) paper.scrollTop = 0;
34
+ else if (event.key === 'End' && event.ctrlKey) paper.scrollTop = paper.scrollHeight;
35
+ else return;
36
+ event.preventDefault();
37
+ });
38
+ };
39
+ const disableCockpitScroll = () => {
40
+ const root = document.documentElement;
41
+ root.classList.remove('eos-intro-scroll-enabled');
42
+ const paper = document.getElementById('app-paper');
43
+ if (!paper || paper.dataset.nexowattScrollable !== 'true') return;
44
+ delete paper.dataset.nexowattScrollable;
45
+ for (const property of INTRO_STYLE_PROPERTIES) paper.style.removeProperty(property);
46
+ };
47
+ const ensureCockpitScroll = () => {
48
+ const root = document.documentElement;
49
+ const paper = document.getElementById('app-paper');
50
+ if (!paper || !root.classList.contains('eos-app')) return;
51
+ if (!isIntroRoute()) {
52
+ disableCockpitScroll();
53
+ return;
54
+ }
55
+ // eos-route-* is owned by the native shell; only add our dedicated scroll flag.
56
+ root.classList.add('eos-intro-scroll-enabled');
57
+ paper.dataset.nexowattScrollable = 'true';
58
+ if (paper.tabIndex < 0) paper.tabIndex = 0;
59
+ const styles = {
60
+ position: 'fixed',
61
+ top: 'var(--nx-content-top, 154px)',
62
+ right: 'var(--nx-page-x, 12px)',
63
+ bottom: '8px',
64
+ left: 'var(--nx-page-x, 12px)',
65
+ width: 'auto',
66
+ height: 'auto',
67
+ minHeight: '0',
68
+ maxHeight: 'none',
69
+ margin: '0',
70
+ overflowX: 'hidden',
71
+ overflowY: 'scroll',
72
+ overscrollBehavior: 'contain',
73
+ scrollbarGutter: 'stable',
74
+ };
75
+ for (const [name, value] of Object.entries(styles)) {
76
+ paper.style.setProperty(name.replace(/[A-Z]/g, char => `-${char.toLowerCase()}`), value, 'important');
77
+ }
78
+ bindScrollInput(paper);
79
+ };
80
+ const apply = () => {
81
+ document.documentElement.classList.add('eos-assist-disabled');
82
+ document.querySelectorAll('.eos-assist-root,.eos-assist-header-root,[data-eos-assist-root],.eos-passwordless-launcher')
83
+ .forEach(element => element.remove());
84
+ const username = document.querySelector('input#username,input[name="username"]');
85
+ const password = document.querySelector('input#password,input[name="password"]');
86
+ const submit = document.querySelector('button[type="submit"],input[type="submit"]');
87
+ if (submit && MANAGED_USERS.has(normalizeUser(username?.value)) && !String(password?.value || '')) {
88
+ submit.disabled = true;
89
+ submit.classList.add('Mui-disabled');
90
+ submit.setAttribute('aria-disabled', 'true');
91
+ }
92
+ ensureCockpitScroll();
93
+ };
94
+ let scheduled = false;
95
+ const schedule = () => {
96
+ if (scheduled) return;
97
+ scheduled = true;
98
+ requestAnimationFrame(() => { scheduled = false; apply(); });
99
+ };
100
+ const start = () => {
101
+ apply();
102
+ new MutationObserver(schedule).observe(document.documentElement, { subtree: true, childList: true });
103
+ document.addEventListener('input', schedule, true);
104
+ document.addEventListener('change', schedule, true);
105
+ window.addEventListener('hashchange', schedule);
106
+ window.addEventListener('resize', schedule, { passive: true });
107
+ window.setInterval(ensureCockpitScroll, 750);
108
+ };
109
+ document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', start, { once: true }) : start();
110
+ window.NEXOWATT_EOS_STABLE_V7100 = Object.freeze({ version: VERSION, refresh: apply, ensureCockpitScroll, disableCockpitScroll });
111
+ })();
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NexoWattStableUpdateManager = void 0;
4
+ exports.isNexoWattRepositoryEntry = isNexoWattRepositoryEntry;
5
+ const ADMIN_USER = 'system.user.admin';
6
+ const POLICY = 'major';
7
+ const RECONCILE_INTERVAL_MS = 6 * 60 * 60 * 1000;
8
+ const REPOSITORY_REFRESH_DELAY_MS = 2500;
9
+ const clone = value => JSON.parse(JSON.stringify(value));
10
+ const normalizedName = value => String(value || '').trim().replace(/^system\.adapter\./, '').replace(/\.\d+$/, '');
11
+ const stableVersion = value => /^\d+\.\d+\.\d+$/.test(String(value || '').trim());
12
+ function isNexoWattRepositoryEntry(name, entry) {
13
+ const adapterName = normalizedName(entry?.name || name).toLowerCase();
14
+ if (adapterName === 'eos-admin' || adapterName.startsWith('nexowatt-') || adapterName.startsWith('eos-'))
15
+ return true;
16
+ const evidence = JSON.stringify({ name, adapterName, title: entry?.title, desc: entry?.desc, author: entry?.author, authors: entry?.authors, publisher: entry?.publisher, url: entry?.url, meta: entry?.meta, readme: entry?.readme, extIcon: entry?.extIcon, licenseInformation: entry?.licenseInformation }).toLowerCase();
17
+ return evidence.includes('nexowatt') || evidence.includes('github.com/nexowatt/');
18
+ }
19
+ class NexoWattStableUpdateManager {
20
+ adapter;
21
+ timer;
22
+ reconcilePromise;
23
+ refreshTimer;
24
+ retryTimer;
25
+ constructor(adapter) { this.adapter = adapter; }
26
+ get instanceId() { return `system.adapter.${this.adapter.namespace}`; }
27
+ async start() { const status = await this.reconcile('startup'); if (!this.timer) { this.timer = setInterval(() => void this.reconcile('interval'), RECONCILE_INTERVAL_MS); this.timer.unref?.(); } return status; }
28
+ stop() { if (this.timer) clearInterval(this.timer); if (this.refreshTimer) clearTimeout(this.refreshTimer); if (this.retryTimer) clearTimeout(this.retryTimer); this.timer = undefined; this.refreshTimer = undefined; this.retryTimer = undefined; }
29
+ async getStatus(refresh = false) { if (refresh) return this.reconcile('status'); const instance = await this.adapter.getForeignObjectAsync(this.instanceId); return this.statusFromNative(instance?.native || {}); }
30
+ async setEnabled(enabled) { await this.adapter.extendForeignObjectAsync(this.instanceId, { native: { eosNexoWattAutoUpdate: enabled } }, { user: ADMIN_USER }); return this.reconcile(enabled ? 'enabled' : 'disabled'); }
31
+ async reconcile(reason = 'manual') { if (this.reconcilePromise) return this.reconcilePromise; this.reconcilePromise = this.reconcileInternal(reason).finally(() => { this.reconcilePromise = undefined; }); return this.reconcilePromise; }
32
+ statusFromNative(native) { const state = native.eosNexoWattAutoUpdateState || {}; return { enabled: native.eosNexoWattAutoUpdate !== false, repository: String(state.selectedRepository || ''), managedAdapters: Array.isArray(state.managedAdapters) ? state.managedAdapters.map(String).sort() : [], availableStableAdapters: Array.isArray(state.availableStableAdapters) ? state.availableStableAdapters.map(String).sort() : [], lastSync: Number(state.lastSync || 0), lastRepositoryRefresh: Number(state.lastRepositoryRefresh || 0), error: String(state.error || ''), policy: POLICY, source: 'ioBroker-native-auto-upgrade' }; }
33
+ async reconcileInternal(reason) { const instance = await this.adapter.getForeignObjectAsync(this.instanceId); const native = instance?.native || {}; const enabled = native.eosNexoWattAutoUpdate !== false; const state = clone(native.eosNexoWattAutoUpdateState || {}); try { if (!enabled) return await this.disable(state, reason); return await this.enable(state, reason); } catch (error) { const message = error instanceof Error ? error.message : String(error || 'unknownError'); state.error = message; state.lastSync = Date.now(); await this.persist(false, state); this.adapter.log.warn(`[NexoWatt stable updates] ${message}`); return this.statusFromNative({ eosNexoWattAutoUpdate: enabled, eosNexoWattAutoUpdateState: state }); } }
34
+ async readInstalledAdapters() { const view = await this.adapter.getObjectViewAsync('system', 'adapter', { startkey: 'system.adapter.', endkey: 'system.adapter.\u9999' }, { user: ADMIN_USER }); const result = new Map(); for (const row of view?.rows || []) { const object = row?.value || row?.doc; const name = normalizedName(object?.common?.name || row?.id || object?._id); if (name && object?.common?.version) result.set(name, object); } return result; }
35
+ async readRepositorySelection(systemConfig) { const repositories = await this.adapter.getForeignObjectAsync('system.repositories'); const rows = repositories?.native?.repositories || {}; const active = Array.isArray(systemConfig?.common?.activeRepo) ? systemConfig.common.activeRepo.map(String) : []; const candidates = Object.entries(rows).map(([name, repo]) => { const entries = repo?.json && typeof repo.json === 'object' ? repo.json : {}; const stableNames = Object.entries(entries).filter(([entryName, entry]) => stableVersion(entry?.version) && isNexoWattRepositoryEntry(entryName, entry)).map(([entryName, entry]) => normalizedName(entry?.name || entryName)).filter(Boolean); return { name, entries, stableNames: [...new Set(stableNames)].sort(), activeIndex: active.indexOf(name) }; }).filter(row => row.stableNames.length > 0); if (!candidates.length) throw new Error('nexowattStableRepositoryNotFound'); candidates.sort((a, b) => { const aa = a.activeIndex < 0 ? Number.MAX_SAFE_INTEGER : a.activeIndex; const bb = b.activeIndex < 0 ? Number.MAX_SAFE_INTEGER : b.activeIndex; return aa - bb || b.stableNames.length - a.stableNames.length || a.name.localeCompare(b.name); }); return candidates[0]; }
36
+ async enable(state, reason) { const systemConfig = await this.adapter.getForeignObjectAsync('system.config'); if (!systemConfig?.common) throw new Error('systemConfigUnavailable'); const selected = await this.readRepositorySelection(systemConfig); const installed = await this.readInstalledAdapters(); const managedNames = selected.stableNames.filter(name => installed.has(name)).sort(); if (!managedNames.length) throw new Error('noInstalledNexoWattStableAdapters'); state.previousPolicies ||= {}; for (const name of managedNames) { if (!(name in state.previousPolicies)) { const current = installed.get(name)?.common?.automaticUpgrade; state.previousPolicies[name] = current == null ? null : current; } await this.writeAdapterPolicy(name, POLICY); } const common = systemConfig.common; const activeRepo = Array.isArray(common.activeRepo) ? [...common.activeRepo] : []; const auto = common.adapterAutoUpgrade && typeof common.adapterAutoUpgrade === 'object' ? clone(common.adapterAutoUpgrade) : { repositories: {}, defaultPolicy: 'none' }; auto.repositories = auto.repositories && typeof auto.repositories === 'object' ? { ...auto.repositories } : {}; if (state.previousRepositoryEnabled === undefined) state.previousRepositoryEnabled = Object.prototype.hasOwnProperty.call(auto.repositories, selected.name) ? Boolean(auto.repositories[selected.name]) : null; auto.repositories[selected.name] = true; if (!auto.defaultPolicy) auto.defaultPolicy = 'none'; common.adapterAutoUpgrade = auto; if (activeRepo[0] !== selected.name) { if (!state.previousActiveRepo) state.previousActiveRepo = [...activeRepo]; state.ownsActiveRepoOrder = true; common.activeRepo = [selected.name, ...activeRepo.filter(name => name !== selected.name)]; } await this.adapter.setForeignObjectAsync('system.config', systemConfig, { user: ADMIN_USER }); state.selectedRepository = selected.name; state.availableStableAdapters = selected.stableNames; state.managedAdapters = managedNames; state.lastSync = Date.now(); state.error = ''; await this.persist(true, state); this.scheduleRepositoryRefresh(selected.name, state); this.adapter.log.info(`[NexoWatt stable updates] Enabled for ${managedNames.join(', ')} from repository "${selected.name}" (${reason})`); return this.statusFromNative({ eosNexoWattAutoUpdate: true, eosNexoWattAutoUpdateState: state }); }
37
+ async disable(state, reason) { for (const [name, previous] of Object.entries(state.previousPolicies || {})) { const current = await this.adapter.getForeignObjectAsync(`system.adapter.${normalizedName(name)}`); if (current?.common?.automaticUpgrade === POLICY) await this.writeAdapterPolicy(name, previous); } const systemConfig = await this.adapter.getForeignObjectAsync('system.config'); if (systemConfig?.common) { const auto = systemConfig.common.adapterAutoUpgrade && typeof systemConfig.common.adapterAutoUpgrade === 'object' ? clone(systemConfig.common.adapterAutoUpgrade) : null; if (auto && state.selectedRepository) { auto.repositories = auto.repositories && typeof auto.repositories === 'object' ? { ...auto.repositories } : {}; if (auto.repositories[state.selectedRepository] === true) { if (state.previousRepositoryEnabled === null || state.previousRepositoryEnabled === undefined) delete auto.repositories[state.selectedRepository]; else auto.repositories[state.selectedRepository] = state.previousRepositoryEnabled; } systemConfig.common.adapterAutoUpgrade = auto; } if (state.ownsActiveRepoOrder && Array.isArray(state.previousActiveRepo) && Array.isArray(systemConfig.common.activeRepo) && systemConfig.common.activeRepo[0] === state.selectedRepository) systemConfig.common.activeRepo = [...state.previousActiveRepo]; await this.adapter.setForeignObjectAsync('system.config', systemConfig, { user: ADMIN_USER }); } state.previousPolicies = {}; state.previousRepositoryEnabled = undefined; state.previousActiveRepo = undefined; state.ownsActiveRepoOrder = false; state.managedAdapters = []; state.lastSync = Date.now(); state.error = ''; await this.persist(false, state); this.adapter.log.info(`[NexoWatt stable updates] Disabled and previous policies restored (${reason})`); return this.statusFromNative({ eosNexoWattAutoUpdate: false, eosNexoWattAutoUpdateState: state }); }
38
+ async writeAdapterPolicy(name, policy) { const id = `system.adapter.${normalizedName(name)}`; const object = await this.adapter.getForeignObjectAsync(id); if (!object?.common) return; if (policy === null) delete object.common.automaticUpgrade; else object.common.automaticUpgrade = policy; await this.adapter.setForeignObjectAsync(id, object, { user: ADMIN_USER }); }
39
+ async persist(enabled, state) { await this.adapter.extendForeignObjectAsync(this.instanceId, { native: { eosNexoWattAutoUpdate: enabled, eosNexoWattAutoUpdateState: state } }, { user: ADMIN_USER }); }
40
+ scheduleRetry() { if (this.retryTimer) clearTimeout(this.retryTimer); this.retryTimer = setTimeout(() => void this.reconcile('retry'), 60000); this.retryTimer.unref?.(); }
41
+ scheduleRepositoryRefresh(repository, state) { if (!this.adapter.sendToHostAsync || !this.adapter.host) return; if (this.refreshTimer) clearTimeout(this.refreshTimer); this.refreshTimer = setTimeout(() => { void this.adapter.sendToHostAsync?.(this.adapter.host, 'getRepository', { repo: repository, update: true }).then(async () => { const current = await this.adapter.getForeignObjectAsync(this.instanceId); if (current?.native?.eosNexoWattAutoUpdate === false) return; state.lastRepositoryRefresh = Date.now(); await this.persist(true, state); }).catch(error => this.adapter.log.warn(`[NexoWatt stable updates] Repository refresh failed: ${error instanceof Error ? error.message : error}`)); }, REPOSITORY_REFRESH_DELAY_MS); this.refreshTimer.unref?.(); }
42
+ }
43
+ exports.NexoWattStableUpdateManager = NexoWattStableUpdateManager;
@@ -2,61 +2,71 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.normalizeEosPasswordTarget = normalizeEosPasswordTarget;
4
4
  exports.setEosUserPasswordWithVerification = setEosUserPasswordWithVerification;
5
+ exports.verifyEosUserPasswordCredential = verifyEosUserPasswordCredential;
5
6
  function normalizeEosPasswordTarget(value) {
6
7
  const raw = String(value || '').trim();
7
8
  const userId = raw.startsWith('system.user.') ? raw : `system.user.${raw}`;
8
9
  const userName = userId.replace(/^system\.user\./, '').trim();
9
- if (!userName || userName.includes('.') || !/^system\.user\.[^.]+$/.test(userId)) {
10
- throw new Error('invalidPasswordTarget');
11
- }
10
+ if (!userName || userName.includes('.') || !/^system\.user\.[^.]+$/.test(userId)) throw new Error('invalidPasswordTarget');
12
11
  return { userId, userName };
13
12
  }
14
- async function callPasswordApi(adapter, userName, password, serviceUser) {
15
- const options = { user: serviceUser };
16
- if (typeof adapter.setPasswordAsync === 'function') {
17
- await adapter.setPasswordAsync(userName, password, options);
18
- return;
13
+ const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
14
+ const accepted = result => result === true || (Array.isArray(result) && result[0] === true);
15
+ async function readPasswordHash(adapter, userId) {
16
+ if (typeof adapter.getForeignObjectAsync !== 'function') return '';
17
+ try {
18
+ const object = await adapter.getForeignObjectAsync(userId);
19
+ return typeof object?.common?.password === 'string' ? object.common.password : '';
19
20
  }
21
+ catch { return ''; }
22
+ }
23
+ async function callPasswordApi(adapter, target, password, serviceUser, withOptions) {
24
+ const options = withOptions ? { user: serviceUser } : null;
25
+ if (typeof adapter.setPasswordAsync === 'function') { await adapter.setPasswordAsync(target, password, options); return; }
20
26
  if (typeof adapter.setPassword === 'function') {
21
- await new Promise((resolve, reject) => {
22
- adapter.setPassword?.(userName, password, options, error => (error ? reject(error) : resolve()));
23
- });
27
+ await new Promise((resolve, reject) => adapter.setPassword?.(target, password, options, error => error ? reject(error) : resolve()));
24
28
  return;
25
29
  }
26
30
  throw new Error('passwordApiUnavailable');
27
31
  }
28
- async function waitForPasswordHash(adapter, userId) {
29
- for (let attempt = 0; attempt < 6; attempt++) {
30
- const user = await adapter.getForeignObjectAsync(userId);
31
- if (typeof user?.common?.password === 'string' && user.common.password.length > 0) {
32
- return;
33
- }
34
- if (attempt < 5) {
35
- await new Promise(resolve => setTimeout(resolve, 40 * (attempt + 1)));
32
+ async function verifyAfterWrite(adapter, normalized, password, serviceUser) {
33
+ const hasCredentialCheck = typeof adapter.checkPasswordAsync === 'function';
34
+ for (let attempt = 0; attempt < 12; attempt++) {
35
+ if (hasCredentialCheck) {
36
+ for (const candidate of [normalized.userId, normalized.userName]) {
37
+ try {
38
+ if (accepted(await adapter.checkPasswordAsync?.(candidate, password, { user: serviceUser }))) return;
39
+ }
40
+ catch { }
41
+ }
36
42
  }
43
+ else if (await readPasswordHash(adapter, normalized.userId)) return;
44
+ if (attempt < 11) await delay(55 + attempt * 35);
37
45
  }
38
- throw new Error('passwordNotPersisted');
46
+ throw new Error(hasCredentialCheck ? 'passwordVerificationFailed' : 'passwordNotPersisted');
39
47
  }
40
- async function verifyCredential(adapter, userName, password, serviceUser) {
41
- if (typeof adapter.checkPasswordAsync !== 'function') {
42
- return;
43
- }
44
- for (let attempt = 0; attempt < 5; attempt++) {
45
- const result = await adapter.checkPasswordAsync(userName, password, { user: serviceUser });
46
- const accepted = result === true || (Array.isArray(result) && result[0] === true);
47
- if (accepted) {
48
- return;
49
- }
50
- if (attempt < 4) {
51
- await new Promise(resolve => setTimeout(resolve, 50 * (attempt + 1)));
52
- }
53
- }
54
- throw new Error('passwordVerificationFailed');
48
+ async function verifyEosUserPasswordCredential(adapter, target, password, serviceUser = 'system.user.admin') {
49
+ const normalized = normalizeEosPasswordTarget(target);
50
+ await verifyAfterWrite(adapter, normalized, password, serviceUser);
51
+ return normalized;
55
52
  }
56
53
  async function setEosUserPasswordWithVerification(adapter, target, password, serviceUser = 'system.user.admin') {
57
54
  const normalized = normalizeEosPasswordTarget(target);
58
- await callPasswordApi(adapter, normalized.userName, password, serviceUser);
59
- await waitForPasswordHash(adapter, normalized.userId);
60
- await verifyCredential(adapter, normalized.userName, password, serviceUser);
61
- return normalized;
55
+ const attempts = [
56
+ { apiTarget: normalized.userId, withOptions: true },
57
+ { apiTarget: normalized.userName, withOptions: true },
58
+ { apiTarget: normalized.userId, withOptions: false },
59
+ { apiTarget: normalized.userName, withOptions: false },
60
+ ];
61
+ let lastError = null;
62
+ for (const attempt of attempts) {
63
+ try {
64
+ await callPasswordApi(adapter, attempt.apiTarget, password, serviceUser, attempt.withOptions);
65
+ await verifyAfterWrite(adapter, normalized, password, serviceUser);
66
+ return { ...normalized, apiTarget: attempt.apiTarget };
67
+ }
68
+ catch (error) { lastError = error; }
69
+ }
70
+ if (lastError instanceof Error) throw lastError;
71
+ throw new Error('passwordNotPersisted');
62
72
  }