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/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 axios, { AxiosInstance } from "axios";
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
- return { signIn };
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
- (axiosInstance: AxiosInstance) =>
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 response = await axiosInstance.get<DeviceInfoRawType>(
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(response.data) as DeviceInfoType;
148
+ return processResponse(data) as DeviceInfoType;
98
149
  };
99
150
 
100
151
  const mqttCommand =
101
- (axiosInstance: AxiosInstance) =>
152
+ (baseURL: string) =>
102
153
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
103
154
  (jwtToken: string, macAddress: string, payload: any) =>
104
- axiosInstance.put(
105
- "mqtt/command",
106
- { mac_address: macAddress, ...payload },
107
- { headers: headers(jwtToken) }
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
- (axiosInstance: AxiosInstance) =>
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(axiosInstance)(jwtToken, macAddress, { name: "power", value });
175
+ mqttCommand(baseURL)(jwtToken, macAddress, { name: "power", value });
122
176
 
123
177
  const setPowerOn =
124
- (axiosInstance: AxiosInstance) =>
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(axiosInstance)(jwtToken, macAddress, 1);
191
+ setPower(baseURL)(jwtToken, macAddress, 1);
138
192
 
139
193
  const setPowerOff =
140
- (axiosInstance: AxiosInstance) =>
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(axiosInstance)(jwtToken, macAddress, 0);
207
+ setPower(baseURL)(jwtToken, macAddress, 0);
154
208
 
155
209
  const getPower =
156
- (axiosInstance: AxiosInstance) =>
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(axiosInstance)(jwtToken, macAddress);
219
+ const info = await deviceInfo(baseURL)(jwtToken, macAddress);
166
220
  return info.status.commands.power;
167
221
  };
168
222
 
169
223
  const getEnvironmentTemperature =
170
- (axiosInstance: AxiosInstance) =>
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(axiosInstance)(jwtToken, macAddress);
233
+ const info = await deviceInfo(baseURL)(jwtToken, macAddress);
180
234
  return info.status.temperatures.enviroment;
181
235
  };
182
236
 
183
237
  const getTargetTemperature =
184
- (axiosInstance: AxiosInstance) =>
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(axiosInstance)(jwtToken, macAddress);
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
- (axiosInstance: AxiosInstance) =>
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(axiosInstance)(jwtToken, macAddress, {
262
+ mqttCommand(baseURL)(jwtToken, macAddress, {
209
263
  name: "enviroment_1_temperature",
210
264
  value: temperature,
211
265
  });
212
266
 
213
267
  const registerDevice =
214
- (axiosInstance: AxiosInstance) =>
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
- const response = await axiosInstance.post<DeviceAssociationResponse>(
240
- "device",
241
- body,
242
- { headers: headers(jwtToken) }
243
- );
244
- return response.data;
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
- (axiosInstance: AxiosInstance) =>
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
- const response = await axiosInstance.put<DeviceAssociationResponse>(
325
+ return fetchJson<DeviceAssociationResponse>(
326
+ baseURL,
270
327
  `device/${normalizedMac}`,
271
- body,
272
- { headers: headers(jwtToken) }
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
- const axiosInstance = axios.create({ baseURL });
290
- const deviceInfoInstance = deviceInfo(axiosInstance);
291
- const registerDeviceInstance = registerDevice(axiosInstance);
292
- const editDeviceInstance = editDevice(axiosInstance);
293
- const setPowerInstance = setPower(axiosInstance);
294
- const setPowerOffInstance = setPowerOff(axiosInstance);
295
- const setPowerOnInstance = setPowerOn(axiosInstance);
296
- const getPowerInstance = getPower(axiosInstance);
297
- const getEnvironmentTemperatureInstance =
298
- getEnvironmentTemperature(axiosInstance);
299
- const getTargetTemperatureInstance = getTargetTemperature(axiosInstance);
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 { configure, createAuthService, headers, signIn };
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
+ };