@camstack/addon-provider-bosch 0.1.29 → 0.1.31

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-DzSesBKb.mjs";
2
- import { a as parseOAuthCallback, i as loginFailureForAttempt, n as cascadeRemoveDevicesForIntegration, r as createSignInAttemptStore, t as setIntegrationDevicesOnline } from "../account-integration-offline-C-XbrgLs.mjs";
1
+ import { A as customAction, B as record, D as DeviceType, E as DeviceRole, F as array, H as union, I as boolean, L as literal, M as isSecretMarker, N as isSecretMarkerLike, O as errMsg, P as _enum, S as runningStateCapability, U as unknown, V as string, W as EventCategory, _ as ingestReading, b as quantityFromUnit, c as binaryCapability, d as consumablesCapability, f as deviceAdoptionCapability, h as eventEmitterCapability, i as RUNNING_STATE_COLD, j as defineCustomActions, k as createEvent, m as enumSensorCapability, n as BaseDeviceProvider, o as VendorLoginProxy, p as dishwasherCapability, r as DishwasherOperationStateSchema, t as BaseDevice, u as connectionTestCapability, v as numericSensorCapability, x as runningFromDishwasherOperation, y as powerMeterCapability, z as object } from "../dist-DxO4cucS.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-Cfcp8s5x.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,294 @@ 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_APP_SCHEME = HOME_CONNECT_REDIRECT_URI.slice(0, HOME_CONNECT_REDIRECT_URI.indexOf("://") + 3);
1446
+ var homeConnectSignInClaims = createSignInClaims();
1447
+ /** Start an attempt whose `authorizeUrl` is the hub-relative proxied login page. */
1448
+ function beginHomeConnectProxiedSignIn(deps) {
1449
+ const nowMs = deps.nowMs ?? (() => Date.now());
1450
+ const published = publishHomeConnectSignIn(homeConnectSignInAttempts, nowMs());
1451
+ if (deps.proxy === null) return published;
1452
+ const entry = openSignInProxy({
1453
+ proxy: deps.proxy,
1454
+ claims: homeConnectSignInClaims,
1455
+ addonId: deps.addonId,
1456
+ attemptId: published.attemptId,
1457
+ authorizeUrl: published.authorizeUrl,
1458
+ hosts: HOME_CONNECT_LOGIN_HOSTS,
1459
+ appScheme: HOME_CONNECT_APP_SCHEME,
1460
+ finish: async (callback) => homeConnectFinishResult(await finishHomeConnectSignIn({
1461
+ attemptId: published.attemptId,
1462
+ callback,
1463
+ nowMs: nowMs()
1464
+ }, homeConnectSignInAttempts, deps.fetchImpl)),
1465
+ nowMs
1466
+ });
1467
+ return {
1468
+ attemptId: published.attemptId,
1469
+ authorizeUrl: entry
1470
+ };
1471
+ }
1472
+ //#endregion
1473
+ //#region src/home-connect/home-connect-discovery.ts
1474
+ /** Dishwasher accessory label: `Dishwasher` plus the appliance name. */
1475
+ function homeConnectDishwasherEntityName(containerName) {
1476
+ const base = containerName.trim();
1477
+ return base.length > 0 ? `Dishwasher ${base}` : "Dishwasher";
1478
+ }
1479
+ function deviceTypeForApplianceType(type) {
1480
+ if (type.toLowerCase().includes("dishwasher")) return DeviceType.Container;
1481
+ return null;
1482
+ }
1483
+ /**
1484
+ * The container candidate for one appliance. Its sensor children are not listed here:
1485
+ * they are the entities the appliance's own description declares, known once it is
1486
+ * connected, and they are attached to the container as that description is read.
1487
+ */
1488
+ function dishwasherContainerCandidate(app, adoptedId) {
1489
+ const name = app.name.length > 0 ? app.name : app.haId;
1490
+ const children = [{
1491
+ childNativeId: `${app.haId}#dishwasher`,
1492
+ name: homeConnectDishwasherEntityName(name),
1493
+ type: DeviceType.Dishwasher,
1494
+ status: "online",
1495
+ metadata: {
1496
+ serialNumber: app.haId,
1497
+ model: app.type
1498
+ },
1499
+ alreadyAdopted: false,
1500
+ adoptedDeviceId: null,
1501
+ capabilities: ["dishwasher"]
1502
+ }];
1503
+ return {
1504
+ childNativeId: app.haId,
1505
+ name,
1506
+ type: DeviceType.Container,
1507
+ status: "online",
1508
+ metadata: {
1509
+ serialNumber: app.haId,
1510
+ model: app.type
1511
+ },
1512
+ alreadyAdopted: adoptedId !== null,
1513
+ adoptedDeviceId: adoptedId,
1514
+ capabilities: [],
1515
+ children
1516
+ };
1517
+ }
1518
+ function buildHomeConnectCandidates(input) {
1519
+ const out = [];
1520
+ for (const app of input.appliances) {
1521
+ if (deviceTypeForApplianceType(app.type) === null) continue;
1522
+ const adoptedId = input.adopted.get(app.haId) ?? null;
1523
+ out.push(dishwasherContainerCandidate(app, adoptedId));
1524
+ }
1525
+ return out;
1526
+ }
1527
+ //#endregion
1528
+ //#region src/home-connect/home-connect-adoption-provider.ts
1529
+ var DEVICES_FILTER = {
1530
+ id: "devices",
1531
+ label: "Devices",
1532
+ isDefault: true
1533
+ };
1534
+ async function adoptedMapForIntegration(integrationId, listAdopted) {
1535
+ const all = await listAdopted();
1536
+ const map = /* @__PURE__ */ new Map();
1537
+ for (const device of all) if (device.config["system"] === "home-connect" && device.config["brokerId"] === integrationId) {
1538
+ const haId = device.config["haId"];
1539
+ if (typeof haId === "string") map.set(haId, device.id);
1540
+ }
1541
+ return map;
1542
+ }
1543
+ function buildHomeConnectAdoptionProvider(deps) {
1544
+ const { registry, hasIntegration, listIntegrations, listAdopted, adoptDevice, removeDevice, findDeviceConfig, logger } = deps;
1545
+ async function allCandidatesForIntegration(integrationId) {
1546
+ return buildHomeConnectCandidates({
1547
+ appliances: registry.appliances(integrationId),
1548
+ adopted: await adoptedMapForIntegration(integrationId, listAdopted)
1549
+ });
1550
+ }
1551
+ return {
1552
+ listCandidateFilters: async () => ({ filters: [DEVICES_FILTER] }),
1553
+ listCandidates: async ({ integrationId, page, pageSize }) => {
1554
+ const cands = await allCandidatesForIntegration(integrationId);
1555
+ const start = (page - 1) * pageSize;
1556
+ return {
1557
+ candidates: cands.slice(start, start + pageSize),
1558
+ totalCount: cands.length,
1559
+ page,
1560
+ pageSize
1561
+ };
1562
+ },
1563
+ getCandidate: async ({ integrationId, childNativeId }) => {
1564
+ return (await allCandidatesForIntegration(integrationId)).find((c) => c.childNativeId === childNativeId) ?? null;
1565
+ },
1566
+ getStatus: async () => {
1567
+ try {
1568
+ let candidateCount = 0;
1569
+ for (const integrationId of listIntegrations()) candidateCount += (await allCandidatesForIntegration(integrationId)).length;
1570
+ const adoptedCount = (await listAdopted()).filter((d) => d.config["system"] === "home-connect").length;
1571
+ return {
1572
+ lastDiscoveryAt: Date.now(),
1573
+ candidateCount,
1574
+ adoptedCount,
1575
+ lastError: null
1576
+ };
1577
+ } catch (err) {
1578
+ return {
1579
+ lastDiscoveryAt: null,
1580
+ candidateCount: 0,
1581
+ adoptedCount: 0,
1582
+ lastError: errMsg(err)
1583
+ };
1584
+ }
1585
+ },
1586
+ refresh: async ({ integrationId }) => {
1587
+ const candidateCount = (await allCandidatesForIntegration(integrationId)).length;
1588
+ const adoptedCount = (await adoptedMapForIntegration(integrationId, listAdopted)).size;
1589
+ return {
1590
+ lastDiscoveryAt: Date.now(),
1591
+ candidateCount,
1592
+ adoptedCount,
1593
+ lastError: null
1594
+ };
1595
+ },
1596
+ adopt: async ({ integrationId, childNativeIds, perCandidate }) => {
1597
+ if (!registry.has(integrationId)) throw new Error(`home-connect adopt: integration ${integrationId} not connected`);
1598
+ const appliances = registry.appliances(integrationId);
1599
+ const adopted = [];
1600
+ let failures = 0;
1601
+ for (const haId of childNativeIds) try {
1602
+ const app = appliances.find((a) => a.haId === haId);
1603
+ if (!app) {
1604
+ failures++;
1605
+ continue;
1606
+ }
1607
+ const candidate = buildHomeConnectCandidates({
1608
+ appliances: [app],
1609
+ adopted: /* @__PURE__ */ new Map()
1610
+ })[0];
1611
+ if (!candidate) {
1612
+ failures++;
1613
+ continue;
1614
+ }
1615
+ const name = perCandidate?.[haId]?.name ?? app.name;
1616
+ const { deviceId } = await adoptDevice({
1617
+ haId,
1618
+ integrationId,
1619
+ type: candidate.type,
1620
+ name,
1621
+ applianceType: app.type
1622
+ });
1623
+ adopted.push({
1624
+ childNativeId: haId,
1625
+ parentDeviceId: deviceId
1626
+ });
1627
+ } catch (err) {
1628
+ logger.warn("home-connect adopt failed", { meta: {
1629
+ haId,
1630
+ integrationId,
1631
+ error: errMsg(err)
1632
+ } });
1633
+ failures++;
1634
+ }
1635
+ if (adopted.length === 0 && failures > 0) throw new Error(`home-connect adopt: all ${failures} adopt(s) failed`);
1636
+ return { adopted: adopted.map((a) => ({
1637
+ ...a,
1638
+ accessoryDeviceIds: []
1639
+ })) };
1640
+ },
1641
+ release: async ({ camDeviceId }) => {
1642
+ await removeDevice(camDeviceId);
1643
+ },
1644
+ resync: async ({ camDeviceId }) => {
1645
+ const cfg = await findDeviceConfig(camDeviceId);
1646
+ if (!cfg || cfg["system"] !== "home-connect") throw new Error(`home-connect resync: device ${camDeviceId} not found`);
1647
+ const integrationId = String(cfg["brokerId"]);
1648
+ const haId = String(cfg["haId"]);
1649
+ if (!hasIntegration(integrationId)) throw new Error(`home-connect resync: integration ${integrationId} not connected`);
1650
+ if (!registry.appliances(integrationId).some((a) => a.haId === haId)) throw new Error(`home-connect resync: appliance ${haId} no longer present`);
1651
+ return {
1652
+ changed: false,
1653
+ rebuiltChildren: 0
1654
+ };
1655
+ }
1656
+ };
1657
+ }
1658
+ //#endregion
1539
1659
  //#region src/home-connect/home-connect-gateway.ts
