edilkamin 1.6.2 → 1.7.3
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 +6 -4
- package/.github/workflows/tests.yml +1 -1
- package/README.md +31 -7
- package/dist/esm/browser-bundle.test.d.ts +1 -0
- package/dist/esm/browser-bundle.test.js +29 -0
- package/dist/esm/cli.js +69 -10
- package/dist/esm/configureAmplify.test.d.ts +1 -0
- package/dist/esm/configureAmplify.test.js +37 -0
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/index.js +1 -1
- package/dist/esm/library.d.ts +18 -6
- package/dist/esm/library.js +87 -55
- package/dist/esm/library.test.js +230 -192
- package/dist/esm/token-storage.d.ts +14 -0
- package/dist/esm/token-storage.js +81 -0
- package/eslint.config.mjs +12 -1
- package/package.json +15 -3
- package/src/browser-bundle.test.ts +21 -0
- package/src/buffer-utils.test.ts +1 -1
- package/src/cli.ts +113 -40
- package/src/configureAmplify.test.ts +47 -0
- package/src/index.ts +1 -1
- package/src/library.test.ts +279 -206
- package/src/library.ts +125 -70
- package/src/token-storage.ts +78 -0
package/src/library.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { strict as assert } from "assert";
|
|
2
2
|
import { Amplify } from "aws-amplify";
|
|
3
3
|
import * as amplifyAuth from "aws-amplify/auth";
|
|
4
|
-
import
|
|
4
|
+
import { cognitoUserPoolsTokenProvider } from "aws-amplify/auth/cognito";
|
|
5
5
|
|
|
6
6
|
import { processResponse } from "./buffer-utils";
|
|
7
7
|
import { API_URL } from "./constants";
|
|
@@ -13,6 +13,22 @@ import {
|
|
|
13
13
|
EditDeviceAssociationBody,
|
|
14
14
|
} from "./types";
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Makes a fetch request and returns parsed JSON response.
|
|
18
|
+
* Throws an error for non-2xx status codes.
|
|
19
|
+
*/
|
|
20
|
+
const fetchJson = async <T>(
|
|
21
|
+
baseURL: string,
|
|
22
|
+
path: string,
|
|
23
|
+
options: RequestInit = {},
|
|
24
|
+
): Promise<T> => {
|
|
25
|
+
const response = await fetch(`${baseURL}${path}`, options);
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
28
|
+
}
|
|
29
|
+
return response.json();
|
|
30
|
+
};
|
|
31
|
+
|
|
16
32
|
const amplifyconfiguration = {
|
|
17
33
|
aws_project_region: "eu-central-1",
|
|
18
34
|
aws_user_pools_id: "eu-central-1_BYmQ2VBlo",
|
|
@@ -31,10 +47,19 @@ let amplifyConfigured = false;
|
|
|
31
47
|
/**
|
|
32
48
|
* Configures Amplify if not already configured.
|
|
33
49
|
* Uses a local flag to avoid calling getConfig() which prints a warning.
|
|
50
|
+
* @param {object} [storage] - Optional custom storage adapter for token persistence
|
|
34
51
|
*/
|
|
35
|
-
const configureAmplify = (
|
|
52
|
+
const configureAmplify = (storage?: {
|
|
53
|
+
setItem: (key: string, value: string) => Promise<void>;
|
|
54
|
+
getItem: (key: string) => Promise<string | null>;
|
|
55
|
+
removeItem: (key: string) => Promise<void>;
|
|
56
|
+
clear: () => Promise<void>;
|
|
57
|
+
}) => {
|
|
36
58
|
if (amplifyConfigured) return;
|
|
37
59
|
Amplify.configure(amplifyconfiguration);
|
|
60
|
+
if (storage) {
|
|
61
|
+
cognitoUserPoolsTokenProvider.setKeyValueStorage(storage);
|
|
62
|
+
}
|
|
38
63
|
amplifyConfigured = true;
|
|
39
64
|
};
|
|
40
65
|
|
|
@@ -55,7 +80,7 @@ const createAuthService = (auth: typeof amplifyAuth) => {
|
|
|
55
80
|
const signIn = async (
|
|
56
81
|
username: string,
|
|
57
82
|
password: string,
|
|
58
|
-
legacy: boolean = false
|
|
83
|
+
legacy: boolean = false,
|
|
59
84
|
): Promise<string> => {
|
|
60
85
|
configureAmplify();
|
|
61
86
|
await auth.signOut(); // Ensure the user is signed out first
|
|
@@ -70,14 +95,38 @@ const createAuthService = (auth: typeof amplifyAuth) => {
|
|
|
70
95
|
assert.ok(tokens.idToken, "No ID token found");
|
|
71
96
|
return tokens.idToken.toString();
|
|
72
97
|
};
|
|
73
|
-
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Retrieves the current session, refreshing tokens if necessary.
|
|
101
|
+
* Requires a prior successful signIn() call.
|
|
102
|
+
* @param {boolean} [forceRefresh=false] - Force token refresh even if valid
|
|
103
|
+
* @param {boolean} [legacy=false] - If true, returns accessToken for legacy API
|
|
104
|
+
* @returns {Promise<string>} - The JWT token (idToken or accessToken)
|
|
105
|
+
* @throws {Error} - If no session exists (user needs to sign in)
|
|
106
|
+
*/
|
|
107
|
+
const getSession = async (
|
|
108
|
+
forceRefresh: boolean = false,
|
|
109
|
+
legacy: boolean = false,
|
|
110
|
+
): Promise<string> => {
|
|
111
|
+
configureAmplify();
|
|
112
|
+
const { tokens } = await auth.fetchAuthSession({ forceRefresh });
|
|
113
|
+
assert.ok(tokens, "No session found - please sign in first");
|
|
114
|
+
if (legacy) {
|
|
115
|
+
assert.ok(tokens.accessToken, "No access token found");
|
|
116
|
+
return tokens.accessToken.toString();
|
|
117
|
+
}
|
|
118
|
+
assert.ok(tokens.idToken, "No ID token found");
|
|
119
|
+
return tokens.idToken.toString();
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
return { signIn, getSession };
|
|
74
123
|
};
|
|
75
124
|
|
|
76
125
|
// Create the default auth service using amplifyAuth
|
|
77
|
-
const { signIn } = createAuthService(amplifyAuth);
|
|
126
|
+
const { signIn, getSession } = createAuthService(amplifyAuth);
|
|
78
127
|
|
|
79
128
|
const deviceInfo =
|
|
80
|
-
(
|
|
129
|
+
(baseURL: string) =>
|
|
81
130
|
/**
|
|
82
131
|
* Retrieves information about a device by its MAC address.
|
|
83
132
|
* Automatically decompresses any gzip-compressed Buffer fields in the response.
|
|
@@ -87,28 +136,33 @@ const deviceInfo =
|
|
|
87
136
|
* @returns {Promise<DeviceInfoType>} - A promise that resolves to the device info.
|
|
88
137
|
*/
|
|
89
138
|
async (jwtToken: string, macAddress: string): Promise<DeviceInfoType> => {
|
|
90
|
-
const
|
|
139
|
+
const data = await fetchJson<DeviceInfoRawType>(
|
|
140
|
+
baseURL,
|
|
91
141
|
`device/${macAddress}/info`,
|
|
92
142
|
{
|
|
143
|
+
method: "GET",
|
|
93
144
|
headers: headers(jwtToken),
|
|
94
|
-
}
|
|
145
|
+
},
|
|
95
146
|
);
|
|
96
147
|
// Process response to decompress any gzipped Buffer fields
|
|
97
|
-
return processResponse(
|
|
148
|
+
return processResponse(data) as DeviceInfoType;
|
|
98
149
|
};
|
|
99
150
|
|
|
100
151
|
const mqttCommand =
|
|
101
|
-
(
|
|
152
|
+
(baseURL: string) =>
|
|
102
153
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
103
154
|
(jwtToken: string, macAddress: string, payload: any) =>
|
|
104
|
-
|
|
105
|
-
"
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
155
|
+
fetchJson(baseURL, "mqtt/command", {
|
|
156
|
+
method: "PUT",
|
|
157
|
+
headers: {
|
|
158
|
+
"Content-Type": "application/json",
|
|
159
|
+
...headers(jwtToken),
|
|
160
|
+
},
|
|
161
|
+
body: JSON.stringify({ mac_address: macAddress, ...payload }),
|
|
162
|
+
});
|
|
109
163
|
|
|
110
164
|
const setPower =
|
|
111
|
-
(
|
|
165
|
+
(baseURL: string) =>
|
|
112
166
|
/**
|
|
113
167
|
* Sends a command to set the power state of a device.
|
|
114
168
|
*
|
|
@@ -118,10 +172,10 @@ const setPower =
|
|
|
118
172
|
* @returns {Promise<string>} - A promise that resolves to the command response.
|
|
119
173
|
*/
|
|
120
174
|
(jwtToken: string, macAddress: string, value: number) =>
|
|
121
|
-
mqttCommand(
|
|
175
|
+
mqttCommand(baseURL)(jwtToken, macAddress, { name: "power", value });
|
|
122
176
|
|
|
123
177
|
const setPowerOn =
|
|
124
|
-
(
|
|
178
|
+
(baseURL: string) =>
|
|
125
179
|
/**
|
|
126
180
|
* Turns a device ON by setting its power state.
|
|
127
181
|
*
|
|
@@ -134,10 +188,10 @@ const setPowerOn =
|
|
|
134
188
|
* console.log(response);
|
|
135
189
|
*/
|
|
136
190
|
(jwtToken: string, macAddress: string) =>
|
|
137
|
-
setPower(
|
|
191
|
+
setPower(baseURL)(jwtToken, macAddress, 1);
|
|
138
192
|
|
|
139
193
|
const setPowerOff =
|
|
140
|
-
(
|
|
194
|
+
(baseURL: string) =>
|
|
141
195
|
/**
|
|
142
196
|
* Turns a device OFF by setting its power state.
|
|
143
197
|
*
|
|
@@ -150,10 +204,10 @@ const setPowerOff =
|
|
|
150
204
|
* console.log(response);
|
|
151
205
|
*/
|
|
152
206
|
(jwtToken: string, macAddress: string) =>
|
|
153
|
-
setPower(
|
|
207
|
+
setPower(baseURL)(jwtToken, macAddress, 0);
|
|
154
208
|
|
|
155
209
|
const getPower =
|
|
156
|
-
(
|
|
210
|
+
(baseURL: string) =>
|
|
157
211
|
/**
|
|
158
212
|
* Retrieves the power status of the device.
|
|
159
213
|
*
|
|
@@ -162,12 +216,12 @@ const getPower =
|
|
|
162
216
|
* @returns {Promise<boolean>} - A promise that resolves to the power status.
|
|
163
217
|
*/
|
|
164
218
|
async (jwtToken: string, macAddress: string): Promise<boolean> => {
|
|
165
|
-
const info = await deviceInfo(
|
|
219
|
+
const info = await deviceInfo(baseURL)(jwtToken, macAddress);
|
|
166
220
|
return info.status.commands.power;
|
|
167
221
|
};
|
|
168
222
|
|
|
169
223
|
const getEnvironmentTemperature =
|
|
170
|
-
(
|
|
224
|
+
(baseURL: string) =>
|
|
171
225
|
/**
|
|
172
226
|
* Retrieves the environment temperature from the device's sensors.
|
|
173
227
|
*
|
|
@@ -176,12 +230,12 @@ const getEnvironmentTemperature =
|
|
|
176
230
|
* @returns {Promise<number>} - A promise that resolves to the temperature value.
|
|
177
231
|
*/
|
|
178
232
|
async (jwtToken: string, macAddress: string): Promise<number> => {
|
|
179
|
-
const info = await deviceInfo(
|
|
233
|
+
const info = await deviceInfo(baseURL)(jwtToken, macAddress);
|
|
180
234
|
return info.status.temperatures.enviroment;
|
|
181
235
|
};
|
|
182
236
|
|
|
183
237
|
const getTargetTemperature =
|
|
184
|
-
(
|
|
238
|
+
(baseURL: string) =>
|
|
185
239
|
/**
|
|
186
240
|
* Retrieves the target temperature value set on the device.
|
|
187
241
|
*
|
|
@@ -190,12 +244,12 @@ const getTargetTemperature =
|
|
|
190
244
|
* @returns {Promise<number>} - A promise that resolves to the target temperature (degree celsius).
|
|
191
245
|
*/
|
|
192
246
|
async (jwtToken: string, macAddress: string): Promise<number> => {
|
|
193
|
-
const info = await deviceInfo(
|
|
247
|
+
const info = await deviceInfo(baseURL)(jwtToken, macAddress);
|
|
194
248
|
return info.nvm.user_parameters.enviroment_1_temperature;
|
|
195
249
|
};
|
|
196
250
|
|
|
197
251
|
const setTargetTemperature =
|
|
198
|
-
(
|
|
252
|
+
(baseURL: string) =>
|
|
199
253
|
/**
|
|
200
254
|
* Sends a command to set the target temperature (degree celsius) of a device.
|
|
201
255
|
*
|
|
@@ -205,13 +259,13 @@ const setTargetTemperature =
|
|
|
205
259
|
* @returns {Promise<string>} - A promise that resolves to the command response.
|
|
206
260
|
*/
|
|
207
261
|
(jwtToken: string, macAddress: string, temperature: number) =>
|
|
208
|
-
mqttCommand(
|
|
262
|
+
mqttCommand(baseURL)(jwtToken, macAddress, {
|
|
209
263
|
name: "enviroment_1_temperature",
|
|
210
264
|
value: temperature,
|
|
211
265
|
});
|
|
212
266
|
|
|
213
267
|
const registerDevice =
|
|
214
|
-
(
|
|
268
|
+
(baseURL: string) =>
|
|
215
269
|
/**
|
|
216
270
|
* Registers a device with the user's account.
|
|
217
271
|
* This must be called before other device operations will work on the new API.
|
|
@@ -228,7 +282,7 @@ const registerDevice =
|
|
|
228
282
|
macAddress: string,
|
|
229
283
|
serialNumber: string,
|
|
230
284
|
deviceName: string = "",
|
|
231
|
-
deviceRoom: string = ""
|
|
285
|
+
deviceRoom: string = "",
|
|
232
286
|
): Promise<DeviceAssociationResponse> => {
|
|
233
287
|
const body: DeviceAssociationBody = {
|
|
234
288
|
macAddress: macAddress.replace(/:/g, ""),
|
|
@@ -236,16 +290,18 @@ const registerDevice =
|
|
|
236
290
|
deviceRoom,
|
|
237
291
|
serialNumber,
|
|
238
292
|
};
|
|
239
|
-
|
|
240
|
-
"
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
293
|
+
return fetchJson<DeviceAssociationResponse>(baseURL, "device", {
|
|
294
|
+
method: "POST",
|
|
295
|
+
headers: {
|
|
296
|
+
"Content-Type": "application/json",
|
|
297
|
+
...headers(jwtToken),
|
|
298
|
+
},
|
|
299
|
+
body: JSON.stringify(body),
|
|
300
|
+
});
|
|
245
301
|
};
|
|
246
302
|
|
|
247
303
|
const editDevice =
|
|
248
|
-
(
|
|
304
|
+
(baseURL: string) =>
|
|
249
305
|
/**
|
|
250
306
|
* Updates a device's name and room.
|
|
251
307
|
*
|
|
@@ -259,19 +315,25 @@ const editDevice =
|
|
|
259
315
|
jwtToken: string,
|
|
260
316
|
macAddress: string,
|
|
261
317
|
deviceName: string = "",
|
|
262
|
-
deviceRoom: string = ""
|
|
318
|
+
deviceRoom: string = "",
|
|
263
319
|
): Promise<DeviceAssociationResponse> => {
|
|
264
320
|
const normalizedMac = macAddress.replace(/:/g, "");
|
|
265
321
|
const body: EditDeviceAssociationBody = {
|
|
266
322
|
deviceName,
|
|
267
323
|
deviceRoom,
|
|
268
324
|
};
|
|
269
|
-
|
|
325
|
+
return fetchJson<DeviceAssociationResponse>(
|
|
326
|
+
baseURL,
|
|
270
327
|
`device/${normalizedMac}`,
|
|
271
|
-
|
|
272
|
-
|
|
328
|
+
{
|
|
329
|
+
method: "PUT",
|
|
330
|
+
headers: {
|
|
331
|
+
"Content-Type": "application/json",
|
|
332
|
+
...headers(jwtToken),
|
|
333
|
+
},
|
|
334
|
+
body: JSON.stringify(body),
|
|
335
|
+
},
|
|
273
336
|
);
|
|
274
|
-
return response.data;
|
|
275
337
|
};
|
|
276
338
|
|
|
277
339
|
/**
|
|
@@ -285,31 +347,24 @@ const editDevice =
|
|
|
285
347
|
* const api = configure();
|
|
286
348
|
* const power = await api.getPower(jwtToken, macAddress);
|
|
287
349
|
*/
|
|
288
|
-
const configure = (baseURL: string = API_URL) => {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
const setTargetTemperatureInstance = setTargetTemperature(axiosInstance);
|
|
301
|
-
return {
|
|
302
|
-
deviceInfo: deviceInfoInstance,
|
|
303
|
-
registerDevice: registerDeviceInstance,
|
|
304
|
-
editDevice: editDeviceInstance,
|
|
305
|
-
setPower: setPowerInstance,
|
|
306
|
-
setPowerOff: setPowerOffInstance,
|
|
307
|
-
setPowerOn: setPowerOnInstance,
|
|
308
|
-
getPower: getPowerInstance,
|
|
309
|
-
getEnvironmentTemperature: getEnvironmentTemperatureInstance,
|
|
310
|
-
getTargetTemperature: getTargetTemperatureInstance,
|
|
311
|
-
setTargetTemperature: setTargetTemperatureInstance,
|
|
312
|
-
};
|
|
313
|
-
};
|
|
350
|
+
const configure = (baseURL: string = API_URL) => ({
|
|
351
|
+
deviceInfo: deviceInfo(baseURL),
|
|
352
|
+
registerDevice: registerDevice(baseURL),
|
|
353
|
+
editDevice: editDevice(baseURL),
|
|
354
|
+
setPower: setPower(baseURL),
|
|
355
|
+
setPowerOff: setPowerOff(baseURL),
|
|
356
|
+
setPowerOn: setPowerOn(baseURL),
|
|
357
|
+
getPower: getPower(baseURL),
|
|
358
|
+
getEnvironmentTemperature: getEnvironmentTemperature(baseURL),
|
|
359
|
+
getTargetTemperature: getTargetTemperature(baseURL),
|
|
360
|
+
setTargetTemperature: setTargetTemperature(baseURL),
|
|
361
|
+
});
|
|
314
362
|
|
|
315
|
-
export {
|
|
363
|
+
export {
|
|
364
|
+
configure,
|
|
365
|
+
configureAmplify,
|
|
366
|
+
createAuthService,
|
|
367
|
+
getSession,
|
|
368
|
+
headers,
|
|
369
|
+
signIn,
|
|
370
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { promises as fs } from "fs";
|
|
2
|
+
import * as os from "os";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
|
|
5
|
+
const TOKEN_DIR = path.join(os.homedir(), ".edilkamin");
|
|
6
|
+
const TOKEN_FILE = path.join(TOKEN_DIR, "session.json");
|
|
7
|
+
|
|
8
|
+
interface StoredData {
|
|
9
|
+
[key: string]: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Custom storage adapter for AWS Amplify that persists to file system.
|
|
14
|
+
* Used for CLI to maintain sessions between invocations.
|
|
15
|
+
*/
|
|
16
|
+
export const createFileStorage = () => {
|
|
17
|
+
let cache: StoredData = {};
|
|
18
|
+
let loaded = false;
|
|
19
|
+
|
|
20
|
+
const ensureDir = async (): Promise<void> => {
|
|
21
|
+
try {
|
|
22
|
+
await fs.mkdir(TOKEN_DIR, { recursive: true, mode: 0o700 });
|
|
23
|
+
} catch {
|
|
24
|
+
// Directory may already exist
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const load = async (): Promise<void> => {
|
|
29
|
+
if (loaded) return;
|
|
30
|
+
try {
|
|
31
|
+
const data = await fs.readFile(TOKEN_FILE, "utf-8");
|
|
32
|
+
cache = JSON.parse(data);
|
|
33
|
+
} catch {
|
|
34
|
+
cache = {};
|
|
35
|
+
}
|
|
36
|
+
loaded = true;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const save = async (): Promise<void> => {
|
|
40
|
+
await ensureDir();
|
|
41
|
+
await fs.writeFile(TOKEN_FILE, JSON.stringify(cache), {
|
|
42
|
+
encoding: "utf-8",
|
|
43
|
+
mode: 0o600,
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
setItem: async (key: string, value: string): Promise<void> => {
|
|
49
|
+
await load();
|
|
50
|
+
cache[key] = value;
|
|
51
|
+
await save();
|
|
52
|
+
},
|
|
53
|
+
getItem: async (key: string): Promise<string | null> => {
|
|
54
|
+
await load();
|
|
55
|
+
return cache[key] ?? null;
|
|
56
|
+
},
|
|
57
|
+
removeItem: async (key: string): Promise<void> => {
|
|
58
|
+
await load();
|
|
59
|
+
delete cache[key];
|
|
60
|
+
await save();
|
|
61
|
+
},
|
|
62
|
+
clear: async (): Promise<void> => {
|
|
63
|
+
cache = {};
|
|
64
|
+
await save();
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Clears all stored session data.
|
|
71
|
+
*/
|
|
72
|
+
export const clearSession = async (): Promise<void> => {
|
|
73
|
+
try {
|
|
74
|
+
await fs.unlink(TOKEN_FILE);
|
|
75
|
+
} catch {
|
|
76
|
+
// File may not exist
|
|
77
|
+
}
|
|
78
|
+
};
|