edilkamin 1.6.1 → 1.7.2
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/.github/workflows/cli-tests.yml +1 -1
- package/.github/workflows/documentation.yml +1 -1
- package/.github/workflows/publish.yml +5 -3
- package/.github/workflows/tests.yml +1 -1
- package/README.md +61 -7
- package/dist/esm/buffer-utils.d.ts +25 -0
- package/dist/esm/buffer-utils.js +70 -0
- package/dist/esm/buffer-utils.test.d.ts +1 -0
- package/dist/esm/buffer-utils.test.js +181 -0
- package/dist/esm/cli.js +110 -9
- package/dist/esm/constants.d.ts +3 -1
- package/dist/esm/constants.js +4 -2
- package/dist/esm/index.d.ts +7 -4
- package/dist/esm/index.js +6 -3
- package/dist/esm/library.d.ts +18 -4
- package/dist/esm/library.js +87 -9
- package/dist/esm/library.test.js +321 -1
- package/dist/esm/serial-utils.d.ts +33 -0
- package/dist/esm/serial-utils.js +45 -0
- package/dist/esm/serial-utils.test.d.ts +1 -0
- package/dist/esm/serial-utils.test.js +48 -0
- package/dist/esm/token-storage.d.ts +14 -0
- package/dist/esm/token-storage.js +81 -0
- package/dist/esm/types.d.ts +49 -1
- package/eslint.config.mjs +12 -1
- package/package.json +5 -3
- package/src/buffer-utils.test.ts +225 -0
- package/src/buffer-utils.ts +83 -0
- package/src/cli.ts +192 -33
- package/src/constants.ts +5 -2
- package/src/index.ts +16 -2
- package/src/library.test.ts +402 -5
- package/src/library.ts +140 -14
- package/src/serial-utils.test.ts +64 -0
- package/src/serial-utils.ts +50 -0
- package/src/token-storage.ts +78 -0
- package/src/types.ts +60 -0
package/dist/esm/library.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as amplifyAuth from "aws-amplify/auth";
|
|
2
|
-
import { DeviceInfoType } from "./types";
|
|
2
|
+
import { DeviceAssociationResponse, DeviceInfoType } from "./types";
|
|
3
3
|
/**
|
|
4
4
|
* Generates headers with a JWT token for authenticated requests.
|
|
5
5
|
* @param {string} jwtToken - The JWT token for authorization.
|
|
@@ -8,15 +8,27 @@ import { DeviceInfoType } from "./types";
|
|
|
8
8
|
declare const headers: (jwtToken: string) => {
|
|
9
9
|
Authorization: string;
|
|
10
10
|
};
|
|
11
|
+
/**
|
|
12
|
+
* Configures Amplify if not already configured.
|
|
13
|
+
* Uses a local flag to avoid calling getConfig() which prints a warning.
|
|
14
|
+
* @param {object} [storage] - Optional custom storage adapter for token persistence
|
|
15
|
+
*/
|
|
16
|
+
declare const configureAmplify: (storage?: {
|
|
17
|
+
setItem: (key: string, value: string) => Promise<void>;
|
|
18
|
+
getItem: (key: string) => Promise<string | null>;
|
|
19
|
+
removeItem: (key: string) => Promise<void>;
|
|
20
|
+
clear: () => Promise<void>;
|
|
21
|
+
}) => void;
|
|
11
22
|
/**
|
|
12
23
|
* Creates an authentication service with sign-in functionality.
|
|
13
24
|
* @param {typeof amplifyAuth} auth - The authentication module to use.
|
|
14
25
|
* @returns {object} - An object containing authentication-related methods.
|
|
15
26
|
*/
|
|
16
27
|
declare const createAuthService: (auth: typeof amplifyAuth) => {
|
|
17
|
-
signIn: (username: string, password: string) => Promise<string>;
|
|
28
|
+
signIn: (username: string, password: string, legacy?: boolean) => Promise<string>;
|
|
29
|
+
getSession: (forceRefresh?: boolean, legacy?: boolean) => Promise<string>;
|
|
18
30
|
};
|
|
19
|
-
declare const signIn: (username: string, password: string) => Promise<string>;
|
|
31
|
+
declare const signIn: (username: string, password: string, legacy?: boolean) => Promise<string>, getSession: (forceRefresh?: boolean, legacy?: boolean) => Promise<string>;
|
|
20
32
|
/**
|
|
21
33
|
* Configures the library for API interactions.
|
|
22
34
|
* Initializes API methods with a specified base URL.
|
|
@@ -30,6 +42,8 @@ declare const signIn: (username: string, password: string) => Promise<string>;
|
|
|
30
42
|
*/
|
|
31
43
|
declare const configure: (baseURL?: string) => {
|
|
32
44
|
deviceInfo: (jwtToken: string, macAddress: string) => Promise<DeviceInfoType>;
|
|
45
|
+
registerDevice: (jwtToken: string, macAddress: string, serialNumber: string, deviceName?: string, deviceRoom?: string) => Promise<DeviceAssociationResponse>;
|
|
46
|
+
editDevice: (jwtToken: string, macAddress: string, deviceName?: string, deviceRoom?: string) => Promise<DeviceAssociationResponse>;
|
|
33
47
|
setPower: (jwtToken: string, macAddress: string, value: number) => Promise<import("axios").AxiosResponse<any, any, {}>>;
|
|
34
48
|
setPowerOff: (jwtToken: string, macAddress: string) => Promise<import("axios").AxiosResponse<any, any, {}>>;
|
|
35
49
|
setPowerOn: (jwtToken: string, macAddress: string) => Promise<import("axios").AxiosResponse<any, any, {}>>;
|
|
@@ -38,4 +52,4 @@ declare const configure: (baseURL?: string) => {
|
|
|
38
52
|
getTargetTemperature: (jwtToken: string, macAddress: string) => Promise<number>;
|
|
39
53
|
setTargetTemperature: (jwtToken: string, macAddress: string, temperature: number) => Promise<import("axios").AxiosResponse<any, any, {}>>;
|
|
40
54
|
};
|
|
41
|
-
export { configure, createAuthService, headers, signIn };
|
|
55
|
+
export { configure, configureAmplify, createAuthService, getSession, headers, signIn, };
|
package/dist/esm/library.js
CHANGED
|
@@ -10,7 +10,9 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
10
10
|
import { strict as assert } from "assert";
|
|
11
11
|
import { Amplify } from "aws-amplify";
|
|
12
12
|
import * as amplifyAuth from "aws-amplify/auth";
|
|
13
|
+
import { cognitoUserPoolsTokenProvider } from "aws-amplify/auth/cognito";
|
|
13
14
|
import axios from "axios";
|
|
15
|
+
import { processResponse } from "./buffer-utils";
|
|
14
16
|
import { API_URL } from "./constants";
|
|
15
17
|
const amplifyconfiguration = {
|
|
16
18
|
aws_project_region: "eu-central-1",
|
|
@@ -23,15 +25,20 @@ const amplifyconfiguration = {
|
|
|
23
25
|
* @returns {object} - The headers object with the Authorization field.
|
|
24
26
|
*/
|
|
25
27
|
const headers = (jwtToken) => ({ Authorization: `Bearer ${jwtToken}` });
|
|
28
|
+
let amplifyConfigured = false;
|
|
26
29
|
/**
|
|
27
30
|
* Configures Amplify if not already configured.
|
|
28
|
-
*
|
|
31
|
+
* Uses a local flag to avoid calling getConfig() which prints a warning.
|
|
32
|
+
* @param {object} [storage] - Optional custom storage adapter for token persistence
|
|
29
33
|
*/
|
|
30
|
-
const configureAmplify = () => {
|
|
31
|
-
|
|
32
|
-
if (Object.keys(currentConfig).length !== 0)
|
|
34
|
+
const configureAmplify = (storage) => {
|
|
35
|
+
if (amplifyConfigured)
|
|
33
36
|
return;
|
|
34
37
|
Amplify.configure(amplifyconfiguration);
|
|
38
|
+
if (storage) {
|
|
39
|
+
cognitoUserPoolsTokenProvider.setKeyValueStorage(storage);
|
|
40
|
+
}
|
|
41
|
+
amplifyConfigured = true;
|
|
35
42
|
};
|
|
36
43
|
/**
|
|
37
44
|
* Creates an authentication service with sign-in functionality.
|
|
@@ -43,26 +50,51 @@ const createAuthService = (auth) => {
|
|
|
43
50
|
* Signs in a user with the provided credentials.
|
|
44
51
|
* @param {string} username - The username of the user.
|
|
45
52
|
* @param {string} password - The password of the user.
|
|
53
|
+
* @param {boolean} [legacy=false] - If true, returns accessToken for legacy API.
|
|
46
54
|
* @returns {Promise<string>} - The JWT token of the signed-in user.
|
|
47
55
|
* @throws {Error} - If sign-in fails or no tokens are retrieved.
|
|
48
56
|
*/
|
|
49
|
-
const signIn = (
|
|
57
|
+
const signIn = (username_1, password_1, ...args_1) => __awaiter(void 0, [username_1, password_1, ...args_1], void 0, function* (username, password, legacy = false) {
|
|
50
58
|
configureAmplify();
|
|
51
59
|
yield auth.signOut(); // Ensure the user is signed out first
|
|
52
60
|
const { isSignedIn } = yield auth.signIn({ username, password });
|
|
53
61
|
assert.ok(isSignedIn, "Sign-in failed");
|
|
54
62
|
const { tokens } = yield auth.fetchAuthSession();
|
|
55
63
|
assert.ok(tokens, "No tokens found");
|
|
64
|
+
if (legacy) {
|
|
65
|
+
assert.ok(tokens.accessToken, "No access token found");
|
|
66
|
+
return tokens.accessToken.toString();
|
|
67
|
+
}
|
|
56
68
|
assert.ok(tokens.idToken, "No ID token found");
|
|
57
69
|
return tokens.idToken.toString();
|
|
58
70
|
});
|
|
59
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Retrieves the current session, refreshing tokens if necessary.
|
|
73
|
+
* Requires a prior successful signIn() call.
|
|
74
|
+
* @param {boolean} [forceRefresh=false] - Force token refresh even if valid
|
|
75
|
+
* @param {boolean} [legacy=false] - If true, returns accessToken for legacy API
|
|
76
|
+
* @returns {Promise<string>} - The JWT token (idToken or accessToken)
|
|
77
|
+
* @throws {Error} - If no session exists (user needs to sign in)
|
|
78
|
+
*/
|
|
79
|
+
const getSession = (...args_1) => __awaiter(void 0, [...args_1], void 0, function* (forceRefresh = false, legacy = false) {
|
|
80
|
+
configureAmplify();
|
|
81
|
+
const { tokens } = yield auth.fetchAuthSession({ forceRefresh });
|
|
82
|
+
assert.ok(tokens, "No session found - please sign in first");
|
|
83
|
+
if (legacy) {
|
|
84
|
+
assert.ok(tokens.accessToken, "No access token found");
|
|
85
|
+
return tokens.accessToken.toString();
|
|
86
|
+
}
|
|
87
|
+
assert.ok(tokens.idToken, "No ID token found");
|
|
88
|
+
return tokens.idToken.toString();
|
|
89
|
+
});
|
|
90
|
+
return { signIn, getSession };
|
|
60
91
|
};
|
|
61
92
|
// Create the default auth service using amplifyAuth
|
|
62
|
-
const { signIn } = createAuthService(amplifyAuth);
|
|
93
|
+
const { signIn, getSession } = createAuthService(amplifyAuth);
|
|
63
94
|
const deviceInfo = (axiosInstance) =>
|
|
64
95
|
/**
|
|
65
96
|
* Retrieves information about a device by its MAC address.
|
|
97
|
+
* Automatically decompresses any gzip-compressed Buffer fields in the response.
|
|
66
98
|
*
|
|
67
99
|
* @param {string} jwtToken - The JWT token for authentication.
|
|
68
100
|
* @param {string} macAddress - The MAC address of the device.
|
|
@@ -72,7 +104,8 @@ const deviceInfo = (axiosInstance) =>
|
|
|
72
104
|
const response = yield axiosInstance.get(`device/${macAddress}/info`, {
|
|
73
105
|
headers: headers(jwtToken),
|
|
74
106
|
});
|
|
75
|
-
|
|
107
|
+
// Process response to decompress any gzipped Buffer fields
|
|
108
|
+
return processResponse(response.data);
|
|
76
109
|
});
|
|
77
110
|
const mqttCommand = (axiosInstance) =>
|
|
78
111
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -162,6 +195,47 @@ const setTargetTemperature = (axiosInstance) =>
|
|
|
162
195
|
name: "enviroment_1_temperature",
|
|
163
196
|
value: temperature,
|
|
164
197
|
});
|
|
198
|
+
const registerDevice = (axiosInstance) =>
|
|
199
|
+
/**
|
|
200
|
+
* Registers a device with the user's account.
|
|
201
|
+
* This must be called before other device operations will work on the new API.
|
|
202
|
+
*
|
|
203
|
+
* @param {string} jwtToken - The JWT token for authentication.
|
|
204
|
+
* @param {string} macAddress - The MAC address of the device (colons optional).
|
|
205
|
+
* @param {string} serialNumber - The device serial number.
|
|
206
|
+
* @param {string} deviceName - User-friendly name for the device (default: empty string).
|
|
207
|
+
* @param {string} deviceRoom - Room name for the device (default: empty string).
|
|
208
|
+
* @returns {Promise<DeviceAssociationResponse>} - A promise that resolves to the registration response.
|
|
209
|
+
*/
|
|
210
|
+
(jwtToken_1, macAddress_1, serialNumber_1, ...args_1) => __awaiter(void 0, [jwtToken_1, macAddress_1, serialNumber_1, ...args_1], void 0, function* (jwtToken, macAddress, serialNumber, deviceName = "", deviceRoom = "") {
|
|
211
|
+
const body = {
|
|
212
|
+
macAddress: macAddress.replace(/:/g, ""),
|
|
213
|
+
deviceName,
|
|
214
|
+
deviceRoom,
|
|
215
|
+
serialNumber,
|
|
216
|
+
};
|
|
217
|
+
const response = yield axiosInstance.post("device", body, { headers: headers(jwtToken) });
|
|
218
|
+
return response.data;
|
|
219
|
+
});
|
|
220
|
+
const editDevice = (axiosInstance) =>
|
|
221
|
+
/**
|
|
222
|
+
* Updates a device's name and room.
|
|
223
|
+
*
|
|
224
|
+
* @param {string} jwtToken - The JWT token for authentication.
|
|
225
|
+
* @param {string} macAddress - The MAC address of the device (colons optional).
|
|
226
|
+
* @param {string} deviceName - New name for the device (default: empty string).
|
|
227
|
+
* @param {string} deviceRoom - New room for the device (default: empty string).
|
|
228
|
+
* @returns {Promise<DeviceAssociationResponse>} - A promise that resolves to the update response.
|
|
229
|
+
*/
|
|
230
|
+
(jwtToken_1, macAddress_1, ...args_1) => __awaiter(void 0, [jwtToken_1, macAddress_1, ...args_1], void 0, function* (jwtToken, macAddress, deviceName = "", deviceRoom = "") {
|
|
231
|
+
const normalizedMac = macAddress.replace(/:/g, "");
|
|
232
|
+
const body = {
|
|
233
|
+
deviceName,
|
|
234
|
+
deviceRoom,
|
|
235
|
+
};
|
|
236
|
+
const response = yield axiosInstance.put(`device/${normalizedMac}`, body, { headers: headers(jwtToken) });
|
|
237
|
+
return response.data;
|
|
238
|
+
});
|
|
165
239
|
/**
|
|
166
240
|
* Configures the library for API interactions.
|
|
167
241
|
* Initializes API methods with a specified base URL.
|
|
@@ -176,6 +250,8 @@ const setTargetTemperature = (axiosInstance) =>
|
|
|
176
250
|
const configure = (baseURL = API_URL) => {
|
|
177
251
|
const axiosInstance = axios.create({ baseURL });
|
|
178
252
|
const deviceInfoInstance = deviceInfo(axiosInstance);
|
|
253
|
+
const registerDeviceInstance = registerDevice(axiosInstance);
|
|
254
|
+
const editDeviceInstance = editDevice(axiosInstance);
|
|
179
255
|
const setPowerInstance = setPower(axiosInstance);
|
|
180
256
|
const setPowerOffInstance = setPowerOff(axiosInstance);
|
|
181
257
|
const setPowerOnInstance = setPowerOn(axiosInstance);
|
|
@@ -185,6 +261,8 @@ const configure = (baseURL = API_URL) => {
|
|
|
185
261
|
const setTargetTemperatureInstance = setTargetTemperature(axiosInstance);
|
|
186
262
|
return {
|
|
187
263
|
deviceInfo: deviceInfoInstance,
|
|
264
|
+
registerDevice: registerDeviceInstance,
|
|
265
|
+
editDevice: editDeviceInstance,
|
|
188
266
|
setPower: setPowerInstance,
|
|
189
267
|
setPowerOff: setPowerOffInstance,
|
|
190
268
|
setPowerOn: setPowerOnInstance,
|
|
@@ -194,4 +272,4 @@ const configure = (baseURL = API_URL) => {
|
|
|
194
272
|
setTargetTemperature: setTargetTemperatureInstance,
|
|
195
273
|
};
|
|
196
274
|
};
|
|
197
|
-
export { configure, createAuthService, headers, signIn };
|
|
275
|
+
export { configure, configureAmplify, createAuthService, getSession, headers, signIn, };
|
package/dist/esm/library.test.js
CHANGED
|
@@ -9,9 +9,21 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
9
9
|
};
|
|
10
10
|
import { strict as assert } from "assert";
|
|
11
11
|
import axios from "axios";
|
|
12
|
+
import pako from "pako";
|
|
12
13
|
import sinon from "sinon";
|
|
13
14
|
import { configure, createAuthService } from "../src/library";
|
|
14
15
|
import { API_URL } from "./constants";
|
|
16
|
+
/**
|
|
17
|
+
* Helper to create a gzip-compressed Buffer object for testing.
|
|
18
|
+
*/
|
|
19
|
+
const createGzippedBuffer = (data) => {
|
|
20
|
+
const json = JSON.stringify(data);
|
|
21
|
+
const compressed = pako.gzip(json);
|
|
22
|
+
return {
|
|
23
|
+
type: "Buffer",
|
|
24
|
+
data: Array.from(compressed),
|
|
25
|
+
};
|
|
26
|
+
};
|
|
15
27
|
describe("library", () => {
|
|
16
28
|
let axiosStub;
|
|
17
29
|
const expectedToken = "mockJwtToken";
|
|
@@ -26,7 +38,7 @@ describe("library", () => {
|
|
|
26
38
|
sinon.restore();
|
|
27
39
|
});
|
|
28
40
|
describe("signIn", () => {
|
|
29
|
-
it("should sign in and return the
|
|
41
|
+
it("should sign in and return the ID token by default", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
30
42
|
const expectedUsername = "testuser";
|
|
31
43
|
const expectedPassword = "testpassword";
|
|
32
44
|
const signIn = sinon.stub().resolves({ isSignedIn: true });
|
|
@@ -34,6 +46,7 @@ describe("library", () => {
|
|
|
34
46
|
const fetchAuthSession = sinon.stub().resolves({
|
|
35
47
|
tokens: {
|
|
36
48
|
idToken: { toString: () => expectedToken },
|
|
49
|
+
accessToken: { toString: () => "accessToken" },
|
|
37
50
|
},
|
|
38
51
|
});
|
|
39
52
|
const authStub = {
|
|
@@ -50,6 +63,27 @@ describe("library", () => {
|
|
|
50
63
|
]);
|
|
51
64
|
assert.equal(token, expectedToken);
|
|
52
65
|
}));
|
|
66
|
+
it("should sign in and return the access token in legacy mode", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
67
|
+
const expectedUsername = "testuser";
|
|
68
|
+
const expectedPassword = "testpassword";
|
|
69
|
+
const signIn = sinon.stub().resolves({ isSignedIn: true });
|
|
70
|
+
const signOut = sinon.stub();
|
|
71
|
+
const fetchAuthSession = sinon.stub().resolves({
|
|
72
|
+
tokens: {
|
|
73
|
+
accessToken: { toString: () => expectedToken },
|
|
74
|
+
idToken: { toString: () => "idToken" },
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
const authStub = {
|
|
78
|
+
signIn,
|
|
79
|
+
signOut,
|
|
80
|
+
fetchAuthSession,
|
|
81
|
+
};
|
|
82
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
83
|
+
const authService = createAuthService(authStub);
|
|
84
|
+
const token = yield authService.signIn(expectedUsername, expectedPassword, true);
|
|
85
|
+
assert.equal(token, expectedToken);
|
|
86
|
+
}));
|
|
53
87
|
it("should throw an error if sign-in fails", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
54
88
|
const expectedUsername = "testuser";
|
|
55
89
|
const expectedPassword = "testpassword";
|
|
@@ -73,9 +107,73 @@ describe("library", () => {
|
|
|
73
107
|
});
|
|
74
108
|
}));
|
|
75
109
|
});
|
|
110
|
+
describe("getSession", () => {
|
|
111
|
+
it("should return idToken by default", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
112
|
+
const mockAuth = {
|
|
113
|
+
signIn: sinon.stub().resolves({ isSignedIn: true }),
|
|
114
|
+
signOut: sinon.stub().resolves(),
|
|
115
|
+
fetchAuthSession: sinon.stub().resolves({
|
|
116
|
+
tokens: {
|
|
117
|
+
idToken: { toString: () => "mock-id-token" },
|
|
118
|
+
accessToken: { toString: () => "mock-access-token" },
|
|
119
|
+
},
|
|
120
|
+
}),
|
|
121
|
+
};
|
|
122
|
+
const { getSession, signIn } = createAuthService(mockAuth);
|
|
123
|
+
yield signIn("user", "pass");
|
|
124
|
+
const token = yield getSession();
|
|
125
|
+
assert.equal(token, "mock-id-token");
|
|
126
|
+
}));
|
|
127
|
+
it("should return accessToken when legacy=true", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
128
|
+
const mockAuth = {
|
|
129
|
+
signIn: sinon.stub().resolves({ isSignedIn: true }),
|
|
130
|
+
signOut: sinon.stub().resolves(),
|
|
131
|
+
fetchAuthSession: sinon.stub().resolves({
|
|
132
|
+
tokens: {
|
|
133
|
+
idToken: { toString: () => "mock-id-token" },
|
|
134
|
+
accessToken: { toString: () => "mock-access-token" },
|
|
135
|
+
},
|
|
136
|
+
}),
|
|
137
|
+
};
|
|
138
|
+
const { getSession, signIn } = createAuthService(mockAuth);
|
|
139
|
+
yield signIn("user", "pass");
|
|
140
|
+
const token = yield getSession(false, true);
|
|
141
|
+
assert.equal(token, "mock-access-token");
|
|
142
|
+
}));
|
|
143
|
+
it("should throw error when no session exists", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
144
|
+
const mockAuth = {
|
|
145
|
+
signIn: sinon.stub().resolves({ isSignedIn: true }),
|
|
146
|
+
signOut: sinon.stub().resolves(),
|
|
147
|
+
fetchAuthSession: sinon.stub().resolves({ tokens: null }),
|
|
148
|
+
};
|
|
149
|
+
const { getSession } = createAuthService(mockAuth);
|
|
150
|
+
yield assert.rejects(() => __awaiter(void 0, void 0, void 0, function* () { return getSession(); }), {
|
|
151
|
+
name: "AssertionError",
|
|
152
|
+
message: "No session found - please sign in first",
|
|
153
|
+
});
|
|
154
|
+
}));
|
|
155
|
+
it("should pass forceRefresh to fetchAuthSession", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
156
|
+
const mockAuth = {
|
|
157
|
+
signIn: sinon.stub().resolves({ isSignedIn: true }),
|
|
158
|
+
signOut: sinon.stub().resolves(),
|
|
159
|
+
fetchAuthSession: sinon.stub().resolves({
|
|
160
|
+
tokens: {
|
|
161
|
+
idToken: { toString: () => "mock-id-token" },
|
|
162
|
+
accessToken: { toString: () => "mock-access-token" },
|
|
163
|
+
},
|
|
164
|
+
}),
|
|
165
|
+
};
|
|
166
|
+
const { getSession, signIn } = createAuthService(mockAuth);
|
|
167
|
+
yield signIn("user", "pass");
|
|
168
|
+
yield getSession(true);
|
|
169
|
+
assert.ok(mockAuth.fetchAuthSession.calledWith({ forceRefresh: true }));
|
|
170
|
+
}));
|
|
171
|
+
});
|
|
76
172
|
describe("configure", () => {
|
|
77
173
|
const expectedApi = [
|
|
78
174
|
"deviceInfo",
|
|
175
|
+
"registerDevice",
|
|
176
|
+
"editDevice",
|
|
79
177
|
"setPower",
|
|
80
178
|
"setPowerOff",
|
|
81
179
|
"setPowerOn",
|
|
@@ -242,4 +340,226 @@ describe("library", () => {
|
|
|
242
340
|
}));
|
|
243
341
|
});
|
|
244
342
|
});
|
|
343
|
+
describe("registerDevice", () => {
|
|
344
|
+
it("should call POST /device with correct payload", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
345
|
+
const mockResponse = {
|
|
346
|
+
macAddress: "AABBCCDDEEFF",
|
|
347
|
+
deviceName: "Test Stove",
|
|
348
|
+
deviceRoom: "Living Room",
|
|
349
|
+
serialNumber: "EDK123",
|
|
350
|
+
};
|
|
351
|
+
const mockAxios = {
|
|
352
|
+
post: sinon.stub().resolves({ data: mockResponse }),
|
|
353
|
+
get: sinon.stub(),
|
|
354
|
+
put: sinon.stub(),
|
|
355
|
+
};
|
|
356
|
+
axiosStub.returns(mockAxios);
|
|
357
|
+
const api = configure("https://example.com/api");
|
|
358
|
+
const result = yield api.registerDevice(expectedToken, "AA:BB:CC:DD:EE:FF", "EDK123", "Test Stove", "Living Room");
|
|
359
|
+
assert.deepEqual(mockAxios.post.args, [
|
|
360
|
+
[
|
|
361
|
+
"device",
|
|
362
|
+
{
|
|
363
|
+
macAddress: "AABBCCDDEEFF",
|
|
364
|
+
deviceName: "Test Stove",
|
|
365
|
+
deviceRoom: "Living Room",
|
|
366
|
+
serialNumber: "EDK123",
|
|
367
|
+
},
|
|
368
|
+
{ headers: { Authorization: `Bearer ${expectedToken}` } },
|
|
369
|
+
],
|
|
370
|
+
]);
|
|
371
|
+
assert.deepEqual(result, mockResponse);
|
|
372
|
+
}));
|
|
373
|
+
it("should normalize MAC address by removing colons", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
374
|
+
const mockAxios = {
|
|
375
|
+
post: sinon.stub().resolves({ data: {} }),
|
|
376
|
+
get: sinon.stub(),
|
|
377
|
+
put: sinon.stub(),
|
|
378
|
+
};
|
|
379
|
+
axiosStub.returns(mockAxios);
|
|
380
|
+
const api = configure("https://example.com/api");
|
|
381
|
+
yield api.registerDevice(expectedToken, "AA:BB:CC:DD:EE:FF", "EDK123");
|
|
382
|
+
assert.equal(mockAxios.post.args[0][1].macAddress, "AABBCCDDEEFF");
|
|
383
|
+
}));
|
|
384
|
+
it("should use empty strings as defaults for name and room", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
385
|
+
const mockAxios = {
|
|
386
|
+
post: sinon.stub().resolves({ data: {} }),
|
|
387
|
+
get: sinon.stub(),
|
|
388
|
+
put: sinon.stub(),
|
|
389
|
+
};
|
|
390
|
+
axiosStub.returns(mockAxios);
|
|
391
|
+
const api = configure("https://example.com/api");
|
|
392
|
+
yield api.registerDevice(expectedToken, "AABBCCDDEEFF", "EDK123");
|
|
393
|
+
assert.equal(mockAxios.post.args[0][1].deviceName, "");
|
|
394
|
+
assert.equal(mockAxios.post.args[0][1].deviceRoom, "");
|
|
395
|
+
}));
|
|
396
|
+
});
|
|
397
|
+
describe("editDevice", () => {
|
|
398
|
+
it("should call PUT /device/{mac} with correct payload", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
399
|
+
const mockResponse = {
|
|
400
|
+
macAddress: "AABBCCDDEEFF",
|
|
401
|
+
deviceName: "Updated Name",
|
|
402
|
+
deviceRoom: "Basement",
|
|
403
|
+
serialNumber: "EDK123",
|
|
404
|
+
};
|
|
405
|
+
const mockAxios = {
|
|
406
|
+
put: sinon.stub().resolves({ data: mockResponse }),
|
|
407
|
+
get: sinon.stub(),
|
|
408
|
+
post: sinon.stub(),
|
|
409
|
+
};
|
|
410
|
+
axiosStub.returns(mockAxios);
|
|
411
|
+
const api = configure("https://example.com/api");
|
|
412
|
+
const result = yield api.editDevice(expectedToken, "AA:BB:CC:DD:EE:FF", "Updated Name", "Basement");
|
|
413
|
+
assert.deepEqual(mockAxios.put.args, [
|
|
414
|
+
[
|
|
415
|
+
"device/AABBCCDDEEFF",
|
|
416
|
+
{
|
|
417
|
+
deviceName: "Updated Name",
|
|
418
|
+
deviceRoom: "Basement",
|
|
419
|
+
},
|
|
420
|
+
{ headers: { Authorization: `Bearer ${expectedToken}` } },
|
|
421
|
+
],
|
|
422
|
+
]);
|
|
423
|
+
assert.deepEqual(result, mockResponse);
|
|
424
|
+
}));
|
|
425
|
+
it("should use empty strings as defaults for name and room", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
426
|
+
const mockAxios = {
|
|
427
|
+
put: sinon.stub().resolves({ data: {} }),
|
|
428
|
+
get: sinon.stub(),
|
|
429
|
+
post: sinon.stub(),
|
|
430
|
+
};
|
|
431
|
+
axiosStub.returns(mockAxios);
|
|
432
|
+
const api = configure("https://example.com/api");
|
|
433
|
+
yield api.editDevice(expectedToken, "AABBCCDDEEFF");
|
|
434
|
+
assert.equal(mockAxios.put.args[0][1].deviceName, "");
|
|
435
|
+
assert.equal(mockAxios.put.args[0][1].deviceRoom, "");
|
|
436
|
+
}));
|
|
437
|
+
});
|
|
438
|
+
describe("deviceInfo with compressed responses", () => {
|
|
439
|
+
it("should decompress Buffer-encoded status field", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
440
|
+
const statusData = {
|
|
441
|
+
commands: { power: true },
|
|
442
|
+
temperatures: { enviroment: 19, board: 25 },
|
|
443
|
+
};
|
|
444
|
+
const mockResponse = {
|
|
445
|
+
status: createGzippedBuffer(statusData),
|
|
446
|
+
nvm: {
|
|
447
|
+
user_parameters: {
|
|
448
|
+
enviroment_1_temperature: 22,
|
|
449
|
+
},
|
|
450
|
+
},
|
|
451
|
+
};
|
|
452
|
+
const mockAxios = {
|
|
453
|
+
get: sinon.stub().resolves({ data: mockResponse }),
|
|
454
|
+
};
|
|
455
|
+
axiosStub.returns(mockAxios);
|
|
456
|
+
const api = configure("https://example.com/api");
|
|
457
|
+
const result = yield api.deviceInfo(expectedToken, "mockMacAddress");
|
|
458
|
+
assert.deepEqual(result.status, statusData);
|
|
459
|
+
}));
|
|
460
|
+
it("should decompress Buffer-encoded nvm field", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
461
|
+
const nvmData = {
|
|
462
|
+
user_parameters: {
|
|
463
|
+
enviroment_1_temperature: 22,
|
|
464
|
+
enviroment_2_temperature: 0,
|
|
465
|
+
enviroment_3_temperature: 0,
|
|
466
|
+
is_auto: false,
|
|
467
|
+
is_sound_active: true,
|
|
468
|
+
},
|
|
469
|
+
};
|
|
470
|
+
const mockResponse = {
|
|
471
|
+
status: {
|
|
472
|
+
commands: { power: true },
|
|
473
|
+
temperatures: { enviroment: 19 },
|
|
474
|
+
},
|
|
475
|
+
nvm: createGzippedBuffer(nvmData),
|
|
476
|
+
};
|
|
477
|
+
const mockAxios = {
|
|
478
|
+
get: sinon.stub().resolves({ data: mockResponse }),
|
|
479
|
+
};
|
|
480
|
+
axiosStub.returns(mockAxios);
|
|
481
|
+
const api = configure("https://example.com/api");
|
|
482
|
+
const result = yield api.deviceInfo(expectedToken, "mockMacAddress");
|
|
483
|
+
assert.deepEqual(result.nvm, nvmData);
|
|
484
|
+
}));
|
|
485
|
+
it("should handle fully compressed response (status and nvm)", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
486
|
+
const statusData = {
|
|
487
|
+
commands: { power: false },
|
|
488
|
+
temperatures: { enviroment: 21, board: 30 },
|
|
489
|
+
};
|
|
490
|
+
const nvmData = {
|
|
491
|
+
user_parameters: {
|
|
492
|
+
enviroment_1_temperature: 20,
|
|
493
|
+
enviroment_2_temperature: 0,
|
|
494
|
+
enviroment_3_temperature: 0,
|
|
495
|
+
is_auto: true,
|
|
496
|
+
is_sound_active: false,
|
|
497
|
+
},
|
|
498
|
+
};
|
|
499
|
+
const mockResponse = {
|
|
500
|
+
status: createGzippedBuffer(statusData),
|
|
501
|
+
nvm: createGzippedBuffer(nvmData),
|
|
502
|
+
};
|
|
503
|
+
const mockAxios = {
|
|
504
|
+
get: sinon.stub().resolves({ data: mockResponse }),
|
|
505
|
+
};
|
|
506
|
+
axiosStub.returns(mockAxios);
|
|
507
|
+
const api = configure("https://example.com/api");
|
|
508
|
+
const result = yield api.deviceInfo(expectedToken, "mockMacAddress");
|
|
509
|
+
assert.deepEqual(result.status, statusData);
|
|
510
|
+
assert.deepEqual(result.nvm, nvmData);
|
|
511
|
+
}));
|
|
512
|
+
it("should work with getPower on compressed response", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
513
|
+
const statusData = {
|
|
514
|
+
commands: { power: true },
|
|
515
|
+
temperatures: { enviroment: 19 },
|
|
516
|
+
};
|
|
517
|
+
const mockResponse = {
|
|
518
|
+
status: createGzippedBuffer(statusData),
|
|
519
|
+
nvm: { user_parameters: { enviroment_1_temperature: 22 } },
|
|
520
|
+
};
|
|
521
|
+
const mockAxios = {
|
|
522
|
+
get: sinon.stub().resolves({ data: mockResponse }),
|
|
523
|
+
};
|
|
524
|
+
axiosStub.returns(mockAxios);
|
|
525
|
+
const api = configure("https://example.com/api");
|
|
526
|
+
const result = yield api.getPower(expectedToken, "mockMacAddress");
|
|
527
|
+
assert.equal(result, true);
|
|
528
|
+
}));
|
|
529
|
+
it("should work with getEnvironmentTemperature on compressed response", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
530
|
+
const statusData = {
|
|
531
|
+
commands: { power: true },
|
|
532
|
+
temperatures: { enviroment: 19, board: 25 },
|
|
533
|
+
};
|
|
534
|
+
const mockResponse = {
|
|
535
|
+
status: createGzippedBuffer(statusData),
|
|
536
|
+
nvm: { user_parameters: { enviroment_1_temperature: 22 } },
|
|
537
|
+
};
|
|
538
|
+
const mockAxios = {
|
|
539
|
+
get: sinon.stub().resolves({ data: mockResponse }),
|
|
540
|
+
};
|
|
541
|
+
axiosStub.returns(mockAxios);
|
|
542
|
+
const api = configure("https://example.com/api");
|
|
543
|
+
const result = yield api.getEnvironmentTemperature(expectedToken, "mockMacAddress");
|
|
544
|
+
assert.equal(result, 19);
|
|
545
|
+
}));
|
|
546
|
+
it("should work with getTargetTemperature on compressed response", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
547
|
+
const nvmData = {
|
|
548
|
+
user_parameters: {
|
|
549
|
+
enviroment_1_temperature: 22,
|
|
550
|
+
},
|
|
551
|
+
};
|
|
552
|
+
const mockResponse = {
|
|
553
|
+
status: { commands: { power: true }, temperatures: { enviroment: 19 } },
|
|
554
|
+
nvm: createGzippedBuffer(nvmData),
|
|
555
|
+
};
|
|
556
|
+
const mockAxios = {
|
|
557
|
+
get: sinon.stub().resolves({ data: mockResponse }),
|
|
558
|
+
};
|
|
559
|
+
axiosStub.returns(mockAxios);
|
|
560
|
+
const api = configure("https://example.com/api");
|
|
561
|
+
const result = yield api.getTargetTemperature(expectedToken, "mockMacAddress");
|
|
562
|
+
assert.equal(result, 22);
|
|
563
|
+
}));
|
|
564
|
+
});
|
|
245
565
|
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts a raw serial number string to hex-encoded format.
|
|
3
|
+
* This is useful when serial numbers contain non-printable characters.
|
|
4
|
+
*
|
|
5
|
+
* @param serial - The raw serial number string
|
|
6
|
+
* @returns Hex-encoded string representation
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* serialNumberToHex("EDK123") // returns "45444b313233"
|
|
10
|
+
*/
|
|
11
|
+
declare const serialNumberToHex: (serial: string) => string;
|
|
12
|
+
/**
|
|
13
|
+
* Converts a hex-encoded serial number back to raw string format.
|
|
14
|
+
*
|
|
15
|
+
* @param hex - The hex-encoded serial number
|
|
16
|
+
* @returns Raw serial number string
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* serialNumberFromHex("45444b313233") // returns "EDK123"
|
|
20
|
+
*/
|
|
21
|
+
declare const serialNumberFromHex: (hex: string) => string;
|
|
22
|
+
/**
|
|
23
|
+
* Produces a display-friendly version of a serial number by removing
|
|
24
|
+
* non-printable characters and collapsing whitespace.
|
|
25
|
+
*
|
|
26
|
+
* @param serial - The raw serial number string
|
|
27
|
+
* @returns Display-friendly serial number
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* serialNumberDisplay("EDK\x00123\x1F") // returns "EDK123"
|
|
31
|
+
*/
|
|
32
|
+
declare const serialNumberDisplay: (serial: string) => string;
|
|
33
|
+
export { serialNumberDisplay, serialNumberFromHex, serialNumberToHex };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts a raw serial number string to hex-encoded format.
|
|
3
|
+
* This is useful when serial numbers contain non-printable characters.
|
|
4
|
+
*
|
|
5
|
+
* @param serial - The raw serial number string
|
|
6
|
+
* @returns Hex-encoded string representation
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* serialNumberToHex("EDK123") // returns "45444b313233"
|
|
10
|
+
*/
|
|
11
|
+
const serialNumberToHex = (serial) => {
|
|
12
|
+
return Buffer.from(serial, "utf-8").toString("hex");
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Converts a hex-encoded serial number back to raw string format.
|
|
16
|
+
*
|
|
17
|
+
* @param hex - The hex-encoded serial number
|
|
18
|
+
* @returns Raw serial number string
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* serialNumberFromHex("45444b313233") // returns "EDK123"
|
|
22
|
+
*/
|
|
23
|
+
const serialNumberFromHex = (hex) => {
|
|
24
|
+
return Buffer.from(hex, "hex").toString("utf-8");
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Produces a display-friendly version of a serial number by removing
|
|
28
|
+
* non-printable characters and collapsing whitespace.
|
|
29
|
+
*
|
|
30
|
+
* @param serial - The raw serial number string
|
|
31
|
+
* @returns Display-friendly serial number
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* serialNumberDisplay("EDK\x00123\x1F") // returns "EDK123"
|
|
35
|
+
*/
|
|
36
|
+
const serialNumberDisplay = (serial) => {
|
|
37
|
+
// Remove non-printable characters (ASCII 0-31, 127)
|
|
38
|
+
// Keep printable ASCII (32-126) and extended characters
|
|
39
|
+
return (serial
|
|
40
|
+
// eslint-disable-next-line no-control-regex
|
|
41
|
+
.replace(/[\x00-\x1F\x7F]/g, "")
|
|
42
|
+
.replace(/\s+/g, " ")
|
|
43
|
+
.trim());
|
|
44
|
+
};
|
|
45
|
+
export { serialNumberDisplay, serialNumberFromHex, serialNumberToHex };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|