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.
@@ -0,0 +1,81 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { promises as fs } from "fs";
11
+ import * as os from "os";
12
+ import * as path from "path";
13
+ const TOKEN_DIR = path.join(os.homedir(), ".edilkamin");
14
+ const TOKEN_FILE = path.join(TOKEN_DIR, "session.json");
15
+ /**
16
+ * Custom storage adapter for AWS Amplify that persists to file system.
17
+ * Used for CLI to maintain sessions between invocations.
18
+ */
19
+ export const createFileStorage = () => {
20
+ let cache = {};
21
+ let loaded = false;
22
+ const ensureDir = () => __awaiter(void 0, void 0, void 0, function* () {
23
+ try {
24
+ yield fs.mkdir(TOKEN_DIR, { recursive: true, mode: 0o700 });
25
+ }
26
+ catch (_a) {
27
+ // Directory may already exist
28
+ }
29
+ });
30
+ const load = () => __awaiter(void 0, void 0, void 0, function* () {
31
+ if (loaded)
32
+ return;
33
+ try {
34
+ const data = yield fs.readFile(TOKEN_FILE, "utf-8");
35
+ cache = JSON.parse(data);
36
+ }
37
+ catch (_a) {
38
+ cache = {};
39
+ }
40
+ loaded = true;
41
+ });
42
+ const save = () => __awaiter(void 0, void 0, void 0, function* () {
43
+ yield ensureDir();
44
+ yield fs.writeFile(TOKEN_FILE, JSON.stringify(cache), {
45
+ encoding: "utf-8",
46
+ mode: 0o600,
47
+ });
48
+ });
49
+ return {
50
+ setItem: (key, value) => __awaiter(void 0, void 0, void 0, function* () {
51
+ yield load();
52
+ cache[key] = value;
53
+ yield save();
54
+ }),
55
+ getItem: (key) => __awaiter(void 0, void 0, void 0, function* () {
56
+ var _a;
57
+ yield load();
58
+ return (_a = cache[key]) !== null && _a !== void 0 ? _a : null;
59
+ }),
60
+ removeItem: (key) => __awaiter(void 0, void 0, void 0, function* () {
61
+ yield load();
62
+ delete cache[key];
63
+ yield save();
64
+ }),
65
+ clear: () => __awaiter(void 0, void 0, void 0, function* () {
66
+ cache = {};
67
+ yield save();
68
+ }),
69
+ };
70
+ };
71
+ /**
72
+ * Clears all stored session data.
73
+ */
74
+ export const clearSession = () => __awaiter(void 0, void 0, void 0, function* () {
75
+ try {
76
+ yield fs.unlink(TOKEN_FILE);
77
+ }
78
+ catch (_a) {
79
+ // File may not exist
80
+ }
81
+ });
package/eslint.config.mjs CHANGED
@@ -1,4 +1,6 @@
1
1
  import typescriptEslint from "@typescript-eslint/eslint-plugin";
2
+ import prettierConfig from "eslint-config-prettier";
3
+ import prettierPlugin from "eslint-plugin-prettier";
2
4
  import simpleImportSort from "eslint-plugin-simple-import-sort";
3
5
  import globals from "globals";
4
6
  import tsParser from "@typescript-eslint/parser";
