@riddix/hamh 2.1.0-alpha.756 → 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.
package/dist/backend/cli.js
CHANGED
|
@@ -124630,7 +124630,8 @@ function buildEntityDiagnostics(exposed, failed, warnings) {
|
|
|
124630
124630
|
const result = failed.map((f) => ({
|
|
124631
124631
|
entityId: f.entityId,
|
|
124632
124632
|
status: "failed",
|
|
124633
|
-
reason: f.reason
|
|
124633
|
+
reason: f.reason,
|
|
124634
|
+
failedAt: f.failedAt
|
|
124634
124635
|
}));
|
|
124635
124636
|
const failedIds = new Set(failed.map((f) => f.entityId));
|
|
124636
124637
|
const issuesByEntity = /* @__PURE__ */ new Map();
|
|
@@ -127169,7 +127170,8 @@ function healthApi(bridgeService, haClient, version2, startTime) {
|
|
|
127169
127170
|
recovery: {
|
|
127170
127171
|
enabled: bridgeService.autoRecoveryEnabled,
|
|
127171
127172
|
lastRecoveryAttempt: bridgeService.lastRecoveryAttempt?.toISOString(),
|
|
127172
|
-
recoveryCount: bridgeService.recoveryCount
|
|
127173
|
+
recoveryCount: bridgeService.recoveryCount,
|
|
127174
|
+
history: bridgeService.recoveryHistory
|
|
127173
127175
|
}
|
|
127174
127176
|
};
|
|
127175
127177
|
res.json(detailed);
|
|
@@ -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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
},
|
|
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
|
-
|
|
130668
|
-
|
|
130669
|
-
|
|
130670
|
-
|
|
130671
|
-
|
|
130672
|
-
|
|
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
|
-
|
|
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 {
|
|
@@ -163000,7 +163115,11 @@ var EntityIsolationServiceImpl = class {
|
|
|
163000
163115
|
return true;
|
|
163001
163116
|
}
|
|
163002
163117
|
const reason = `${classification}. Entity isolated to protect bridge stability.`;
|
|
163003
|
-
this.isolatedEntities.set(key, {
|
|
163118
|
+
this.isolatedEntities.set(key, {
|
|
163119
|
+
entityId: entityName,
|
|
163120
|
+
reason,
|
|
163121
|
+
failedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
163122
|
+
});
|
|
163004
163123
|
logger231.warn(
|
|
163005
163124
|
`Isolating entity "${entityName}" from bridge ${bridgeId} due to: ${reason}`
|
|
163006
163125
|
);
|
|
@@ -163088,6 +163207,13 @@ var BridgeEndpointManager = class extends Service {
|
|
|
163088
163207
|
const isolated = EntityIsolationService.getIsolatedEntities(this.bridgeId);
|
|
163089
163208
|
return [...this._failedEntities, ...isolated];
|
|
163090
163209
|
}
|
|
163210
|
+
addFailedEntity(entityId, reason) {
|
|
163211
|
+
this._failedEntities.push({
|
|
163212
|
+
entityId,
|
|
163213
|
+
reason,
|
|
163214
|
+
failedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
163215
|
+
});
|
|
163216
|
+
}
|
|
163091
163217
|
wirePluginCallbacks() {
|
|
163092
163218
|
if (!this.pluginManager) return;
|
|
163093
163219
|
this.pluginManager.onDeviceRegistered = async (pluginName, device) => {
|
|
@@ -163498,10 +163624,10 @@ var BridgeEndpointManager = class extends Service {
|
|
|
163498
163624
|
}
|
|
163499
163625
|
if (memoryLimitReached) {
|
|
163500
163626
|
if (!existingEndpoints.some((e) => e.entityId === entityId)) {
|
|
163501
|
-
this.
|
|
163627
|
+
this.addFailedEntity(
|
|
163502
163628
|
entityId,
|
|
163503
|
-
|
|
163504
|
-
|
|
163629
|
+
"Skipped due to memory pressure, reduce entities or increase heap size"
|
|
163630
|
+
);
|
|
163505
163631
|
}
|
|
163506
163632
|
continue;
|
|
163507
163633
|
}
|
|
@@ -163519,7 +163645,7 @@ var BridgeEndpointManager = class extends Service {
|
|
|
163519
163645
|
if (entityId.length > MAX_ENTITY_ID_LENGTH) {
|
|
163520
163646
|
const reason = `Entity ID too long (${entityId.length} chars, max ${MAX_ENTITY_ID_LENGTH}). This would cause filesystem errors.`;
|
|
163521
163647
|
this.log.warn(`Skipping entity: ${entityId}. Reason: ${reason}`);
|
|
163522
|
-
this.
|
|
163648
|
+
this.addFailedEntity(entityId, reason);
|
|
163523
163649
|
continue;
|
|
163524
163650
|
}
|
|
163525
163651
|
let endpoint = existingEndpoints.find((e) => e.entityId === entityId);
|
|
@@ -163535,7 +163661,7 @@ var BridgeEndpointManager = class extends Service {
|
|
|
163535
163661
|
} catch (e) {
|
|
163536
163662
|
const reason = this.extractErrorReason(e);
|
|
163537
163663
|
this.log.warn(`Failed to create device ${entityId}: ${reason}`);
|
|
163538
|
-
this.
|
|
163664
|
+
this.addFailedEntity(entityId, reason);
|
|
163539
163665
|
continue;
|
|
163540
163666
|
}
|
|
163541
163667
|
if (endpoint) {
|
|
@@ -163551,10 +163677,7 @@ var BridgeEndpointManager = class extends Service {
|
|
|
163551
163677
|
`Failed to add endpoint for ${entityId}: ${errorMessage}`
|
|
163552
163678
|
);
|
|
163553
163679
|
this.logDetailedError(entityId, e);
|
|
163554
|
-
this.
|
|
163555
|
-
entityId,
|
|
163556
|
-
reason: this.extractErrorReason(e)
|
|
163557
|
-
});
|
|
163680
|
+
this.addFailedEntity(entityId, this.extractErrorReason(e));
|
|
163558
163681
|
}
|
|
163559
163682
|
}
|
|
163560
163683
|
}
|
|
@@ -166000,6 +166123,14 @@ async function startHandler(startOptions, webUiDist) {
|
|
|
166000
166123
|
bridgeService.onBridgeChanged = (bridgeId) => {
|
|
166001
166124
|
webApi.websocket.broadcastBridgeUpdate(bridgeId);
|
|
166002
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
|
+
});
|
|
166003
166134
|
let shuttingDown = false;
|
|
166004
166135
|
const gracefulShutdown = async (signal) => {
|
|
166005
166136
|
if (shuttingDown) return;
|