@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.
- package/CHANGELOG.md +16 -1
- package/README.md +20 -13
- package/dist/{account-integration-offline-2MI94wOE.js → account-integration-offline-BMoYdiX2.js} +126 -1
- package/dist/{account-integration-offline-DKaazSyb.mjs → account-integration-offline-pv6cqOKu.mjs} +115 -2
- package/dist/{dist-DEZDBpGW.js → dist-BioIJ3NA.js} +1818 -126
- package/dist/{dist-Cbcvh8mB.mjs → dist-DBUnIovD.mjs} +1807 -127
- package/dist/home-connect/addon.js +349 -259
- package/dist/home-connect/addon.mjs +349 -259
- package/dist/homecom/addon.js +94 -12
- package/dist/homecom/addon.mjs +94 -12
- package/dist/shc/addon.js +1 -1
- package/dist/shc/addon.mjs +1 -1
- package/i18n/en.json +15 -2
- package/i18n/it.json +15 -2
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.30] - 2026-10-27
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Home Connect and HomeCom now sign in through a page served by the hub; no more copying the redirect address out of the browser's developer tools.
|
|
15
|
+
|
|
16
|
+
## [0.1.29] - 2026-10-27
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- The Smart Home Controller addon is shown as "Bosch Smart Home" (was "Bosch SHC").
|
|
21
|
+
|
|
22
|
+
## [0.1.28] - 2026-10-27
|
|
23
|
+
|
|
10
24
|
### Added
|
|
11
25
|
|
|
12
|
-
-
|
|
26
|
+
- Bosch: Bosch smart-home device-provider addons for CamStack — SHC, Home Connect, and HomeCom cloud integrations (scaffold).
|
|
27
|
+
- Provides the addons Bosch Smart Home, Bosch Home Connect, Bosch HomeCom.
|
package/README.md
CHANGED
|
@@ -1,24 +1,31 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Bosch
|
|
2
2
|
|
|
3
|
-
Bosch smart-home device-provider addons for CamStack — SHC, Home Connect, and HomeCom cloud integrations (scaffold)
|
|
3
|
+
Bosch smart-home device-provider addons for CamStack — SHC, Home Connect, and HomeCom cloud integrations (scaffold).
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
<!-- readme:generated:start (derived from the manifest; edit outside this block) -->
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
|
|
7
|
+
Package `@camstack/addon-provider-bosch` on npm.
|
|
8
|
+
|
|
9
|
+
## What it provides
|
|
10
|
+
|
|
11
|
+
- **Bosch Smart Home** (`bosch-shc`): Adopt Bosch Smart Home Controller (SHC) devices via account onboarding — scaffold. Category: providers.
|
|
9
12
|
- Capabilities: `device-provider`, `device-adoption`, `connection-test`.
|
|
10
|
-
- **Bosch Home Connect** (`bosch-home-connect`): Adopt Bosch Home Connect appliances via account onboarding — scaffold
|
|
11
|
-
- Node placement: `hub-only`.
|
|
13
|
+
- **Bosch Home Connect** (`bosch-home-connect`): Adopt Bosch Home Connect appliances via account onboarding — scaffold. Category: providers.
|
|
12
14
|
- Capabilities: `device-provider`, `device-adoption`, `connection-test`, `dishwasher`, `consumables`, `running-state`.
|
|
13
|
-
- **Bosch HomeCom** (`bosch-homecom`): Adopt Bosch HomeCom climate and water-heating devices via account onboarding — scaffold
|
|
14
|
-
- Node placement: `hub-only`.
|
|
15
|
+
- **Bosch HomeCom** (`bosch-homecom`): Adopt Bosch HomeCom climate and water-heating devices via account onboarding — scaffold. Category: providers.
|
|
15
16
|
- Capabilities: `device-provider`, `device-adoption`, `connection-test`, `climate-control`, `water-heater`.
|
|
16
17
|
|
|
17
|
-
##
|
|
18
|
+
## Requirements
|
|
18
19
|
|
|
19
|
-
|
|
20
|
+
- Placement: hub-only.
|
|
21
|
+
- A CamStack hub. An agent is used only where the placement above allows it.
|
|
20
22
|
|
|
21
|
-
##
|
|
23
|
+
## Documentation
|
|
24
|
+
|
|
25
|
+
- [docs/addons/addon-provider-bosch-homecom.md](https://github.com/camstack/server/blob/main/docs/addons/addon-provider-bosch-homecom.md)
|
|
26
|
+
- [docs/addons/addon-provider-bosch-home-connect.md](https://github.com/camstack/server/blob/main/docs/addons/addon-provider-bosch-home-connect.md)
|
|
27
|
+
- [docs/architecture/device-model.md](https://github.com/camstack/server/blob/main/docs/architecture/device-model.md)
|
|
22
28
|
|
|
23
|
-
|
|
29
|
+
See [CHANGELOG.md](./CHANGELOG.md) for what changed in each version.
|
|
24
30
|
|
|
31
|
+
<!-- readme:generated:end -->
|
package/dist/{account-integration-offline-2MI94wOE.js → account-integration-offline-BMoYdiX2.js}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const require_dist = require("./dist-
|
|
1
|
+
const require_dist = require("./dist-BioIJ3NA.js");
|
|
2
2
|
let node_crypto = require("node:crypto");
|
|
3
3
|
//#region src/shared/oauth-callback.ts
|
|
4
4
|
/** A code on its own: no scheme, no query, no separators. */
|
|
@@ -118,6 +118,119 @@ function loginFailureForAttempt(reason) {
|
|
|
118
118
|
return reason === "expired" ? "code-rejected" : "state-mismatch";
|
|
119
119
|
}
|
|
120
120
|
//#endregion
|
|
121
|
+
//#region src/shared/login-proxy-sign-in.ts
|
|
122
|
+
function createSignInClaims(ttlMs = SIGN_IN_ATTEMPT_TTL_MS) {
|
|
123
|
+
const parked = /* @__PURE__ */ new Map();
|
|
124
|
+
return {
|
|
125
|
+
settle(attemptId, outcome, nowMs) {
|
|
126
|
+
parked.set(attemptId, {
|
|
127
|
+
outcome,
|
|
128
|
+
expiresAtMs: nowMs + ttlMs
|
|
129
|
+
});
|
|
130
|
+
},
|
|
131
|
+
claim(attemptId, nowMs) {
|
|
132
|
+
const found = parked.get(attemptId);
|
|
133
|
+
if (found === void 0) return { state: "pending" };
|
|
134
|
+
parked.delete(attemptId);
|
|
135
|
+
if (nowMs > found.expiresAtMs) return { state: "pending" };
|
|
136
|
+
if (!found.outcome.ok) throw new Error(found.outcome.message);
|
|
137
|
+
return {
|
|
138
|
+
state: "done",
|
|
139
|
+
result: found.outcome.result
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/** An `&`-escaped address inside HTML is the same address. */
|
|
145
|
+
function unescapeHtml(text) {
|
|
146
|
+
return text.replace(/&/g, "&");
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Terminal condition for an app-scheme redirect: the `Location` of a response, or — when the
|
|
150
|
+
* vendor's page navigates by script — the first address in its body, that starts with the app
|
|
151
|
+
* scheme.
|
|
152
|
+
*/
|
|
153
|
+
function appSchemeTerminal(scheme) {
|
|
154
|
+
const inBody = new RegExp(`${scheme.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[^"'<>\\s\\\\]+`);
|
|
155
|
+
return (view) => {
|
|
156
|
+
if (view.location !== null && view.location.startsWith(scheme)) return {
|
|
157
|
+
kind: "done",
|
|
158
|
+
payload: view.location
|
|
159
|
+
};
|
|
160
|
+
const fromBody = view.bodyText === null ? null : inBody.exec(unescapeHtml(view.bodyText));
|
|
161
|
+
if (fromBody !== null) return {
|
|
162
|
+
kind: "done",
|
|
163
|
+
payload: fromBody[0]
|
|
164
|
+
};
|
|
165
|
+
return { kind: "continue" };
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/** The result-page reason code for a sign-in failure reason (`HomeConnectLoginFailure`, …). */
|
|
169
|
+
function landingReasonForLogin(reason) {
|
|
170
|
+
switch (reason) {
|
|
171
|
+
case "denied": return "access_denied";
|
|
172
|
+
case "state-mismatch":
|
|
173
|
+
case "state-missing": return "invalid_state";
|
|
174
|
+
case "empty":
|
|
175
|
+
case "no-code": return "invalid_request";
|
|
176
|
+
case "code-rejected":
|
|
177
|
+
case "rejected": return "exchange_failed";
|
|
178
|
+
case "no-refresh": return "no_refresh_token";
|
|
179
|
+
default: return "unknown";
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function reasonOf(err) {
|
|
183
|
+
if (typeof err === "object" && err !== null && "reason" in err) {
|
|
184
|
+
const reason = err.reason;
|
|
185
|
+
if (typeof reason === "string") return reason;
|
|
186
|
+
}
|
|
187
|
+
return "unknown";
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Open the proxied login for one attempt and return the hub-relative path the browser opens.
|
|
191
|
+
* A failed exchange is parked as an operator-facing message and the browser lands on the
|
|
192
|
+
* result page with a reason code; nothing about the captured address leaves this function.
|
|
193
|
+
*/
|
|
194
|
+
function openSignInProxy(input) {
|
|
195
|
+
const started = input.nowMs();
|
|
196
|
+
input.proxy.open({
|
|
197
|
+
attemptId: input.attemptId,
|
|
198
|
+
expiresAtMs: started + SIGN_IN_ATTEMPT_TTL_MS,
|
|
199
|
+
upstreams: input.hosts.map((host) => ({
|
|
200
|
+
host,
|
|
201
|
+
scheme: input.scheme ?? "https"
|
|
202
|
+
})),
|
|
203
|
+
terminal: appSchemeTerminal(input.appScheme),
|
|
204
|
+
onDone: async (callback) => {
|
|
205
|
+
try {
|
|
206
|
+
const result = await input.finish(callback);
|
|
207
|
+
input.claims.settle(input.attemptId, {
|
|
208
|
+
ok: true,
|
|
209
|
+
result
|
|
210
|
+
}, input.nowMs());
|
|
211
|
+
return {
|
|
212
|
+
status: "success",
|
|
213
|
+
addonId: input.addonId
|
|
214
|
+
};
|
|
215
|
+
} catch (err) {
|
|
216
|
+
const message = err instanceof Error ? err.message : "The sign-in failed.";
|
|
217
|
+
input.claims.settle(input.attemptId, {
|
|
218
|
+
ok: false,
|
|
219
|
+
message
|
|
220
|
+
}, input.nowMs());
|
|
221
|
+
return {
|
|
222
|
+
status: "error",
|
|
223
|
+
addonId: input.addonId,
|
|
224
|
+
reason: landingReasonForLogin(reasonOf(err))
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
const entry = input.proxy.entryPath(input.attemptId, input.authorizeUrl);
|
|
230
|
+
if (entry === null) throw new Error("The sign-in page host is not reachable through the proxy.");
|
|
231
|
+
return entry;
|
|
232
|
+
}
|
|
233
|
+
//#endregion
|
|
121
234
|
//#region src/shared/account-integration-cascade.ts
|
|
122
235
|
async function cascadeRemoveDevicesForIntegration(input) {
|
|
123
236
|
const { reg, devices, addonId, system, integrationId, logger } = input;
|
|
@@ -167,12 +280,24 @@ Object.defineProperty(exports, "createSignInAttemptStore", {
|
|
|
167
280
|
return createSignInAttemptStore;
|
|
168
281
|
}
|
|
169
282
|
});
|
|
283
|
+
Object.defineProperty(exports, "createSignInClaims", {
|
|
284
|
+
enumerable: true,
|
|
285
|
+
get: function() {
|
|
286
|
+
return createSignInClaims;
|
|
287
|
+
}
|
|
288
|
+
});
|
|
170
289
|
Object.defineProperty(exports, "loginFailureForAttempt", {
|
|
171
290
|
enumerable: true,
|
|
172
291
|
get: function() {
|
|
173
292
|
return loginFailureForAttempt;
|
|
174
293
|
}
|
|
175
294
|
});
|
|
295
|
+
Object.defineProperty(exports, "openSignInProxy", {
|
|
296
|
+
enumerable: true,
|
|
297
|
+
get: function() {
|
|
298
|
+
return openSignInProxy;
|
|
299
|
+
}
|
|
300
|
+
});
|
|
176
301
|
Object.defineProperty(exports, "parseOAuthCallback", {
|
|
177
302
|
enumerable: true,
|
|
178
303
|
get: function() {
|
package/dist/{account-integration-offline-DKaazSyb.mjs → account-integration-offline-pv6cqOKu.mjs}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { D as errMsg } from "./dist-DBUnIovD.mjs";
|
|
2
2
|
import { randomBytes } from "node:crypto";
|
|
3
3
|
//#region src/shared/oauth-callback.ts
|
|
4
4
|
/** A code on its own: no scheme, no query, no separators. */
|
|
@@ -118,6 +118,119 @@ function loginFailureForAttempt(reason) {
|
|
|
118
118
|
return reason === "expired" ? "code-rejected" : "state-mismatch";
|
|
119
119
|
}
|
|
120
120
|
//#endregion
|
|
121
|
+
//#region src/shared/login-proxy-sign-in.ts
|
|
122
|
+
function createSignInClaims(ttlMs = SIGN_IN_ATTEMPT_TTL_MS) {
|
|
123
|
+
const parked = /* @__PURE__ */ new Map();
|
|
124
|
+
return {
|
|
125
|
+
settle(attemptId, outcome, nowMs) {
|
|
126
|
+
parked.set(attemptId, {
|
|
127
|
+
outcome,
|
|
128
|
+
expiresAtMs: nowMs + ttlMs
|
|
129
|
+
});
|
|
130
|
+
},
|
|
131
|
+
claim(attemptId, nowMs) {
|
|
132
|
+
const found = parked.get(attemptId);
|
|
133
|
+
if (found === void 0) return { state: "pending" };
|
|
134
|
+
parked.delete(attemptId);
|
|
135
|
+
if (nowMs > found.expiresAtMs) return { state: "pending" };
|
|
136
|
+
if (!found.outcome.ok) throw new Error(found.outcome.message);
|
|
137
|
+
return {
|
|
138
|
+
state: "done",
|
|
139
|
+
result: found.outcome.result
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/** An `&`-escaped address inside HTML is the same address. */
|
|
145
|
+
function unescapeHtml(text) {
|
|
146
|
+
return text.replace(/&/g, "&");
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Terminal condition for an app-scheme redirect: the `Location` of a response, or — when the
|
|
150
|
+
* vendor's page navigates by script — the first address in its body, that starts with the app
|
|
151
|
+
* scheme.
|
|
152
|
+
*/
|
|
153
|
+
function appSchemeTerminal(scheme) {
|
|
154
|
+
const inBody = new RegExp(`${scheme.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[^"'<>\\s\\\\]+`);
|
|
155
|
+
return (view) => {
|
|
156
|
+
if (view.location !== null && view.location.startsWith(scheme)) return {
|
|
157
|
+
kind: "done",
|
|
158
|
+
payload: view.location
|
|
159
|
+
};
|
|
160
|
+
const fromBody = view.bodyText === null ? null : inBody.exec(unescapeHtml(view.bodyText));
|
|
161
|
+
if (fromBody !== null) return {
|
|
162
|
+
kind: "done",
|
|
163
|
+
payload: fromBody[0]
|
|
164
|
+
};
|
|
165
|
+
return { kind: "continue" };
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/** The result-page reason code for a sign-in failure reason (`HomeConnectLoginFailure`, …). */
|
|
169
|
+
function landingReasonForLogin(reason) {
|
|
170
|
+
switch (reason) {
|
|
171
|
+
case "denied": return "access_denied";
|
|
172
|
+
case "state-mismatch":
|
|
173
|
+
case "state-missing": return "invalid_state";
|
|
174
|
+
case "empty":
|
|
175
|
+
case "no-code": return "invalid_request";
|
|
176
|
+
case "code-rejected":
|
|
177
|
+
case "rejected": return "exchange_failed";
|
|
178
|
+
case "no-refresh": return "no_refresh_token";
|
|
179
|
+
default: return "unknown";
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function reasonOf(err) {
|
|
183
|
+
if (typeof err === "object" && err !== null && "reason" in err) {
|
|
184
|
+
const reason = err.reason;
|
|
185
|
+
if (typeof reason === "string") return reason;
|
|
186
|
+
}
|
|
187
|
+
return "unknown";
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Open the proxied login for one attempt and return the hub-relative path the browser opens.
|
|
191
|
+
* A failed exchange is parked as an operator-facing message and the browser lands on the
|
|
192
|
+
* result page with a reason code; nothing about the captured address leaves this function.
|
|
193
|
+
*/
|
|
194
|
+
function openSignInProxy(input) {
|
|
195
|
+
const started = input.nowMs();
|
|
196
|
+
input.proxy.open({
|
|
197
|
+
attemptId: input.attemptId,
|
|
198
|
+
expiresAtMs: started + SIGN_IN_ATTEMPT_TTL_MS,
|
|
199
|
+
upstreams: input.hosts.map((host) => ({
|
|
200
|
+
host,
|
|
201
|
+
scheme: input.scheme ?? "https"
|
|
202
|
+
})),
|
|
203
|
+
terminal: appSchemeTerminal(input.appScheme),
|
|
204
|
+
onDone: async (callback) => {
|
|
205
|
+
try {
|
|
206
|
+
const result = await input.finish(callback);
|
|
207
|
+
input.claims.settle(input.attemptId, {
|
|
208
|
+
ok: true,
|
|
209
|
+
result
|
|
210
|
+
}, input.nowMs());
|
|
211
|
+
return {
|
|
212
|
+
status: "success",
|
|
213
|
+
addonId: input.addonId
|
|
214
|
+
};
|
|
215
|
+
} catch (err) {
|
|
216
|
+
const message = err instanceof Error ? err.message : "The sign-in failed.";
|
|
217
|
+
input.claims.settle(input.attemptId, {
|
|
218
|
+
ok: false,
|
|
219
|
+
message
|
|
220
|
+
}, input.nowMs());
|
|
221
|
+
return {
|
|
222
|
+
status: "error",
|
|
223
|
+
addonId: input.addonId,
|
|
224
|
+
reason: landingReasonForLogin(reasonOf(err))
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
const entry = input.proxy.entryPath(input.attemptId, input.authorizeUrl);
|
|
230
|
+
if (entry === null) throw new Error("The sign-in page host is not reachable through the proxy.");
|
|
231
|
+
return entry;
|
|
232
|
+
}
|
|
233
|
+
//#endregion
|
|
121
234
|
//#region src/shared/account-integration-cascade.ts
|
|
122
235
|
async function cascadeRemoveDevicesForIntegration(input) {
|
|
123
236
|
const { reg, devices, addonId, system, integrationId, logger } = input;
|
|
@@ -155,4 +268,4 @@ function setIntegrationDevicesOnline(devices, system, integrationId, online) {
|
|
|
155
268
|
return count;
|
|
156
269
|
}
|
|
157
270
|
//#endregion
|
|
158
|
-
export {
|
|
271
|
+
export { createSignInAttemptStore as a, openSignInProxy as i, cascadeRemoveDevicesForIntegration as n, loginFailureForAttempt as o, createSignInClaims as r, parseOAuthCallback as s, setIntegrationDevicesOnline as t };
|