@@ -19,7 +21,7 @@ const compat = new FlatCompat({
19
21
  export default [
20
22
  ...compat.extends(
21
23
  "eslint:recommended",
22
- "plugin:@typescript-eslint/recommended"
24
+ "plugin:@typescript-eslint/recommended",
23
25
  ),
24
26
  {
25
27
  plugins: {
@@ -32,4 +34,13 @@ export default [
32
34
  "simple-import-sort/exports": "error",
33
35
  },
34
36
  },
37
+ prettierConfig,
38
+ {
39
+ plugins: {
40
+ prettier: prettierPlugin,
41
+ },
42
+ rules: {
43
+ "prettier/prettier": "error",
44
+ },
45
+ },
35
46
  ];
package/package.json CHANGED
@@ -1,10 +1,22 @@
1
1
  {
2
2
  "name": "edilkamin",
3
- "version": "1.6.2",
3
+ "version": "1.7.3",
4
4
  "description": "",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
7
7
  "types": "dist/esm/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": {
11
+ "types": "./dist/esm/index.d.ts",
12
+ "default": "./dist/esm/index.js"
13
+ },
14
+ "require": {
15
+ "types": "./dist/cjs/index.d.ts",
16
+ "default": "./dist/cjs/index.js"
17
+ }
18
+ }
19
+ },
8
20
  "scripts": {
9
21
  "cli": "ts-node src/cli.ts",
10
22
  "cli:debug": "node --inspect --require ts-node/register/transpile-only src/cli.ts",
@@ -42,7 +54,6 @@
42
54
  },
43
55
  "dependencies": {
44
56
  "aws-amplify": "^6.10.0",
45
- "axios": "^1.13.2",
46
57
  "pako": "^2.1.0"
47
58
  },
48
59
  "devDependencies": {
@@ -54,13 +65,14 @@
54
65
  "@types/sinon": "^17.0.3",
55
66
  "@typescript-eslint/eslint-plugin": "^8.17.0",
56
67
  "@typescript-eslint/parser": "^8.17.0",
68
+ "esbuild": "^0.27.1",
57
69
  "eslint": "^9.16.0",
58
70
  "eslint-config-prettier": "^10.1.8",
59
71
  "eslint-plugin-prettier": "^5.2.1",
60
72
  "eslint-plugin-simple-import-sort": "^12.1.1",
61
73
  "mocha": "^11.7.5",
62
74
  "nyc": "^17.1.0",
63
- "prettier": "^2.5.1",
75
+ "prettier": "^3.7.4",
64
76
  "sinon": "^19.0.2",
65
77
  "ts-node": "^10.9.1",
66
78
  "typedoc": "^0.28.15",
@@ -0,0 +1,21 @@
1
+ import { strict as assert } from "assert";
2
+ import * as esbuild from "esbuild";
3
+
4
+ describe("browser-bundle", () => {
5
+ it("should bundle for browser without Node.js built-ins", async () => {
6
+ // This test verifies that the library can be bundled for browser environments
7
+ // without requiring Node.js built-in modules (fs, os, path).
8
+ // If this test fails, it means Node.js-only code has leaked into the main exports.
9
+ const result = await esbuild.build({
10
+ entryPoints: ["dist/esm/index.js"],
11
+ platform: "browser",
12
+ bundle: true,
13
+ write: false,
14
+ // External dependencies that are expected (real deps + assert which is used for validation)
15
+ external: ["aws-amplify", "aws-amplify/*", "pako", "assert"],
16
+ logLevel: "silent",
17
+ });
18
+ // If we get here without error, the bundle succeeded
19
+ assert.ok(result.outputFiles.length > 0, "Bundle should produce output");
20
+ });
21
+ });
@@ -8,7 +8,7 @@ import { decompressBuffer, isBuffer, processResponse } from "./buffer-utils";
8
8
  * Helper to create a gzip-compressed Buffer object for testing.
9
9
  */
10
10
  const createGzippedBuffer = (
11
- data: unknown
11
+ data: unknown,
12
12
  ): { type: "Buffer"; data: number[] } => {
13
13
  const json = JSON.stringify(data);
14
14
  const compressed = pako.gzip(json);
package/src/cli.ts CHANGED
@@ -4,7 +4,8 @@ import readline from "readline";
4
4
 
5
5
  import { version } from "../package.json";
6
6
  import { NEW_API_URL, OLD_API_URL } from "./constants";
7
- import { configure, signIn } from "./library";
7
+ import { configure, configureAmplify, getSession, signIn } from "./library";
8
+ import { clearSession, createFileStorage } from "./token-storage";
8
9
 
9
10
  const promptPassword = (): Promise<string> => {
10
11
  const rl = readline.createInterface({
@@ -30,12 +31,16 @@ const promptPassword = (): Promise<string> => {
30
31
 
31
32
  /**
32
33
  * Adds common options (username and password) to a command.
34
+ * Username is optional if a session already exists.
33
35
  * @param command The command to which options should be added.
34
36
  * @returns The command with options added.
35
37
  */
36
38
  const addAuthOptions = (command: Command): Command =>
37
39
  command
38
- .requiredOption("-u, --username <username>", "Username")
40
+ .option(
41
+ "-u, --username <username>",
42
+ "Username (optional if session exists)",
43
+ )
39
44
  .option("-p, --password <password>", "Password");
40
45
 
41
46
  /**
@@ -56,11 +61,12 @@ const addLegacyOption = (command: Command): Command =>
56
61
 
57
62
  /**
58
63
  * Handles common authentication and API initialization logic.
64
+ * Tries to use existing session first, falls back to sign-in if needed.
59
65
  * @param options The options passed from the CLI command.
60
66
  * @returns An object containing the normalized MAC, JWT token, and configured API instance.
61
67
  */
62
68
  const initializeCommand = async (options: {
63
- username: string;
69
+ username?: string;
64
70
  password?: string;
65
71
  mac: string;
66
72
  legacy?: boolean;
@@ -71,8 +77,26 @@ const initializeCommand = async (options: {
71
77
  }> => {
72
78
  const { username, password, mac, legacy = false } = options;
73
79
  const normalizedMac = mac.replace(/:/g, "");
74
- const pwd = password || (await promptPassword());
75
- const jwtToken = await signIn(username, pwd, legacy);
80
+
81
+ // Initialize file storage for session persistence
82
+ const storage = createFileStorage();
83
+ configureAmplify(storage);
84
+
85
+ let jwtToken: string;
86
+ try {
87
+ // Try to get existing session first
88
+ jwtToken = await getSession(false, legacy);
89
+ } catch {
90
+ // No session, need to sign in
91
+ if (!username) {
92
+ throw new Error(
93
+ "No session found. Please provide --username to sign in.",
94
+ );
95
+ }
96
+ const pwd = password || (await promptPassword());
97
+ jwtToken = await signIn(username, pwd, legacy);
98
+ }
99
+
76
100
  const apiUrl = legacy ? OLD_API_URL : NEW_API_URL;
77
101
  const api = configure(apiUrl);
78
102
  return { normalizedMac, jwtToken, api };
@@ -85,7 +109,7 @@ const initializeCommand = async (options: {
85
109
  */
86
110
  const executeGetter = async (
87
111
  options: {
88
- username: string;
112
+ username?: string;
89
113
  password?: string;
90
114
  mac: string;
91
115
  legacy?: boolean;
@@ -93,8 +117,8 @@ const executeGetter = async (
93
117
  getter: (
94
118
  api: ReturnType<typeof configure>,
95
119
  jwtToken: string,
96
- mac: string
97
- ) => Promise<unknown>
120
+ mac: string,
121
+ ) => Promise<unknown>,
98
122
  ): Promise<void> => {
99
123
  const { normalizedMac, jwtToken, api } = await initializeCommand(options);
100
124
  const result = await getter(api, jwtToken, normalizedMac);
@@ -108,7 +132,7 @@ const executeGetter = async (
108
132
  */
109
133
  const executeSetter = async (
110
134
  options: {
111
- username: string;
135
+ username?: string;
112
136
  password?: string;
113
137
  mac: string;
114
138
  value: number;
@@ -118,8 +142,8 @@ const executeSetter = async (
118
142
  api: ReturnType<typeof configure>,
119
143
  jwtToken: string,
120
144
  mac: string,
121
- value: number
122
- ) => Promise<unknown>
145
+ value: number,
146
+ ) => Promise<unknown>,
123
147
  ): Promise<void> => {
124
148
  const { normalizedMac, jwtToken, api } = await initializeCommand(options);
125
149
  const result = await setter(api, jwtToken, normalizedMac, options.value);
@@ -133,14 +157,29 @@ const createProgram = (): Command => {
133
157
  .description("CLI tool for interacting with the Edilkamin API")
134
158
  .version(version);
135
159
  // Command: signIn
136
- addAuthOptions(
137
- program.command("signIn").description("Sign in and retrieve a JWT token")
138
- ).action(async (options) => {
139
- const { username, password } = options;
140
- const pwd = password || (await promptPassword());
141
- const jwtToken = await signIn(username, pwd);
142
- console.log("JWT Token:", jwtToken);
143
- });
160
+ program
161
+ .command("signIn")
162
+ .description("Sign in and retrieve a JWT token")
163
+ .requiredOption("-u, --username <username>", "Username")
164
+ .option("-p, --password <password>", "Password")
165
+ .action(async (options) => {
166
+ const { username, password } = options;
167
+ // Initialize file storage for session persistence
168
+ const storage = createFileStorage();
169
+ configureAmplify(storage);
170
+ const pwd = password || (await promptPassword());
171
+ const jwtToken = await signIn(username, pwd);
172
+ console.log("JWT Token:", jwtToken);
173
+ });
174
+
175
+ // Command: logout
176
+ program
177
+ .command("logout")
178
+ .description("Clear stored session")
179
+ .action(async () => {
180
+ await clearSession();
181
+ console.log("Session cleared successfully");
182
+ });
144
183
  // Generic getter commands
145
184
  [
146
185
  {
@@ -149,7 +188,7 @@ const createProgram = (): Command => {
149
188
  getter: (
150
189
  api: ReturnType<typeof configure>,
151
190
  jwtToken: string,
152
- mac: string
191
+ mac: string,
153
192
  ) => api.deviceInfo(jwtToken, mac),
154
193
  },
155
194
  {
@@ -158,7 +197,7 @@ const createProgram = (): Command => {
158
197
  getter: (
159
198
  api: ReturnType<typeof configure>,
160
199
  jwtToken: string,
161
- mac: string
200
+ mac: string,
162
201
  ) => api.getPower(jwtToken, mac),
163
202
  },
164
203
  {
@@ -167,7 +206,7 @@ const createProgram = (): Command => {
167
206
  getter: (
168
207
  api: ReturnType<typeof configure>,
169
208
  jwtToken: string,
170
- mac: string
209
+ mac: string,
171
210
  ) => api.getEnvironmentTemperature(jwtToken, mac),
172
211
  },
173
212
  {
@@ -176,14 +215,14 @@ const createProgram = (): Command => {
176
215
  getter: (
177
216
  api: ReturnType<typeof configure>,
178
217
  jwtToken: string,
179
- mac: string
218
+ mac: string,
180
219
  ) => api.getTargetTemperature(jwtToken, mac),
181
220
  },
182
221
  ].forEach(({ commandName, description, getter }) => {
183
222
  addLegacyOption(
184
223
  addMacOption(
185
- addAuthOptions(program.command(commandName).description(description))
186
- )
224
+ addAuthOptions(program.command(commandName).description(description)),
225
+ ),
187
226
  ).action((options) => executeGetter(options, getter));
188
227
  });
189
228
  // Generic setter commands
@@ -195,7 +234,7 @@ const createProgram = (): Command => {
195
234
  api: ReturnType<typeof configure>,
196
235
  jwtToken: string,
197
236
  mac: string,
198
- value: number
237
+ value: number,
199
238
  ) => api.setPower(jwtToken, mac, value),
200
239
  },
201
240
  {
@@ -205,16 +244,16 @@ const createProgram = (): Command => {
205
244
  api: ReturnType<typeof configure>,
206
245
  jwtToken: string,
207
246
  mac: string,
208
- value: number
247
+ value: number,
209
248
  ) => api.setTargetTemperature(jwtToken, mac, value),
210
249
  },
211
250
  ].forEach(({ commandName, description, setter }) => {
212
251
  addLegacyOption(
213
252
  addMacOption(
214
253
  addAuthOptions(
215
- program.command(commandName).description(description)
216
- ).requiredOption("-v, --value <number>", "Value to set", parseFloat)
217
- )
254
+ program.command(commandName).description(description),
255
+ ).requiredOption("-v, --value <number>", "Value to set", parseFloat),
256
+ ),
218
257
  ).action((options) => executeSetter(options, setter));
219
258
  });
220
259
 
@@ -223,8 +262,8 @@ const createProgram = (): Command => {
223
262
  addAuthOptions(
224
263
  program
225
264
  .command("register")
226
- .description("Register a device with your account")
227
- )
265
+ .description("Register a device with your account"),
266
+ ),
228
267
  )
229
268
  .requiredOption("-m, --mac <macAddress>", "MAC address of the device")
230
269
  .requiredOption("-s, --serial <serialNumber>", "Device serial number")
@@ -241,8 +280,24 @@ const createProgram = (): Command => {
241
280
  legacy = false,
242
281
  } = options;
243
282
  const normalizedMac = mac.replace(/:/g, "");
244
- const pwd = password || (await promptPassword());
245
- const jwtToken = await signIn(username, pwd, legacy);
283
+
284
+ // Initialize file storage for session persistence
285
+ const storage = createFileStorage();
286
+ configureAmplify(storage);
287
+
288
+ let jwtToken: string;
289
+ try {
290
+ jwtToken = await getSession(false, legacy);
291
+ } catch {
292
+ if (!username) {
293
+ throw new Error(
294
+ "No session found. Please provide --username to sign in.",
295
+ );
296
+ }
297
+ const pwd = password || (await promptPassword());
298
+ jwtToken = await signIn(username, pwd, legacy);
299
+ }
300
+
246
301
  const apiUrl = legacy ? OLD_API_URL : NEW_API_URL;
247
302
  const api = configure(apiUrl);
248
303
  const result = await api.registerDevice(
@@ -250,7 +305,7 @@ const createProgram = (): Command => {
250
305
  normalizedMac,
251
306
  serial,
252
307
  name,
253
- room
308
+ room,
254
309
  );
255
310
  console.log("Device registered successfully:");
256
311
  console.log(JSON.stringify(result, null, 2));
@@ -260,17 +315,35 @@ const createProgram = (): Command => {
260
315
  addLegacyOption(
261
316
  addMacOption(
262
317
  addAuthOptions(
263
- program.command("editDevice").description("Update device name and room")
264
- )
265
- )
318
+ program
319
+ .command("editDevice")
320
+ .description("Update device name and room"),
321
+ ),
322
+ ),
266
323
  )
267
324
  .requiredOption("-n, --name <deviceName>", "Device name")
268
325
  .requiredOption("-r, --room <deviceRoom>", "Room name")
269
326
  .action(async (options) => {
270
327
  const { username, password, mac, name, room, legacy = false } = options;
271
328
  const normalizedMac = mac.replace(/:/g, "");
272
- const pwd = password || (await promptPassword());
273
- const jwtToken = await signIn(username, pwd, legacy);
329
+
330
+ // Initialize file storage for session persistence
331
+ const storage = createFileStorage();
332
+ configureAmplify(storage);
333
+
334
+ let jwtToken: string;
335
+ try {
336
+ jwtToken = await getSession(false, legacy);
337
+ } catch {
338
+ if (!username) {
339
+ throw new Error(
340
+ "No session found. Please provide --username to sign in.",
341
+ );
342
+ }
343
+ const pwd = password || (await promptPassword());
344
+ jwtToken = await signIn(username, pwd, legacy);
345
+ }
346
+
274
347
  const apiUrl = legacy ? OLD_API_URL : NEW_API_URL;
275
348
  const api = configure(apiUrl);
276
349
  const result = await api.editDevice(jwtToken, normalizedMac, name, room);
@@ -0,0 +1,47 @@
1
+ import { strict as assert } from "assert";
2
+ import sinon from "sinon";
3
+
4
+ import { configureAmplify } from "../src/library";
5
+
6
+ /**
7
+ * This test file specifically tests the configureAmplify function with custom storage.
8
+ * It tests line 61 in library.ts:
9
+ * cognitoUserPoolsTokenProvider.setKeyValueStorage(storage)
10
+ *
11
+ * IMPORTANT: This file is named to run BEFORE library.test.ts (alphabetically)
12
+ * to ensure amplifyConfigured is still false when these tests run.
13
+ */
14
+
15
+ describe("configureAmplify", () => {
16
+ it("should configure Amplify with custom storage", () => {
17
+ const mockStorage = {
18
+ setItem: sinon.stub().resolves(),
19
+ getItem: sinon.stub().resolves(null),
20
+ removeItem: sinon.stub().resolves(),
21
+ clear: sinon.stub().resolves(),
22
+ };
23
+
24
+ // Call configureAmplify with custom storage
25
+ // This is the first call in the test suite, so amplifyConfigured is false
26
+ // This should trigger line 61 in library.ts
27
+ configureAmplify(mockStorage);
28
+
29
+ // The test passes if no error is thrown
30
+ // Coverage confirms line 61 is executed
31
+ assert.ok(true, "configureAmplify with storage completed without error");
32
+ });
33
+
34
+ it("should only configure Amplify once (idempotent)", () => {
35
+ // Call configureAmplify multiple times without storage
36
+ configureAmplify();
37
+ configureAmplify();
38
+ configureAmplify();
39
+
40
+ // Should not throw or have any side effects
41
+ // The function returns early if already configured (line 58)
42
+ assert.ok(
43
+ true,
44
+ "Multiple calls to configureAmplify completed without error",
45
+ );
46
+ });
47
+ });
package/src/index.ts CHANGED
@@ -2,7 +2,7 @@ import { configure } from "./library";
2
2
 
3
3
  export { decompressBuffer, isBuffer, processResponse } from "./buffer-utils";
4
4
  export { API_URL, NEW_API_URL, OLD_API_URL } from "./constants";
5
- export { configure, signIn } from "./library";
5
+ export { configure, getSession, signIn } from "./library";
6
6
  export {
7
7
  serialNumberDisplay,
8
8
  serialNumberFromHex,