@camstack/addon-provider-bosch 0.1.28 → 0.1.30

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.
@@ -1,5 +1,5 @@
1
- import { A as isSecretMarker, B as unknown, D as createEvent, E as errMsg, I as object, L as record, M as _enum, N as array, O as customAction, P as literal, R as string, T as DeviceType, V as EventCategory, _ as powerMeterCapability, b as runningStateCapability, c as connectionTestCapability, d as dishwasherCapability, f as enumSensorCapability, g as numericSensorCapability, h as ingestReading, i as RUNNING_STATE_COLD, j as isSecretMarkerLike, k as defineCustomActions, l as consumablesCapability, n as BaseDeviceProvider, o as binaryCapability, p as eventEmitterCapability, r as DishwasherOperationStateSchema, t as BaseDevice, u as deviceAdoptionCapability, v as quantityFromUnit, w as DeviceRole, y as runningFromDishwasherOperation, z as union } from "../dist-Cbcvh8mB.mjs";
2
- import { a as parseOAuthCallback, i as loginFailureForAttempt, n as cascadeRemoveDevicesForIntegration, r as createSignInAttemptStore, t as setIntegrationDevicesOnline } from "../account-integration-offline-DKaazSyb.mjs";
1
+ import { A as defineCustomActions, B as string, D as errMsg, E as DeviceType, F as boolean, H as unknown, I as literal, M as isSecretMarkerLike, N as _enum, O as createEvent, P as array, R as object, T as DeviceRole, U as EventCategory, V as union, _ as numericSensorCapability, a as VendorLoginProxy, b as runningFromDishwasherOperation, d as deviceAdoptionCapability, f as dishwasherCapability, g as ingestReading, i as RUNNING_STATE_COLD, j as isSecretMarker, k as customAction, l as connectionTestCapability, m as eventEmitterCapability, n as BaseDeviceProvider, p as enumSensorCapability, r as DishwasherOperationStateSchema, s as binaryCapability, t as BaseDevice, u as consumablesCapability, v as powerMeterCapability, x as runningStateCapability, y as quantityFromUnit, z as record } from "../dist-DBUnIovD.mjs";
2
+ import { a as createSignInAttemptStore, i as openSignInProxy, n as cascadeRemoveDevicesForIntegration, o as loginFailureForAttempt, r as createSignInClaims, s as parseOAuthCallback, t as setIntegrationDevicesOnline } from "../account-integration-offline-pv6cqOKu.mjs";
3
3
  import { createHash, randomBytes } from "node:crypto";
4
4
  import { connect } from "node:tls";
5
5
  import { inflateRawSync } from "node:zlib";
@@ -30,6 +30,23 @@ var homeConnectActions = defineCustomActions({
30
30
  kind: "mutation",
31
31
  auth: "admin"
32
32
  }),
