@riddix/hamh 2.1.0-alpha.755 → 2.1.0-alpha.757

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.
@@ -124071,6 +124071,14 @@ function computeControllerWarnings(controllers, exposed) {
124071
124071
  }
124072
124072
  return warnings;
124073
124073
  }
124074
+ function controllerWarningsForFabrics(fabrics, exposed) {
124075
+ const controllers = [
124076
+ ...new Set(fabrics.map((f) => classifyController(f.rootVendorId)).filter((c) => c !== void 0))
124077
+ ];
124078
+ if (controllers.length === 0)
124079
+ return [];
124080
+ return computeControllerWarnings(controllers, exposed);
124081
+ }
124074
124082
  var controllerByVendorId, deviceTypeIdSupport, controllerLabels;
124075
124083
  var init_controller_compat = __esm({
124076
124084
  "../common/dist/controller-compat.js"() {
@@ -124622,7 +124630,8 @@ function buildEntityDiagnostics(exposed, failed, warnings) {
124622
124630
  const result = failed.map((f) => ({
124623
124631
  entityId: f.entityId,
124624
124632
  status: "failed",
124625
- reason: f.reason
124633
+ reason: f.reason,
124634
+ failedAt: f.failedAt
124626
124635
  }));
124627
124636
  const failedIds = new Set(failed.map((f) => f.entityId));
124628
124637
  const issuesByEntity = /* @__PURE__ */ new Map();
@@ -127121,12 +127130,7 @@ function healthApi(bridgeService, haClient, version2, startTime) {
127121
127130
  const fabrics = data.commissioning?.fabrics ?? [];
127122
127131
  const sessionInfo = b.getSessionInfo();
127123
127132
  const exposed = b.getExposedDeviceTypes();
127124
- const controllers = [
127125
- ...new Set(
127126
- fabrics.map((f) => classifyController(f.rootVendorId)).filter((c) => c !== void 0)
127127
- )
127128
- ];
127129
- const controllerWarnings = controllers.length > 0 ? computeControllerWarnings(controllers, exposed) : [];
127133
+ const controllerWarnings = data.controllerWarnings ?? [];
127130
127134
  const entityDiagnostics = buildEntityDiagnostics(
127131
127135
  exposed,
127132
127136
  data.failedEntities ?? [],
@@ -127166,7 +127170,8 @@ function healthApi(bridgeService, haClient, version2, startTime) {
127166
127170
  recovery: {
127167
127171
  enabled: bridgeService.autoRecoveryEnabled,
127168
127172
  lastRecoveryAttempt: bridgeService.lastRecoveryAttempt?.toISOString(),
127169
- recoveryCount: bridgeService.recoveryCount
127173
+ recoveryCount: bridgeService.recoveryCount,
127174
+ history: bridgeService.recoveryHistory
127170
127175
  }
127171
127176
  };
127172
127177
  res.json(detailed);
@@ -128353,15 +128358,13 @@ function metricsApi(bridgeService, haClient, haRegistry, startTime) {
128353
128358
  router.get("/", (_, res) => {
128354
128359
  const memoryUsage = process.memoryUsage();
128355
128360
  const bridges = bridgeService.bridges;
128356
- const running = bridges.filter((b) => b.data.status === "running").length;
128357
- const stopped = bridges.filter((b) => b.data.status === "stopped").length;
128358
- const failed = bridges.filter((b) => b.data.status === "failed").length;
128359
- const totalDevices = bridges.reduce(
128360
- (sum, b) => sum + b.data.deviceCount,
128361
- 0
128362
- );
128363
- const totalFabrics = bridges.reduce(
128364
- (sum, b) => sum + (b.data.commissioning?.fabrics?.length ?? 0),
128361
+ const datas = bridges.map((b) => b.data);
128362
+ const running = datas.filter((d) => d.status === "running").length;
128363
+ const stopped = datas.filter((d) => d.status === "stopped").length;
128364
+ const failed = datas.filter((d) => d.status === "failed").length;
128365
+ const totalDevices = datas.reduce((sum, d) => sum + d.deviceCount, 0);
128366
+ const totalFabrics = datas.reduce(
128367
+ (sum, d) => sum + (d.commissioning?.fabrics?.length ?? 0),
128365
128368
  0
128366
128369
  );
128367
128370
  const metrics = {
@@ -128393,15 +128396,13 @@ function metricsApi(bridgeService, haClient, haRegistry, startTime) {
128393
128396
  const memoryUsage = process.memoryUsage();
128394
128397
  const bridges = bridgeService.bridges;
128395
128398
  const uptime2 = Math.floor((Date.now() - startTime) / 1e3);
128396
- const running = bridges.filter((b) => b.data.status === "running").length;
128397
- const stopped = bridges.filter((b) => b.data.status === "stopped").length;
128398
- const failed = bridges.filter((b) => b.data.status === "failed").length;
128399
- const totalDevices = bridges.reduce(
128400
- (sum, b) => sum + b.data.deviceCount,
128401
- 0
128402
- );
128403
- const totalFabrics = bridges.reduce(
128404
- (sum, b) => sum + (b.data.commissioning?.fabrics?.length ?? 0),
128399
+ const datas = bridges.map((b) => b.data);
128400
+ const running = datas.filter((d) => d.status === "running").length;
128401
+ const stopped = datas.filter((d) => d.status === "stopped").length;
128402
+ const failed = datas.filter((d) => d.status === "failed").length;
128403
+ const totalDevices = datas.reduce((sum, d) => sum + d.deviceCount, 0);
128404
+ const totalFabrics = datas.reduce(
128405
+ (sum, d) => sum + (d.commissioning?.fabrics?.length ?? 0),
128405
128406
  0
128406
128407
  );
128407
128408
  const lines = [
@@ -128459,8 +128460,9 @@ function metricsApi(bridgeService, haClient, haRegistry, startTime) {
128459
128460
  ""
128460
128461
  ];
128461
128462
  for (const bridge of bridges) {
128462
- const status3 = bridge.data.status === "running" ? 1 : 0;
128463
- const safeName = bridge.data.name.replace(/[^a-zA-Z0-9_]/g, "_");
128463
+ const data = bridge.data;
128464
+ const status3 = data.status === "running" ? 1 : 0;
128465
+ const safeName = data.name.replace(/[^a-zA-Z0-9_]/g, "_");
128464
128466
  lines.push(
128465
128467
  `# HELP hamh_bridge_status Bridge status (1=running, 0=not running)`,
128466
128468
  `# TYPE hamh_bridge_status gauge`,
@@ -128468,7 +128470,7 @@ function metricsApi(bridgeService, haClient, haRegistry, startTime) {
128468
128470
  "",
128469
128471
  `# HELP hamh_bridge_devices Number of devices on bridge`,
128470
128472
  `# TYPE hamh_bridge_devices gauge`,
128471
- `hamh_bridge_devices{bridge_id="${bridge.id}",bridge_name="${safeName}"} ${bridge.data.deviceCount}`,
128473
+ `hamh_bridge_devices{bridge_id="${bridge.id}",bridge_name="${safeName}"} ${data.deviceCount}`,
128472
128474
  ""
128473
128475
  );
128474
128476
  }
@@ -129336,7 +129338,177 @@ function buildPath(...paths) {
129336
129338
 
129337
129339
  // src/api/settings-api.ts
129338
129340
  import { Router as Router2 } from "express";
129339
- function settingsApi(settingsStorage, envAuth) {
129341
+
129342
+ // src/services/storage/app-settings-storage.ts
129343
+ init_service();
129344
+ import { randomBytes as randomBytes2, scryptSync, timingSafeEqual } from "node:crypto";
129345
+ var SCRYPT_KEYLEN = 64;
129346
+ var SCRYPT_COST_N = 16384;
129347
+ var SCRYPT_BLOCK_R = 8;
129348
+ var SCRYPT_PARALLEL_P = 1;
129349
+ var SALT_BYTES = 16;
129350
+ var DEFAULT_BACKUP_SETTINGS = {
129351
+ autoBackup: true,
129352
+ backupRetentionCount: 5
129353
+ };
129354
+ var MIN_RECOVERY_INTERVAL_MS = 1e4;
129355
+ var MAX_RECOVERY_INTERVAL_MS = 36e5;
129356
+ var DEFAULT_RECOVERY_SETTINGS = {
129357
+ autoRecoveryEnabled: true,
129358
+ recoveryIntervalMs: 6e4
129359
+ };
129360
+ function hashPassword(plain2) {
129361
+ const salt = randomBytes2(SALT_BYTES);
129362
+ const derived = scryptSync(plain2, salt, SCRYPT_KEYLEN, {
129363
+ N: SCRYPT_COST_N,
129364
+ r: SCRYPT_BLOCK_R,
129365
+ p: SCRYPT_PARALLEL_P
129366
+ });
129367
+ return {
129368
+ passwordHash: derived.toString("hex"),
129369
+ passwordSalt: salt.toString("hex")
129370
+ };
129371
+ }
129372
+ function verifyPasswordHash(plain2, passwordHash, passwordSalt) {
129373
+ let expected;
129374
+ let salt;
129375
+ try {
129376
+ expected = Buffer.from(passwordHash, "hex");
129377
+ salt = Buffer.from(passwordSalt, "hex");
129378
+ } catch {
129379
+ return false;
129380
+ }
129381
+ if (expected.length !== SCRYPT_KEYLEN || salt.length === 0) {
129382
+ return false;
129383
+ }
129384
+ const derived = scryptSync(plain2, salt, SCRYPT_KEYLEN, {
129385
+ N: SCRYPT_COST_N,
129386
+ r: SCRYPT_BLOCK_R,
129387
+ p: SCRYPT_PARALLEL_P
129388
+ });
129389
+ return timingSafeEqual(derived, expected);
129390
+ }
129391
+ function constantTimeStringEquals(a, b) {
129392
+ const bufA = Buffer.from(a);
129393
+ const bufB = Buffer.from(b);
129394
+ if (bufA.length !== bufB.length) {
129395
+ timingSafeEqual(bufA, bufA);
129396
+ return false;
129397
+ }
129398
+ return timingSafeEqual(bufA, bufB);
129399
+ }
129400
+ var AppSettingsStorage = class extends Service {
129401
+ constructor(appStorage) {
129402
+ super("AppSettingsStorage");
129403
+ this.appStorage = appStorage;
129404
+ }
129405
+ appStorage;
129406
+ storage;
129407
+ settings = {};
129408
+ // Caches the last plaintext password that was successfully verified
129409
+ // against the stored hash, keyed by username. This lets repeated basic-auth
129410
+ // calls avoid re-running scrypt on every HTTP request without persisting
129411
+ // the plaintext to disk.
129412
+ verifiedPassword = null;
129413
+ async initialize() {
129414
+ this.storage = this.appStorage.createContext("settings");
129415
+ const stored = await this.storage.get(
129416
+ "data",
129417
+ {}
129418
+ );
129419
+ this.settings = stored ?? {};
129420
+ }
129421
+ get auth() {
129422
+ if (!this.settings.auth) {
129423
+ return void 0;
129424
+ }
129425
+ return { username: this.settings.auth.username };
129426
+ }
129427
+ async setAuth(credentials) {
129428
+ if (!credentials) {
129429
+ this.settings.auth = void 0;
129430
+ this.verifiedPassword = null;
129431
+ } else {
129432
+ const { passwordHash, passwordSalt } = hashPassword(credentials.password);
129433
+ this.settings.auth = {
129434
+ username: credentials.username,
129435
+ passwordHash,
129436
+ passwordSalt
129437
+ };
129438
+ this.verifiedPassword = {
129439
+ username: credentials.username,
129440
+ password: credentials.password
129441
+ };
129442
+ }
129443
+ await this.persist();
129444
+ }
129445
+ verifyAuth(username, password) {
129446
+ const stored = this.settings.auth;
129447
+ if (!stored) {
129448
+ return false;
129449
+ }
129450
+ if (!constantTimeStringEquals(username, stored.username)) {
129451
+ return false;
129452
+ }
129453
+ if (this.verifiedPassword && this.verifiedPassword.username === stored.username && constantTimeStringEquals(password, this.verifiedPassword.password)) {
129454
+ return true;
129455
+ }
129456
+ if (stored.password !== void 0) {
129457
+ const matches = constantTimeStringEquals(password, stored.password);
129458
+ if (matches) {
129459
+ this.verifiedPassword = { username: stored.username, password };
129460
+ void this.migrateLegacyPlaintext(password);
129461
+ }
129462
+ return matches;
129463
+ }
129464
+ if (stored.passwordHash && stored.passwordSalt) {
129465
+ if (verifyPasswordHash(password, stored.passwordHash, stored.passwordSalt)) {
129466
+ this.verifiedPassword = { username: stored.username, password };
129467
+ return true;
129468
+ }
129469
+ }
129470
+ return false;
129471
+ }
129472
+ async migrateLegacyPlaintext(plain2) {
129473
+ const current = this.settings.auth;
129474
+ if (!current || current.password === void 0) {
129475
+ return;
129476
+ }
129477
+ const { passwordHash, passwordSalt } = hashPassword(plain2);
129478
+ this.settings.auth = {
129479
+ username: current.username,
129480
+ passwordHash,
129481
+ passwordSalt
129482
+ };
129483
+ try {
129484
+ await this.persist();
129485
+ } catch {
129486
+ }
129487
+ }
129488
+ get backupSettings() {
129489
+ return { ...DEFAULT_BACKUP_SETTINGS, ...this.settings.backup };
129490
+ }
129491
+ async setBackupSettings(settings) {
129492
+ this.settings.backup = { ...this.settings.backup, ...settings };
129493
+ await this.persist();
129494
+ }
129495
+ get recoverySettings() {
129496
+ return { ...DEFAULT_RECOVERY_SETTINGS, ...this.settings.recovery };
129497
+ }
129498
+ async setRecoverySettings(settings) {
129499
+ this.settings.recovery = { ...this.settings.recovery, ...settings };
129500
+ await this.persist();
129501
+ }
129502
+ async persist() {
129503
+ await this.storage.set(
129504
+ "data",
129505
+ this.settings
129506
+ );
129507
+ }
129508
+ };
129509
+
129510
+ // src/api/settings-api.ts
129511
+ function settingsApi(settingsStorage, bridgeService, envAuth) {
129340
129512
  const router = Router2();
129341
129513
  router.get("/auth", (_req, res) => {
129342
129514
  if (envAuth) {
@@ -129393,6 +129565,31 @@ function settingsApi(settingsStorage, envAuth) {
129393
129565
  res.json({ enabled: false, source: "none" });
129394
129566
  }
129395
129567
  );
129568
+ router.get("/recovery", (_req, res) => {
129569
+ res.json(settingsStorage.recoverySettings);
129570
+ });
129571
+ router.put(
129572
+ "/recovery",
129573
+ async (req, res) => {
129574
+ const next = { ...settingsStorage.recoverySettings };
129575
+ const { autoRecoveryEnabled, recoveryIntervalMs } = req.body;
129576
+ if (typeof autoRecoveryEnabled === "boolean") {
129577
+ next.autoRecoveryEnabled = autoRecoveryEnabled;
129578
+ }
129579
+ if (typeof recoveryIntervalMs === "number") {
129580
+ if (recoveryIntervalMs < MIN_RECOVERY_INTERVAL_MS || recoveryIntervalMs > MAX_RECOVERY_INTERVAL_MS) {
129581
+ res.status(400).json({
129582
+ error: `recoveryIntervalMs must be between ${MIN_RECOVERY_INTERVAL_MS} and ${MAX_RECOVERY_INTERVAL_MS}`
129583
+ });
129584
+ return;
129585
+ }
129586
+ next.recoveryIntervalMs = recoveryIntervalMs;
129587
+ }
129588
+ await settingsStorage.setRecoverySettings(next);
129589
+ bridgeService.applyRecoverySettings(next);
129590
+ res.json(next);
129591
+ }
129592
+ );
129396
129593
  return router;
129397
129594
  }
129398
129595
 
@@ -129824,7 +130021,10 @@ var WebApi = class extends Service {
129824
130021
  ).use("/bridges", bridgeExportApi(this.bridgeStorage)).use("/bridge-icons", bridgeIconApi(this.props.storageLocation)).use(
129825
130022
  "/device-images",
129826
130023
  deviceImageApi(this.props.storageLocation, this.haRegistry)
129827
- ).use("/entity-mappings", entityMappingApi(this.mappingStorage)).use("/mapping-profiles", mappingProfileApi(this.mappingStorage)).use("/lock-credentials", lockCredentialApi(this.lockCredentialStorage)).use("/settings", settingsApi(this.settingsStorage, this.props.auth)).use(
130024
+ ).use("/entity-mappings", entityMappingApi(this.mappingStorage)).use("/mapping-profiles", mappingProfileApi(this.mappingStorage)).use("/lock-credentials", lockCredentialApi(this.lockCredentialStorage)).use(
130025
+ "/settings",
130026
+ settingsApi(this.settingsStorage, this.bridgeService, this.props.auth)
130027
+ ).use(
129828
130028
  "/backup",
129829
130029
  backupApi(
129830
130030
  this.bridgeStorage,
@@ -130611,13 +130811,23 @@ var BridgeFactory = class extends Service {
130611
130811
  init_esm();
130612
130812
  init_service();
130613
130813
  import crypto3 from "node:crypto";
130614
- var BridgeService = class extends Service {
130814
+ function clampRecoveryInterval(ms) {
130815
+ if (!Number.isFinite(ms)) return 6e4;
130816
+ return Math.min(
130817
+ Math.max(ms, MIN_RECOVERY_INTERVAL_MS),
130818
+ MAX_RECOVERY_INTERVAL_MS
130819
+ );
130820
+ }
130821
+ var BridgeService = class _BridgeService extends Service {
130615
130822
  constructor(bridgeStorage, bridgeFactory, props) {
130616
130823
  super("BridgeService");
130617
130824
  this.bridgeStorage = bridgeStorage;
130618
130825
  this.bridgeFactory = bridgeFactory;
130619
130826
  this.props = props;
130620
130827
  this.autoRecoveryEnabled = props.autoRecovery ?? true;
130828
+ this.recoveryIntervalMs = clampRecoveryInterval(
130829
+ props.recoveryIntervalMs ?? 6e4
130830
+ );
130621
130831
  }
130622
130832
  bridgeStorage;
130623
130833
  bridgeFactory;
@@ -130631,6 +130841,12 @@ var BridgeService = class extends Service {
130631
130841
  // Set by the caller (e.g. start-handler) to broadcast updates via WebSocket.
130632
130842
  onBridgeChanged;
130633
130843
  recoveryInterval;
130844
+ recoveryIntervalMs = 6e4;
130845
+ recoveryInProgress = false;
130846
+ _recoveryHistory = [];
130847
+ lastReactiveRecovery = 0;
130848
+ static MAX_RECOVERY_HISTORY = 20;
130849
+ static REACTIVE_DEBOUNCE_MS = 5e3;
130634
130850
  async initialize() {
130635
130851
  for (const data of this.bridgeStorage.bridges) {
130636
130852
  const normalized = this.normalizeBridgeData(data);
@@ -130653,24 +130869,80 @@ var BridgeService = class extends Service {
130653
130869
  };
130654
130870
  }
130655
130871
  startAutoRecovery() {
130656
- const intervalMs = this.props.recoveryIntervalMs ?? 6e4;
130657
130872
  this.recoveryInterval = setInterval(() => {
130658
130873
  this.attemptRecovery();
130659
- }, intervalMs);
130874
+ }, this.recoveryIntervalMs);
130875
+ }
130876
+ stopAutoRecovery() {
130877
+ if (this.recoveryInterval) {
130878
+ clearInterval(this.recoveryInterval);
130879
+ this.recoveryInterval = void 0;
130880
+ }
130881
+ }
130882
+ get recoverySettings() {
130883
+ return {
130884
+ autoRecoveryEnabled: this.autoRecoveryEnabled,
130885
+ recoveryIntervalMs: this.recoveryIntervalMs
130886
+ };
130887
+ }
130888
+ get recoveryHistory() {
130889
+ return [...this._recoveryHistory];
130890
+ }
130891
+ // Apply persisted recovery settings (at startup) or a change from the UI.
130892
+ applyRecoverySettings(settings) {
130893
+ this.autoRecoveryEnabled = settings.autoRecoveryEnabled;
130894
+ this.recoveryIntervalMs = clampRecoveryInterval(
130895
+ settings.recoveryIntervalMs
130896
+ );
130897
+ this.stopAutoRecovery();
130898
+ if (this.autoRecoveryEnabled) {
130899
+ this.startAutoRecovery();
130900
+ }
130901
+ }
130902
+ // Run a recovery pass right away, e.g. just after Home Assistant reconnects,
130903
+ // instead of waiting for the next interval. Debounced so a burst of reconnect
130904
+ // events only triggers one pass. Still only touches failed bridges.
130905
+ recoverFailedBridgesNow() {
130906
+ if (!this.autoRecoveryEnabled) return;
130907
+ const now = Date.now();
130908
+ if (now - this.lastReactiveRecovery < _BridgeService.REACTIVE_DEBOUNCE_MS) {
130909
+ return;
130910
+ }
130911
+ this.lastReactiveRecovery = now;
130912
+ void this.attemptRecovery();
130660
130913
  }
130661
130914
  async attemptRecovery() {
130915
+ if (this.recoveryInProgress) return;
130662
130916
  const failedBridges = this.bridges.filter(
130663
130917
  (b) => b.data.status === "failed"
130664
130918
  );
130665
130919
  if (failedBridges.length === 0) return;
130920
+ this.recoveryInProgress = true;
130666
130921
  this.lastRecoveryAttempt = /* @__PURE__ */ new Date();
130667
- for (const bridge of failedBridges) {
130668
- try {
130669
- await bridge.start();
130670
- this.recoveryCount++;
130671
- } catch (e) {
130672
- this.log.warn(`Recovery attempt failed for bridge ${bridge.id}:`, e);
130922
+ try {
130923
+ for (const bridge of failedBridges) {
130924
+ try {
130925
+ await bridge.start();
130926
+ this.recoveryCount++;
130927
+ this.recordRecovery(bridge, "success");
130928
+ } catch (e) {
130929
+ this.log.warn(`Recovery attempt failed for bridge ${bridge.id}:`, e);
130930
+ this.recordRecovery(bridge, "failed");
130931
+ }
130673
130932
  }
130933
+ } finally {
130934
+ this.recoveryInProgress = false;
130935
+ }
130936
+ }
130937
+ recordRecovery(bridge, outcome) {
130938
+ this._recoveryHistory.push({
130939
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
130940
+ bridgeId: bridge.id,
130941
+ bridgeName: bridge.data.name,
130942
+ outcome
130943
+ });
130944
+ if (this._recoveryHistory.length > _BridgeService.MAX_RECOVERY_HISTORY) {
130945
+ this._recoveryHistory.shift();
130674
130946
  }
130675
130947
  }
130676
130948
  async restartBridge(bridgeId) {
@@ -130681,9 +130953,7 @@ var BridgeService = class extends Service {
130681
130953
  return true;
130682
130954
  }
130683
130955
  async dispose() {
130684
- if (this.recoveryInterval) {
130685
- clearInterval(this.recoveryInterval);
130686
- }
130956
+ this.stopAutoRecovery();
130687
130957
  await Promise.all(this.bridges.map((bridge) => bridge.dispose()));
130688
130958
  }
130689
130959
  getNextAvailablePort(startPort = 5540) {
@@ -131259,161 +131529,6 @@ function mockDeviceId(entityId) {
131259
131529
  return `e__${hash2}`;
131260
131530
  }
131261
131531
 
131262
- // src/services/storage/app-settings-storage.ts
131263
- init_service();
131264
- import { randomBytes as randomBytes2, scryptSync, timingSafeEqual } from "node:crypto";
131265
- var SCRYPT_KEYLEN = 64;
131266
- var SCRYPT_COST_N = 16384;
131267
- var SCRYPT_BLOCK_R = 8;
131268
- var SCRYPT_PARALLEL_P = 1;
131269
- var SALT_BYTES = 16;
131270
- var DEFAULT_BACKUP_SETTINGS = {
131271
- autoBackup: true,
131272
- backupRetentionCount: 5
131273
- };
131274
- function hashPassword(plain2) {
131275
- const salt = randomBytes2(SALT_BYTES);
131276
- const derived = scryptSync(plain2, salt, SCRYPT_KEYLEN, {
131277
- N: SCRYPT_COST_N,
131278
- r: SCRYPT_BLOCK_R,
131279
- p: SCRYPT_PARALLEL_P
131280
- });
131281
- return {
131282
- passwordHash: derived.toString("hex"),
131283
- passwordSalt: salt.toString("hex")
131284
- };
131285
- }
131286
- function verifyPasswordHash(plain2, passwordHash, passwordSalt) {
131287
- let expected;
131288
- let salt;
131289
- try {
131290
- expected = Buffer.from(passwordHash, "hex");
131291
- salt = Buffer.from(passwordSalt, "hex");
131292
- } catch {
131293
- return false;
131294
- }
131295
- if (expected.length !== SCRYPT_KEYLEN || salt.length === 0) {
131296
- return false;
131297
- }
131298
- const derived = scryptSync(plain2, salt, SCRYPT_KEYLEN, {
131299
- N: SCRYPT_COST_N,
131300
- r: SCRYPT_BLOCK_R,
131301
- p: SCRYPT_PARALLEL_P
131302
- });
131303
- return timingSafeEqual(derived, expected);
131304
- }
131305
- function constantTimeStringEquals(a, b) {
131306
- const bufA = Buffer.from(a);
131307
- const bufB = Buffer.from(b);
131308
- if (bufA.length !== bufB.length) {
131309
- timingSafeEqual(bufA, bufA);
131310
- return false;
131311
- }
131312
- return timingSafeEqual(bufA, bufB);
131313
- }
131314
- var AppSettingsStorage = class extends Service {
131315
- constructor(appStorage) {
131316
- super("AppSettingsStorage");
131317
- this.appStorage = appStorage;
131318
- }
131319
- appStorage;
131320
- storage;
131321
- settings = {};
131322
- // Caches the last plaintext password that was successfully verified
131323
- // against the stored hash, keyed by username. This lets repeated basic-auth
131324
- // calls avoid re-running scrypt on every HTTP request without persisting
131325
- // the plaintext to disk.
131326
- verifiedPassword = null;
131327
- async initialize() {
131328
- this.storage = this.appStorage.createContext("settings");
131329
- const stored = await this.storage.get(
131330
- "data",
131331
- {}
131332
- );
131333
- this.settings = stored ?? {};
131334
- }
131335
- get auth() {
131336
- if (!this.settings.auth) {
131337
- return void 0;
131338
- }
131339
- return { username: this.settings.auth.username };
131340
- }
131341
- async setAuth(credentials) {
131342
- if (!credentials) {
131343
- this.settings.auth = void 0;
131344
- this.verifiedPassword = null;
131345
- } else {
131346
- const { passwordHash, passwordSalt } = hashPassword(credentials.password);
131347
- this.settings.auth = {
131348
- username: credentials.username,
131349
- passwordHash,
131350
- passwordSalt
131351
- };
131352
- this.verifiedPassword = {
131353
- username: credentials.username,
131354
- password: credentials.password
131355
- };
131356
- }
131357
- await this.persist();
131358
- }
131359
- verifyAuth(username, password) {
131360
- const stored = this.settings.auth;
131361
- if (!stored) {
131362
- return false;
131363
- }
131364
- if (!constantTimeStringEquals(username, stored.username)) {
131365
- return false;
131366
- }
131367
- if (this.verifiedPassword && this.verifiedPassword.username === stored.username && constantTimeStringEquals(password, this.verifiedPassword.password)) {
131368
- return true;
131369
- }
131370
- if (stored.password !== void 0) {
131371
- const matches = constantTimeStringEquals(password, stored.password);
131372
- if (matches) {
131373
- this.verifiedPassword = { username: stored.username, password };
131374
- void this.migrateLegacyPlaintext(password);
131375
- }
131376
- return matches;
131377
- }
131378
- if (stored.passwordHash && stored.passwordSalt) {
131379
- if (verifyPasswordHash(password, stored.passwordHash, stored.passwordSalt)) {
131380
- this.verifiedPassword = { username: stored.username, password };
131381
- return true;
131382
- }
131383
- }
131384
- return false;
131385
- }
131386
- async migrateLegacyPlaintext(plain2) {
131387
- const current = this.settings.auth;
131388
- if (!current || current.password === void 0) {
131389
- return;
131390
- }
131391
- const { passwordHash, passwordSalt } = hashPassword(plain2);
131392
- this.settings.auth = {
131393
- username: current.username,
131394
- passwordHash,
131395
- passwordSalt
131396
- };
131397
- try {
131398
- await this.persist();
131399
- } catch {
131400
- }
131401
- }
131402
- get backupSettings() {
131403
- return { ...DEFAULT_BACKUP_SETTINGS, ...this.settings.backup };
131404
- }
131405
- async setBackupSettings(settings) {
131406
- this.settings.backup = { ...this.settings.backup, ...settings };
131407
- await this.persist();
131408
- }
131409
- async persist() {
131410
- await this.storage.set(
131411
- "data",
131412
- this.settings
131413
- );
131414
- }
131415
- };
131416
-
131417
131532
  // src/services/storage/app-storage.ts
131418
131533
  init_service();
131419
131534
  var AppStorage = class extends Service {
@@ -147233,6 +147348,7 @@ init_esm7();
147233
147348
  import crypto4 from "node:crypto";
147234
147349
 
147235
147350
  // src/services/bridges/bridge-data-provider.ts
147351
+ init_dist();
147236
147352
  init_service();
147237
147353
  import { values as values2 } from "lodash-es";
147238
147354
  var BridgeDataProvider = class extends Service {
@@ -147289,8 +147405,13 @@ var BridgeDataProvider = class extends Service {
147289
147405
  /**
147290
147406
  * @deprecated
147291
147407
  */
147292
- withMetadata(status3, serverNode, deviceCount, failedEntities = []) {
147408
+ withMetadata(status3, serverNode, deviceCount, failedEntities = [], exposedDeviceTypes = []) {
147293
147409
  const commissioning = serverNode.state.commissioning;
147410
+ const fabrics = commissioning ? values2(commissioning.fabrics) : [];
147411
+ const controllerWarnings = controllerWarningsForFabrics(
147412
+ fabrics,
147413
+ exposedDeviceTypes
147414
+ );
147294
147415
  return {
147295
147416
  id: this.id,
147296
147417
  name: this.name,
@@ -147311,7 +147432,7 @@ var BridgeDataProvider = class extends Service {
147311
147432
  discriminator: commissioning.discriminator,
147312
147433
  manualPairingCode: commissioning.pairingCodes.manualPairingCode,
147313
147434
  qrPairingCode: commissioning.pairingCodes.qrPairingCode,
147314
- fabrics: values2(commissioning.fabrics).map((fabric) => ({
147435
+ fabrics: fabrics.map((fabric) => ({
147315
147436
  fabricIndex: fabric.fabricIndex,
147316
147437
  fabricId: Number(fabric.fabricId),
147317
147438
  nodeId: Number(fabric.nodeId),
@@ -147321,7 +147442,8 @@ var BridgeDataProvider = class extends Service {
147321
147442
  }))
147322
147443
  } : void 0,
147323
147444
  deviceCount,
147324
- failedEntities: failedEntities.length > 0 ? failedEntities : void 0
147445
+ failedEntities: failedEntities.length > 0 ? failedEntities : void 0,
147446
+ controllerWarnings: controllerWarnings.length > 0 ? controllerWarnings : void 0
147325
147447
  };
147326
147448
  }
147327
147449
  };
@@ -148500,7 +148622,8 @@ var Bridge = class {
148500
148622
  this.status,
148501
148623
  this.server,
148502
148624
  this.aggregator.parts.size,
148503
- this.endpointManager.failedEntities
148625
+ this.endpointManager.failedEntities,
148626
+ this.getExposedDeviceTypes()
148504
148627
  );
148505
148628
  }
148506
148629
  getSessionInfo() {
@@ -162992,7 +163115,11 @@ var EntityIsolationServiceImpl = class {
162992
163115
  return true;
162993
163116
  }
162994
163117
  const reason = `${classification}. Entity isolated to protect bridge stability.`;
162995
- this.isolatedEntities.set(key, { entityId: entityName, reason });
163118
+ this.isolatedEntities.set(key, {
163119
+ entityId: entityName,
163120
+ reason,
163121
+ failedAt: (/* @__PURE__ */ new Date()).toISOString()
163122
+ });
162996
163123
  logger231.warn(
162997
163124
  `Isolating entity "${entityName}" from bridge ${bridgeId} due to: ${reason}`
162998
163125
  );
@@ -163080,6 +163207,13 @@ var BridgeEndpointManager = class extends Service {
163080
163207
  const isolated = EntityIsolationService.getIsolatedEntities(this.bridgeId);
163081
163208
  return [...this._failedEntities, ...isolated];
163082
163209
  }
163210
+ addFailedEntity(entityId, reason) {
163211
+ this._failedEntities.push({
163212
+ entityId,
163213
+ reason,
163214
+ failedAt: (/* @__PURE__ */ new Date()).toISOString()
163215
+ });
163216
+ }
163083
163217
  wirePluginCallbacks() {
163084
163218
  if (!this.pluginManager) return;
163085
163219
  this.pluginManager.onDeviceRegistered = async (pluginName, device) => {
@@ -163490,10 +163624,10 @@ var BridgeEndpointManager = class extends Service {
163490
163624
  }
163491
163625
  if (memoryLimitReached) {
163492
163626
  if (!existingEndpoints.some((e) => e.entityId === entityId)) {
163493
- this._failedEntities.push({
163627
+ this.addFailedEntity(
163494
163628
  entityId,
163495
- reason: "Skipped due to memory pressure, reduce entities or increase heap size"
163496
- });
163629
+ "Skipped due to memory pressure, reduce entities or increase heap size"
163630
+ );
163497
163631
  }
163498
163632
  continue;
163499
163633
  }
@@ -163511,7 +163645,7 @@ var BridgeEndpointManager = class extends Service {
163511
163645
  if (entityId.length > MAX_ENTITY_ID_LENGTH) {
163512
163646
  const reason = `Entity ID too long (${entityId.length} chars, max ${MAX_ENTITY_ID_LENGTH}). This would cause filesystem errors.`;
163513
163647
  this.log.warn(`Skipping entity: ${entityId}. Reason: ${reason}`);
163514
- this._failedEntities.push({ entityId, reason });
163648
+ this.addFailedEntity(entityId, reason);
163515
163649
  continue;
163516
163650
  }
163517
163651
  let endpoint = existingEndpoints.find((e) => e.entityId === entityId);
@@ -163527,7 +163661,7 @@ var BridgeEndpointManager = class extends Service {
163527
163661
  } catch (e) {
163528
163662
  const reason = this.extractErrorReason(e);
163529
163663
  this.log.warn(`Failed to create device ${entityId}: ${reason}`);
163530
- this._failedEntities.push({ entityId, reason });
163664
+ this.addFailedEntity(entityId, reason);
163531
163665
  continue;
163532
163666
  }
163533
163667
  if (endpoint) {
@@ -163543,10 +163677,7 @@ var BridgeEndpointManager = class extends Service {
163543
163677
  `Failed to add endpoint for ${entityId}: ${errorMessage}`
163544
163678
  );
163545
163679
  this.logDetailedError(entityId, e);
163546
- this._failedEntities.push({
163547
- entityId,
163548
- reason: this.extractErrorReason(e)
163549
- });
163680
+ this.addFailedEntity(entityId, this.extractErrorReason(e));
163550
163681
  }
163551
163682
  }
163552
163683
  }
@@ -164427,7 +164558,8 @@ var ServerModeBridge = class {
164427
164558
  this.status,
164428
164559
  this.server,
164429
164560
  this.endpointManager.devices.length,
164430
- this.endpointManager.failedEntities
164561
+ this.endpointManager.failedEntities,
164562
+ this.getExposedDeviceTypes()
164431
164563
  );
164432
164564
  }
164433
164565
  /**
@@ -165991,6 +166123,14 @@ async function startHandler(startOptions, webUiDist) {
165991
166123
  bridgeService.onBridgeChanged = (bridgeId) => {
165992
166124
  webApi.websocket.broadcastBridgeUpdate(bridgeId);
165993
166125
  };
166126
+ const [haClient, settingsStorage] = await Promise.all([
166127
+ appEnvironment.load(HomeAssistantClient),
166128
+ appEnvironment.load(AppSettingsStorage)
166129
+ ]);
166130
+ bridgeService.applyRecoverySettings(settingsStorage.recoverySettings);
166131
+ haClient.connection.addEventListener("ready", () => {
166132
+ bridgeService.recoverFailedBridgesNow();
166133
+ });
165994
166134
  let shuttingDown = false;
165995
166135
  const gracefulShutdown = async (signal) => {
165996
166136
  if (shuttingDown) return;