1540
1660
  /**
1541
1661
  * Where devices find the client of their integration. A device that started before
@@ -3118,51 +3238,6 @@ function unmapped(appliances) {
3118
3238
  return appliances.filter((a) => a.mode === "TLS" && entityCountInStoredDescription(a.deviceDescription) === 0).map((a) => a.haId);
3119
3239
  }
3120
3240
  //#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
3241
  //#region src/home-connect/home-connect-integration-manager.ts
3167
3242
  /** Delays between IDDF fetch retries for an appliance that has no entity map; the last repeats. */
3168
3243
  var HOME_CONNECT_IDDF_RETRY_MS = [
@@ -5242,11 +5317,14 @@ var BoschHomeConnectProviderAddon = class extends BaseDeviceProvider {
5242
5317
  };
5243
5318
  clients = null;
5244
5319
  reconciler = null;
5320
+ loginProxy = null;
5321
+ loginPlane = null;
5245
5322
  constructor() {
5246
5323
  super({});
5247
5324
  }
5248
5325
  async onInitialize() {
5249
5326
  const regs = this.providerRegistrationsFromSuper(await super.onInitialize());
5327
+ await this.serveLoginProxy();
5250
5328
  this.clients = new HomeConnectClientRegistry(this.ctx.logger, { onConnectionUpdated: (integrationId, connection) => {
5251
5329
  this.persistIntegrationAppliances(integrationId, connection);
5252
5330
  } });
@@ -5276,32 +5354,22 @@ var BoschHomeConnectProviderAddon = class extends BaseDeviceProvider {
5276
5354
  customActions: homeConnectActions,
5277
5355
  actionHandlers: {
5278
5356
  beginSignIn: async () => ({
5279
- ...publishHomeConnectSignIn(homeConnectSignInAttempts, Date.now()),
5280
- summary: "Open the link in the browser and sign in."
5357
+ ...beginHomeConnectProxiedSignIn({
5358
+ proxy: this.loginProxy,
5359
+ addonId: this.ctx.id
5360
+ }),
5361
+ summary: "Open the sign-in page and sign in."
5281
5362
  }),
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
5363
+ finishSignIn: async (input) => homeConnectFinishResult(await finishHomeConnectSignIn({
5364
+ attemptId: input.attemptId,
5365
+ callback: input.callback,
5366
+ nowMs: Date.now()
5367
+ }, homeConnectSignInAttempts)),
5368
+ claimSignIn: async (input) => {
5369
+ const claim = homeConnectSignInClaims.claim(input.attemptId, Date.now());
5370
+ return claim.state === "pending" ? { pending: true } : {
5371
+ pending: false,
5372
+ ...claim.result
5305
5373
  };
5306
5374
  },
5307
5375
  signOut: async () => ({
@@ -5311,7 +5379,27 @@ var BoschHomeConnectProviderAddon = class extends BaseDeviceProvider {
5311
5379
  }
5312
5380
  };
5313
5381
  }
5382
+ /** The hub's HTTPS face for the vendor's login pages (D955): admin session only. */
5383
+ async serveLoginProxy() {
5384
+ const proxy = new VendorLoginProxy({
5385
+ addonId: this.ctx.id,
5386
+ logger: this.ctx.logger
5387
+ });
5388
+ try {
5389
+ this.loginPlane = await this.ctx.dataPlane?.serve({
5390
+ prefix: "vendor-login",
5391
+ access: "admin",
5392
+ handler: proxy.handler
5393
+ }) ?? null;
5394
+ if (this.loginPlane !== null) this.loginProxy = proxy;
5395
+ } catch (err) {
5396
+ this.ctx.logger.warn("Home Connect login proxy failed to serve — sign-in falls back to paste", { meta: { error: errMsg(err) } });
5397
+ }
5398
+ }
5314
5399
  async onShutdown() {
5400
+ await this.loginPlane?.dispose().catch(() => void 0);
5401
+ this.loginPlane = null;
5402
+ this.loginProxy = null;
5315
5403
  this.reconciler?.stop();
5316
5404
  this.reconciler = null;
5317
5405
  try {