33
+ /**
34
+ * The form asks while the proxied login page is open: `pending` until the hub captured the
35
+ * app-scheme redirect and finished the exchange, then the same fields `finishSignIn` returns.
36
+ * Claimed once. A failed exchange answers with its operator-facing message as an error.
37
+ */
38
+ claimSignIn: customAction(object({ attemptId: string().min(1) }), object({
39
+ pending: boolean(),
40
+ token: string().optional().meta({ secret: true }),
41
+ refresh: string().optional().meta({ secret: true }),
42
+ accountLabel: string().optional(),
43
+ appliances: string().optional(),
44
+ applianceSecrets: string().optional().meta({ secret: true }),
45
+ summary: string().optional()
46
+ }), {
47
+ kind: "mutation",
48
+ auth: "admin"
49
+ }),
33
50
  signOut: customAction(object({}), object({
34
51
  signedOut: literal(true),
35
52
  summary: string()
@@ -986,192 +1003,6 @@ async function finishHomeConnectSignIn(input, store, fetchImpl = homeConnectLogi
986
1003
  }, fetchImpl);
987
1004
  }
988
1005
  var homeConnectSignInAttempts = createSignInAttemptStore();
989
- //#endregion
990
- //#region src/home-connect/home-connect-discovery.ts
991
- /** Dishwasher accessory label: `Dishwasher` plus the appliance name. */
992
- function homeConnectDishwasherEntityName(containerName) {
993
- const base = containerName.trim();
994
- return base.length > 0 ? `Dishwasher ${base}` : "Dishwasher";
995
- }
996
- function deviceTypeForApplianceType(type) {
997
- if (type.toLowerCase().includes("dishwasher")) return DeviceType.Container;
998
- return null;
999
- }
1000
- /**
1001
- * The container candidate for one appliance. Its sensor children are not listed here:
1002
- * they are the entities the appliance's own description declares, known once it is
1003
- * connected, and they are attached to the container as that description is read.
1004
- */
1005
- function dishwasherContainerCandidate(app, adoptedId) {
1006
- const name = app.name.length > 0 ? app.name : app.haId;
1007
- const children = [{
1008
- childNativeId: `${app.haId}#dishwasher`,
1009
- name: homeConnectDishwasherEntityName(name),
1010
- type: DeviceType.Dishwasher,
1011
- status: "online",
1012
- metadata: {
1013
- serialNumber: app.haId,
1014
- model: app.type
1015
- },
1016
- alreadyAdopted: false,
1017
- adoptedDeviceId: null,
1018
- capabilities: ["dishwasher"]
1019
- }];
1020
- return {
1021
- childNativeId: app.haId,
1022
- name,
1023
- type: DeviceType.Container,
1024
- status: "online",
1025
- metadata: {
1026
- serialNumber: app.haId,
1027
- model: app.type
1028
- },
1029
- alreadyAdopted: adoptedId !== null,
1030
- adoptedDeviceId: adoptedId,
1031
- capabilities: [],
1032
- children
1033
- };
1034
- }
1035
- function buildHomeConnectCandidates(input) {
1036
- const out = [];
1037
- for (const app of input.appliances) {
1038
- if (deviceTypeForApplianceType(app.type) === null) continue;
1039
- const adoptedId = input.adopted.get(app.haId) ?? null;
1040
- out.push(dishwasherContainerCandidate(app, adoptedId));
1041
- }
1042
- return out;
1043
- }
1044
- //#endregion
1045
- //#region src/home-connect/home-connect-adoption-provider.ts
1046
- var DEVICES_FILTER = {
1047
- id: "devices",
1048
- label: "Devices",
1049
- isDefault: true
1050
- };
1051
- async function adoptedMapForIntegration(integrationId, listAdopted) {
1052
- const all = await listAdopted();
1053
- const map = /* @__PURE__ */ new Map();
1054
- for (const device of all) if (device.config["system"] === "home-connect" && device.config["brokerId"] === integrationId) {
1055
- const haId = device.config["haId"];
1056
- if (typeof haId === "string") map.set(haId, device.id);
1057
- }
1058
- return map;
1059
- }
1060
- function buildHomeConnectAdoptionProvider(deps) {
1061
- const { registry, hasIntegration, listIntegrations, listAdopted, adoptDevice, removeDevice, findDeviceConfig, logger } = deps;
1062
- async function allCandidatesForIntegration(integrationId) {
1063
- return buildHomeConnectCandidates({
1064
- appliances: registry.appliances(integrationId),
1065
- adopted: await adoptedMapForIntegration(integrationId, listAdopted)
1066
- });
1067
- }
1068
- return {
1069
- listCandidateFilters: async () => ({ filters: [DEVICES_FILTER] }),
1070
- listCandidates: async ({ integrationId, page, pageSize }) => {
1071
- const cands = await allCandidatesForIntegration(integrationId);
1072
- const start = (page - 1) * pageSize;
1073
- return {
1074
- candidates: cands.slice(start, start + pageSize),
1075
- totalCount: cands.length,
1076
- page,
1077
- pageSize
1078
- };
1079
- },
1080
- getCandidate: async ({ integrationId, childNativeId }) => {
1081
- return (await allCandidatesForIntegration(integrationId)).find((c) => c.childNativeId === childNativeId) ?? null;
1082
- },
1083
- getStatus: async () => {
1084
- try {
1085
- let candidateCount = 0;
1086
- for (const integrationId of listIntegrations()) candidateCount += (await allCandidatesForIntegration(integrationId)).length;
1087
- const adoptedCount = (await listAdopted()).filter((d) => d.config["system"] === "home-connect").length;
1088
- return {
1089
- lastDiscoveryAt: Date.now(),
1090
- candidateCount,
1091
- adoptedCount,
1092
- lastError: null
1093
- };
1094
- } catch (err) {
1095
- return {
1096
- lastDiscoveryAt: null,
1097
- candidateCount: 0,
1098
- adoptedCount: 0,
1099
- lastError: errMsg(err)
1100
- };
1101
- }
1102
- },
1103
- refresh: async ({ integrationId }) => {
1104
- const candidateCount = (await allCandidatesForIntegration(integrationId)).length;
1105
- const adoptedCount = (await adoptedMapForIntegration(integrationId, listAdopted)).size;
1106
- return {
1107
- lastDiscoveryAt: Date.now(),
1108
- candidateCount,
1109
- adoptedCount,
1110
- lastError: null
1111
- };
1112
- },
1113
- adopt: async ({ integrationId, childNativeIds, perCandidate }) => {
1114
- if (!registry.has(integrationId)) throw new Error(`home-connect adopt: integration ${integrationId} not connected`);
1115
- const appliances = registry.appliances(integrationId);
1116
- const adopted = [];
1117
- let failures = 0;
1118
- for (const haId of childNativeIds) try {
1119
- const app = appliances.find((a) => a.haId === haId);
1120
- if (!app) {
1121
- failures++;
1122
- continue;
1123
- }
1124
- const candidate = buildHomeConnectCandidates({
1125
- appliances: [app],
1126
- adopted: /* @__PURE__ */ new Map()
1127
- })[0];
1128
- if (!candidate) {
1129
- failures++;
1130
- continue;
1131
- }
1132
- const name = perCandidate?.[haId]?.name ?? app.name;
1133
- const { deviceId } = await adoptDevice({
1134
- haId,
1135
- integrationId,
1136
- type: candidate.type,
1137
- name,
1138
- applianceType: app.type
1139
- });
1140
- adopted.push({
1141
- childNativeId: haId,
1142
- parentDeviceId: deviceId
1143
- });
1144
- } catch (err) {
1145
- logger.warn("home-connect adopt failed", { meta: {
1146
- haId,
1147
- integrationId,
1148
- error: errMsg(err)
1149
- } });
1150
- failures++;
1151
- }
1152
- if (adopted.length === 0 && failures > 0) throw new Error(`home-connect adopt: all ${failures} adopt(s) failed`);
1153
- return { adopted: adopted.map((a) => ({
1154
- ...a,
1155
- accessoryDeviceIds: []
1156
- })) };
1157
- },
1158
- release: async ({ camDeviceId }) => {
1159
- await removeDevice(camDeviceId);
1160
- },
1161
- resync: async ({ camDeviceId }) => {
1162
- const cfg = await findDeviceConfig(camDeviceId);
1163
- if (!cfg || cfg["system"] !== "home-connect") throw new Error(`home-connect resync: device ${camDeviceId} not found`);
1164
- const integrationId = String(cfg["brokerId"]);
1165
- const haId = String(cfg["haId"]);
1166
- if (!hasIntegration(integrationId)) throw new Error(`home-connect resync: integration ${integrationId} not connected`);
1167
- if (!registry.appliances(integrationId).some((a) => a.haId === haId)) throw new Error(`home-connect resync: appliance ${haId} no longer present`);
1168
- return {
1169
- changed: false,
1170
- rebuiltChildren: 0
1171
- };
1172
- }
1173
- };
1174
- }
1175
1006
  var HomeConnectModeSchema = _enum(["AES", "TLS"]);
1176
1007
  function isPlainObject$1(value) {
1177
1008
  return value !== null && typeof value === "object" && !Array.isArray(value);
@@ -1439,7 +1270,7 @@ function buildHomeConnectFormSchema() {
1439
1270
  id: "gateway",
1440
1271
  title: "Home Connect",
1441
1272
  titleKey: "provider-bosch:home-connect.s_gateway.title",
1442
- description: "Sign in with your Home Connect account. Paste the address the browser stops on (hcauth://auth/prod?code=…).",
1273
+ description: "Sign in with your Home Connect account. A new tab opens the sign-in page through this hub and finishes on its own.",
1443
1274
  descriptionKey: "provider-bosch:home-connect.s_gateway.description",
1444
1275
  columns: 1,
1445
1276
  fields: [
@@ -1452,6 +1283,7 @@ function buildHomeConnectFormSchema() {
1452
1283
  providerLabel: "Home Connect",
1453
1284
  beginAction: "beginSignIn",
1454
1285
  completeAction: "finishSignIn",
1286
+ claimAction: "claimSignIn",
1455
1287
  signOutAction: "signOut",
1456
1288
  accountKey: "accountLabel",
1457
1289
  countFromKey: "appliances",
@@ -1536,6 +1368,295 @@ function mergeAppliancesForPersist(storedAppliances, fromConnection) {
1536
1368
  return out;
1537
1369
  }
1538
1370
  //#endregion
1371
+ //#region src/home-connect/home-connect-stored-appliance.ts
1372
+ function stubDeviceDescriptionObject(row) {
1373
+ return { info: {
1374
+ deviceID: row.haId,
1375
+ deviceType: row.type,
1376
+ model: row.name
1377
+ } };
1378
+ }
1379
+ function stubDeviceDescription(row) {
1380
+ return JSON.stringify(stubDeviceDescriptionObject(row));
1381
+ }
1382
+ function serializedDeviceDescription(row) {
1383
+ if (row.deviceDescription === void 0) return stubDeviceDescription(row);
1384
+ if (typeof row.deviceDescription === "string") return row.deviceDescription;
1385
+ return JSON.stringify(row.deviceDescription);
1386
+ }
1387
+ function isDescriptionRecord(value) {
1388
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1389
+ }
1390
+ function parseLoginDeviceDescription(raw) {
1391
+ const trimmed = raw.trim();
1392
+ if (trimmed.length === 0) return {};
1393
+ try {
1394
+ const parsed = JSON.parse(trimmed);
1395
+ if (isDescriptionRecord(parsed)) return { deviceDescription: parsed };
1396
+ } catch {
1397
+ return {};
1398
+ }
1399
+ return {};
1400
+ }
1401
+ function listedApplianceFromStored(row) {
1402
+ return {
1403
+ haId: row.haId,
1404
+ name: row.name,
1405
+ type: row.type,
1406
+ connection: {
1407
+ host: row.host,
1408
+ psk: row.psk,
1409
+ mode: row.mode,
1410
+ iv64: row.iv64 ?? "",
1411
+ deviceDescription: serializedDeviceDescription(row)
1412
+ }
1413
+ };
1414
+ }
1415
+ //#endregion
1416
+ //#region src/home-connect/home-connect-sign-in-result.ts
1417
+ function homeConnectFinishResult(account) {
1418
+ const { rows, secrets } = splitApplianceSecrets(account.appliances.map((app) => ({
1419
+ haId: app.haId,
1420
+ name: app.name,
1421
+ type: app.type,
1422
+ host: app.connection.host,
1423
+ psk: app.connection.psk,
1424
+ mode: app.connection.mode,
1425
+ ...app.connection.iv64.length > 0 ? { iv64: app.connection.iv64 } : {},
1426
+ ...parseLoginDeviceDescription(app.connection.deviceDescription)
1427
+ })));
1428
+ return {
1429
+ token: account.token,
1430
+ ...account.refresh.length > 0 ? { refresh: account.refresh } : {},
1431
+ accountLabel: account.accountId,
1432
+ appliances: JSON.stringify(rows),
1433
+ applianceSecrets: JSON.stringify(secrets),
1434
+ summary: account.summary
1435
+ };
1436
+ }
1437
+ //#endregion
1438
+ //#region src/home-connect/home-connect-sign-in-proxy.ts
1439
+ /**
1440
+ * Home Connect signs in through its own identity pages, which end on `hcauth://auth/prod?...`
1441
+ * — an address no browser can open. These are the hosts those pages are served from; the
1442
+ * proxy reaches nothing else. (Not verified against the live pages: no vendor credentials.)
1443
+ */
1444
+ var HOME_CONNECT_LOGIN_HOSTS = ["api.home-connect.com", "singlekey-id.com"];
1445
+ var HOME_CONNECT_PROXY_PREFIX = "login";
1446
+ var HOME_CONNECT_APP_SCHEME = HOME_CONNECT_REDIRECT_URI.slice(0, HOME_CONNECT_REDIRECT_URI.indexOf("://") + 3);
1447
+ var homeConnectSignInClaims = createSignInClaims();
1448
+ /** Start an attempt whose `authorizeUrl` is the hub-relative proxied login page. */
1449
+ function beginHomeConnectProxiedSignIn(deps) {
1450
+ const nowMs = deps.nowMs ?? (() => Date.now());
1451
+ const published = publishHomeConnectSignIn(homeConnectSignInAttempts, nowMs());
1452
+ if (deps.proxy === null) return published;
1453
+ const entry = openSignInProxy({
1454
+ proxy: deps.proxy,
1455
+ claims: homeConnectSignInClaims,
1456
+ addonId: deps.addonId,
1457
+ attemptId: published.attemptId,
1458
+ authorizeUrl: published.authorizeUrl,
1459
+ hosts: HOME_CONNECT_LOGIN_HOSTS,
1460
+ appScheme: HOME_CONNECT_APP_SCHEME,
1461
+ finish: async (callback) => homeConnectFinishResult(await finishHomeConnectSignIn({
1462
+ attemptId: published.attemptId,
1463
+ callback,
1464
+ nowMs: nowMs()
1465
+ }, homeConnectSignInAttempts, deps.fetchImpl)),
1466
+ nowMs
1467
+ });
1468
+ return {
1469
+ attemptId: published.attemptId,
1470
+ authorizeUrl: entry
1471
+ };
1472
+ }
1473
+ //#endregion
1474
+ //#region src/home-connect/home-connect-discovery.ts
1475
+ /** Dishwasher accessory label: `Dishwasher` plus the appliance name. */
1476
+ function homeConnectDishwasherEntityName(containerName) {
1477
+ const base = containerName.trim();
1478
+ return base.length > 0 ? `Dishwasher ${base}` : "Dishwasher";
1479
+ }
1480
+ function deviceTypeForApplianceType(type) {
1481
+ if (type.toLowerCase().includes("dishwasher")) return DeviceType.Container;
1482
+ return null;
1483
+ }
1484
+ /**
1485
+ * The container candidate for one appliance. Its sensor children are not listed here:
1486
+ * they are the entities the appliance's own description declares, known once it is
1487
+ * connected, and they are attached to the container as that description is read.
1488
+ */
1489
+ function dishwasherContainerCandidate(app, adoptedId) {
1490
+ const name = app.name.length > 0 ? app.name : app.haId;
1491
+ const children = [{
1492
+ childNativeId: `${app.haId}#dishwasher`,
1493
+ name: homeConnectDishwasherEntityName(name),
1494
+ type: DeviceType.Dishwasher,
1495
+ status: "online",
1496
+ metadata: {
1497
+ serialNumber: app.haId,
1498
+ model: app.type
1499
+ },
1500
+ alreadyAdopted: false,
1501
+ adoptedDeviceId: null,
1502
+ capabilities: ["dishwasher"]
1503
+ }];
1504
+ return {
1505
+ childNativeId: app.haId,
1506
+ name,
1507
+ type: DeviceType.Container,
1508
+ status: "online",
1509
+ metadata: {
1510
+ serialNumber: app.haId,
1511
+ model: app.type
1512
+ },
1513
+ alreadyAdopted: adoptedId !== null,
1514
+ adoptedDeviceId: adoptedId,
1515
+ capabilities: [],
1516
+ children
1517
+ };
1518
+ }
1519
+ function buildHomeConnectCandidates(input) {
1520
+ const out = [];
1521
+ for (const app of input.appliances) {
1522
+ if (deviceTypeForApplianceType(app.type) === null) continue;
1523
+ const adoptedId = input.adopted.get(app.haId) ?? null;
1524
+ out.push(dishwasherContainerCandidate(app, adoptedId));
1525
+ }
1526
+ return out;
1527
+ }
1528
+ //#endregion
1529
+ //#region src/home-connect/home-connect-adoption-provider.ts
1530
+ var DEVICES_FILTER = {
1531
+ id: "devices",
1532
+ label: "Devices",
1533
+ isDefault: true
1534
+ };
1535
+ async function adoptedMapForIntegration(integrationId, listAdopted) {
1536
+ const all = await listAdopted();
1537
+ const map = /* @__PURE__ */ new Map();
1538
+ for (const device of all) if (device.config["system"] === "home-connect" && device.config["brokerId"] === integrationId) {
1539
+ const haId = device.config["haId"];
1540
+ if (typeof haId === "string") map.set(haId, device.id);
1541
+ }
1542
+ return map;
1543
+ }
1544
+ function buildHomeConnectAdoptionProvider(deps) {
1545
+ const { registry, hasIntegration, listIntegrations, listAdopted, adoptDevice, removeDevice, findDeviceConfig, logger } = deps;
1546
+ async function allCandidatesForIntegration(integrationId) {
1547
+ return buildHomeConnectCandidates({
1548
+ appliances: registry.appliances(integrationId),
1549
+ adopted: await adoptedMapForIntegration(integrationId, listAdopted)
1550
+ });
1551
+ }
1552
+ return {
1553
+ listCandidateFilters: async () => ({ filters: [DEVICES_FILTER] }),
1554
+ listCandidates: async ({ integrationId, page, pageSize }) => {
1555
+ const cands = await allCandidatesForIntegration(integrationId);
1556
+ const start = (page - 1) * pageSize;
1557
+ return {
1558
+ candidates: cands.slice(start, start + pageSize),
1559
+ totalCount: cands.length,
1560
+ page,
1561
+ pageSize
1562
+ };
1563
+ },
1564
+ getCandidate: async ({ integrationId, childNativeId }) => {
1565
+ return (await allCandidatesForIntegration(integrationId)).find((c) => c.childNativeId === childNativeId) ?? null;
1566
+ },
1567
+ getStatus: async () => {
1568
+ try {
1569
+ let candidateCount = 0;
1570
+ for (const integrationId of listIntegrations()) candidateCount += (await allCandidatesForIntegration(integrationId)).length;
1571
+ const adoptedCount = (await listAdopted()).filter((d) => d.config["system"] === "home-connect").length;
1572
+ return {
1573
+ lastDiscoveryAt: Date.now(),
1574
+ candidateCount,
1575
+ adoptedCount,
1576
+ lastError: null
1577
+ };
1578
+ } catch (err) {
1579
+ return {
1580
+ lastDiscoveryAt: null,
1581
+ candidateCount: 0,
1582
+ adoptedCount: 0,
1583
+ lastError: errMsg(err)
1584
+ };
1585
+ }
1586
+ },
1587
+ refresh: async ({ integrationId }) => {
1588
+ const candidateCount = (await allCandidatesForIntegration(integrationId)).length;
1589
+ const adoptedCount = (await adoptedMapForIntegration(integrationId, listAdopted)).size;
1590
+ return {
1591
+ lastDiscoveryAt: Date.now(),
1592
+ candidateCount,
1593
+ adoptedCount,
1594
+ lastError: null
1595
+ };
1596
+ },
1597
+ adopt: async ({ integrationId, childNativeIds, perCandidate }) => {
1598
+ if (!registry.has(integrationId)) throw new Error(`home-connect adopt: integration ${integrationId} not connected`);
1599
+ const appliances = registry.appliances(integrationId);
1600
+ const adopted = [];
1601
+ let failures = 0;
1602
+ for (const haId of childNativeIds) try {
1603
+ const app = appliances.find((a) => a.haId === haId);
1604
+ if (!app) {
1605
+ failures++;
1606
+ continue;
1607
+ }
1608
+ const candidate = buildHomeConnectCandidates({
1609
+ appliances: [app],
1610
+ adopted: /* @__PURE__ */ new Map()
1611
+ })[0];
1612
+ if (!candidate) {
1613
+ failures++;
1614
+ continue;
1615
+ }
1616
+ const name = perCandidate?.[haId]?.name ?? app.name;
1617
+ const { deviceId } = await adoptDevice({
1618
+ haId,
1619
+ integrationId,
1620
+ type: candidate.type,
1621
+ name,
1622
+ applianceType: app.type
1623
+ });
1624
+ adopted.push({
1625
+ childNativeId: haId,
1626
+ parentDeviceId: deviceId
1627
+ });
1628
+ } catch (err) {
1629
+ logger.warn("home-connect adopt failed", { meta: {
1630
+ haId,
1631
+ integrationId,
1632
+ error: errMsg(err)
1633
+ } });
1634
+ failures++;
1635
+ }
1636
+ if (adopted.length === 0 && failures > 0) throw new Error(`home-connect adopt: all ${failures} adopt(s) failed`);
1637
+ return { adopted: adopted.map((a) => ({
1638
+ ...a,
1639
+ accessoryDeviceIds: []
1640
+ })) };
1641
+ },
1642
+ release: async ({ camDeviceId }) => {
1643
+ await removeDevice(camDeviceId);
1644
+ },
1645
+ resync: async ({ camDeviceId }) => {
1646
+ const cfg = await findDeviceConfig(camDeviceId);
1647
+ if (!cfg || cfg["system"] !== "home-connect") throw new Error(`home-connect resync: device ${camDeviceId} not found`);
1648
+ const integrationId = String(cfg["brokerId"]);
1649
+ const haId = String(cfg["haId"]);
1650
+ if (!hasIntegration(integrationId)) throw new Error(`home-connect resync: integration ${integrationId} not connected`);
1651
+ if (!registry.appliances(integrationId).some((a) => a.haId === haId)) throw new Error(`home-connect resync: appliance ${haId} no longer present`);
1652
+ return {
1653
+ changed: false,
1654
+ rebuiltChildren: 0
1655
+ };
1656
+ }
1657
+ };
1658
+ }
1659
+ //#endregion
1539
1660
  //#region src/home-connect/home-connect-gateway.ts
1540
1661
  /**
1541
1662
  * Where devices find the client of their integration. A device that started before
@@ -3118,51 +3239,6 @@ function unmapped(appliances) {
3118
3239
  return appliances.filter((a) => a.mode === "TLS" && entityCountInStoredDescription(a.deviceDescription) === 0).map((a) => a.haId);
3119
3240
  }
3120
3241
  //#endregion
3121
- //#region src/home-connect/home-connect-stored-appliance.ts
3122
- function stubDeviceDescriptionObject(row) {
3123
- return { info: {
3124
- deviceID: row.haId,
3125
- deviceType: row.type,
3126
- model: row.name
3127
- } };
3128
- }
3129
- function stubDeviceDescription(row) {
3130
- return JSON.stringify(stubDeviceDescriptionObject(row));
3131
- }
3132
- function serializedDeviceDescription(row) {
3133
- if (row.deviceDescription === void 0) return stubDeviceDescription(row);
3134
- if (typeof row.deviceDescription === "string") return row.deviceDescription;
3135
- return JSON.stringify(row.deviceDescription);
3136
- }
3137
- function isDescriptionRecord(value) {
3138
- return typeof value === "object" && value !== null && !Array.isArray(value);
3139
- }
3140
- function parseLoginDeviceDescription(raw) {
3141
- const trimmed = raw.trim();
3142
- if (trimmed.length === 0) return {};
3143
- try {
3144
- const parsed = JSON.parse(trimmed);
3145
- if (isDescriptionRecord(parsed)) return { deviceDescription: parsed };
3146
- } catch {
3147
- return {};
3148
- }
3149
- return {};
3150
- }
3151
- function listedApplianceFromStored(row) {
3152
- return {
3153
- haId: row.haId,
3154
- name: row.name,
3155
- type: row.type,
3156
- connection: {
3157
- host: row.host,
3158
- psk: row.psk,
3159
- mode: row.mode,
3160
- iv64: row.iv64 ?? "",
3161
- deviceDescription: serializedDeviceDescription(row)
3162
- }
3163
- };
3164
- }
3165
- //#endregion
3166
3242
  //#region src/home-connect/home-connect-integration-manager.ts
3167
3243
  /** Delays between IDDF fetch retries for an appliance that has no entity map; the last repeats. */
3168
3244
  var HOME_CONNECT_IDDF_RETRY_MS = [
@@ -5242,11 +5318,14 @@ var BoschHomeConnectProviderAddon = class extends BaseDeviceProvider {
5242
5318
  };
5243
5319
  clients = null;
5244
5320
  reconciler = null;
5321
+ loginProxy = null;
5322
+ loginPlane = null;
5245
5323
  constructor() {
5246
5324
  super({});
5247
5325
  }
5248
5326
  async onInitialize() {
5249
5327
  const regs = this.providerRegistrationsFromSuper(await super.onInitialize());
5328
+ await this.serveLoginProxy();
5250
5329
  this.clients = new HomeConnectClientRegistry(this.ctx.logger, { onConnectionUpdated: (integrationId, connection) => {
5251
5330
  this.persistIntegrationAppliances(integrationId, connection);
5252
5331
  } });
@@ -5276,32 +5355,22 @@ var BoschHomeConnectProviderAddon = class extends BaseDeviceProvider {
5276
5355
  customActions: homeConnectActions,
5277
5356
  actionHandlers: {
5278
5357
  beginSignIn: async () => ({
5279
- ...publishHomeConnectSignIn(homeConnectSignInAttempts, Date.now()),
5280
- summary: "Open the link in the browser and sign in."
5358
+ ...beginHomeConnectProxiedSignIn({
5359
+ proxy: this.loginProxy,
5360
+ addonId: this.ctx.id
5361
+ }),
5362
+ summary: "Open the sign-in page and sign in."
5281
5363
  }),
5282
- finishSignIn: async (input) => {
5283
- const account = await finishHomeConnectSignIn({
5284
- attemptId: input.attemptId,
5285
- callback: input.callback,
5286
- nowMs: Date.now()
5287
- }, homeConnectSignInAttempts);
5288
- const { rows, secrets } = splitApplianceSecrets(account.appliances.map((app) => ({
5289
- haId: app.haId,
5290
- name: app.name,
5291
- type: app.type,
5292
- host: app.connection.host,
5293
- psk: app.connection.psk,
5294
- mode: app.connection.mode,
5295
- ...app.connection.iv64.length > 0 ? { iv64: app.connection.iv64 } : {},
5296
- ...parseLoginDeviceDescription(app.connection.deviceDescription)
5297
- })));
5298
- return {
5299
- token: account.token,
5300
- ...account.refresh.length > 0 ? { refresh: account.refresh } : {},
5301
- accountLabel: account.accountId,
5302
- appliances: JSON.stringify(rows),
5303
- applianceSecrets: JSON.stringify(secrets),
5304
- summary: account.summary
5364
+ finishSignIn: async (input) => homeConnectFinishResult(await finishHomeConnectSignIn({
5365
+ attemptId: input.attemptId,
5366
+ callback: input.callback,
5367
+ nowMs: Date.now()
5368
+ }, homeConnectSignInAttempts)),
5369
+ claimSignIn: async (input) => {
5370
+ const claim = homeConnectSignInClaims.claim(input.attemptId, Date.now());
5371
+ return claim.state === "pending" ? { pending: true } : {
5372
+ pending: false,
5373
+ ...claim.result
5305
5374
  };
5306
5375
  },
5307
5376
  signOut: async () => ({
@@ -5311,7 +5380,28 @@ var BoschHomeConnectProviderAddon = class extends BaseDeviceProvider {
5311
5380
  }
5312
5381
  };
5313
5382
  }
5383
+ /** The hub's HTTPS face for the vendor's login pages (D955): admin session only. */
5384
+ async serveLoginProxy() {
5385
+ const proxy = new VendorLoginProxy({
5386
+ addonId: this.ctx.id,
5387
+ prefix: HOME_CONNECT_PROXY_PREFIX,
5388
+ logger: this.ctx.logger
5389
+ });
5390
+ try {
5391
+ this.loginPlane = await this.ctx.dataPlane?.serve({
5392
+ prefix: "login",
5393
+ access: "admin",
5394
+ handler: proxy.handler
5395
+ }) ?? null;
5396
+ if (this.loginPlane !== null) this.loginProxy = proxy;
5397
+ } catch (err) {
5398
+ this.ctx.logger.warn("Home Connect login proxy failed to serve — sign-in falls back to paste", { meta: { error: errMsg(err) } });
5399
+ }
5400
+ }
5314
5401
  async onShutdown() {
5402
+ await this.loginPlane?.dispose().catch(() => void 0);
5403
+ this.loginPlane = null;
5404
+ this.loginProxy = null;
5315
5405
  this.reconciler?.stop();
5316
5406
  this.reconciler = null;
5317
5407
  try {