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.
@@ -10,7 +10,7 @@ jobs:
10
10
  timeout-minutes: 5
11
11
  strategy:
12
12
  matrix:
13
- node-version: [20.x, 22.x]
13
+ node-version: [20.x, 22.x, 24.x]
14
14
  steps:
15
15
  - uses: actions/checkout@v6
16
16
  - uses: actions/setup-node@v6
@@ -12,7 +12,7 @@ jobs:
12
12
  - uses: actions/checkout@v6
13
13
  - uses: actions/setup-node@v6
14
14
  with:
15
- node-version: "22.x"
15
+ node-version: "24.x"
16
16
  - name: git config
17
17
  run: |
18
18
  git config user.name documentation-deploy-action
@@ -4,6 +4,10 @@ on:
4
4
  push:
5
5
  pull_request:
6
6
 
7
+ permissions:
8
+ id-token: write
9
+ contents: read
10
+
7
11
  jobs:
8
12
  build:
9
13
  runs-on: ubuntu-latest
@@ -12,12 +16,10 @@ jobs:
12
16
  - uses: actions/checkout@v6
13
17
  - uses: actions/setup-node@v6
14
18
  with:
15
- node-version: "22.x"
19
+ node-version: "24.x"
16
20
  registry-url: "https://registry.npmjs.org"
17
21
  - run: yarn install
18
22
  - run: yarn build
19
- - run: npm publish --dry-run
23
+ - run: npm pack --dry-run
20
24
  - run: npm publish
21
25
  if: startsWith(github.ref, 'refs/tags/')
22
- env:
23
- NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }}
@@ -10,7 +10,7 @@ jobs:
10
10
  timeout-minutes: 5
11
11
  strategy:
12
12
  matrix:
13
- node-version: [20.x, 22.x]
13
+ node-version: [20.x, 22.x, 24.x]
14
14
  steps:
15
15
  - uses: actions/checkout@v6
16
16
  - uses: actions/setup-node@v6
package/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # Edilkamin.js
2
2
 
3
- [![Tests](https://github.com/AndreMiras/edilkamin.js/workflows/Tests/badge.svg)](https://github.com/AndreMiras/edilkamin.js/actions/workflows/tests.yml)
3
+ [![Tests](https://github.com/AndreMiras/edilkamin.js/actions/workflows/tests.yml/badge.svg)](https://github.com/AndreMiras/edilkamin.js/actions/workflows/tests.yml)
4
4
  [![CLI Tests](https://github.com/AndreMiras/edilkamin.js/actions/workflows/cli-tests.yml/badge.svg)](https://github.com/AndreMiras/edilkamin.js/actions/workflows/cli-tests.yml)
5
5
  [![codecov](https://codecov.io/gh/AndreMiras/edilkamin.js/graph/badge.svg?token=YG3LKXNZWU)](https://app.codecov.io/gh/AndreMiras/edilkamin.js/tree/main)
6
- [![Documentation](https://github.com/AndreMiras/edilkamin.js/workflows/Documentation/badge.svg)](https://github.com/AndreMiras/edilkamin.js/actions/workflows/documentation.yml)
6
+ [![Documentation](https://github.com/AndreMiras/edilkamin.js/actions/workflows/documentation.yml/badge.svg)](https://github.com/AndreMiras/edilkamin.js/actions/workflows/documentation.yml)
7
7
  [![npm version](https://badge.fury.io/js/edilkamin.svg)](https://badge.fury.io/js/edilkamin)
8
8
 
9
9
  This is a library for the [Reverse Engineered](docs/ReverseEngineering.md) "The Mind" Edilkamin API.
@@ -31,11 +31,26 @@ Basic usage:
31
31
  import { signIn, deviceInfo, setPowerOff } from "edilkamin";
32
32
 
33
33
  const macAddress = "aabbccddeeff";
34
- const token = signIn(username, password);
35
- deviceInfo(token, macAddress).then(console.log);
36
- setPowerOff(token, macAddress).then(console.log);
34
+ const token = await signIn(username, password);
35
+ console.log(await deviceInfo(token, macAddress));
36
+ console.log(await setPowerOff(token, macAddress));
37
37
  ```
38
38
 
39
+ For long-running applications, use `getSession()` to automatically refresh tokens:
40
+
41
+ ```js
42
+ import { signIn, getSession, deviceInfo } from "edilkamin";
43
+
44
+ // Authenticate once
45
+ await signIn(username, password);
46
+
47
+ // Get current session (auto-refreshes if expired)
48
+ const token = await getSession();
49
+ console.log(await deviceInfo(token, macAddress));
50
+ ```
51
+
52
+ Sessions persist for ~30 days without re-authentication. Call `getSession()` to retrieve fresh tokens as needed.
53
+
39
54
  It's also possible to change the default backend URL:
40
55
 
41
56
  ```js
@@ -43,8 +58,8 @@ import { signIn, configure } from "edilkamin";
43
58
 
44
59
  const baseUrl = "https://my-proxy.com/";
45
60
  const { deviceInfo, setPower } = configure(baseUrl);
46
- deviceInfo(token, macAddress).then(console.log);
47
- setPower(token, macAddress, 0).then(console.log);
61
+ console.log(await deviceInfo(token, macAddress));
62
+ console.log(await setPower(token, macAddress, 0));
48
63
  ```
49
64
 
50
65
  ## CLI
@@ -52,7 +67,14 @@ setPower(token, macAddress, 0).then(console.log);
52
67
  The library includes a CLI tool that is useful for debugging.
53
68
 
54
69
  ```sh
70
+ # First time: provide credentials to authenticate
55
71
  yarn cli deviceInfo --mac $MAC --username $USERNAME --password $PASSWORD
72
+
73
+ # Subsequent calls: session persists, credentials optional
74
+ yarn cli deviceInfo --mac $MAC
75
+
76
+ # Clear stored session
77
+ yarn cli logout
56
78
  ```
57
79
 
58
80
  Or with `npx` once the library is installed:
@@ -61,6 +83,8 @@ Or with `npx` once the library is installed:
61
83
  npx edilkamin deviceInfo --mac $MAC --username $USERNAME --password $PASSWORD
62
84
  ```
63
85
 
86
+ The CLI stores session data in `~/.edilkamin/session.json` and automatically refreshes tokens when needed. Sessions remain valid for ~30 days.
87
+
64
88
  ## API Versions
65
89
 
66
90
  This library supports both the new and legacy Edilkamin API endpoints.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,29 @@
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 { strict as assert } from "assert";
11
+ import * as esbuild from "esbuild";
12
+ describe("browser-bundle", () => {
13
+ it("should bundle for browser without Node.js built-ins", () => __awaiter(void 0, void 0, void 0, function* () {
14
+ // This test verifies that the library can be bundled for browser environments
15
+ // without requiring Node.js built-in modules (fs, os, path).
16
+ // If this test fails, it means Node.js-only code has leaked into the main exports.
17
+ const result = yield esbuild.build({
18
+ entryPoints: ["dist/esm/index.js"],
19
+ platform: "browser",
20
+ bundle: true,
21
+ write: false,
22
+ // External dependencies that are expected (real deps + assert which is used for validation)
23
+ external: ["aws-amplify", "aws-amplify/*", "pako", "assert"],
24
+ logLevel: "silent",
25
+ });
26
+ // If we get here without error, the bundle succeeded
27
+ assert.ok(result.outputFiles.length > 0, "Bundle should produce output");
28
+ }));
29
+ });
package/dist/esm/cli.js CHANGED
@@ -12,7 +12,8 @@ import { Command } from "commander";
12
12
  import readline from "readline";
13
13
  import { version } from "../package.json";
14
14
  import { NEW_API_URL, OLD_API_URL } from "./constants";
15
- import { configure, signIn } from "./library";
15
+ import { configure, configureAmplify, getSession, signIn } from "./library";
16
+ import { clearSession, createFileStorage } from "./token-storage";
16
17
  const promptPassword = () => {
17
18
  const rl = readline.createInterface({
18
19
  input: process.stdin,
@@ -37,11 +38,12 @@ const promptPassword = () => {
37
38
  };
38
39
  /**
39
40
  * Adds common options (username and password) to a command.
41
+ * Username is optional if a session already exists.
40
42
  * @param command The command to which options should be added.
41
43
  * @returns The command with options added.
42
44
  */
43
45
  const addAuthOptions = (command) => command
44
- .requiredOption("-u, --username <username>", "Username")
46
+ .option("-u, --username <username>", "Username (optional if session exists)")
45
47
  .option("-p, --password <password>", "Password");
46
48
  /**
47
49
  * Adds MAC address option to a command.
@@ -57,14 +59,29 @@ const addMacOption = (command) => command.requiredOption("-m, --mac <macAddress>
57
59
  const addLegacyOption = (command) => command.option("--legacy", "Use legacy API endpoint (old AWS Gateway)");
58
60
  /**
59
61
  * Handles common authentication and API initialization logic.
62
+ * Tries to use existing session first, falls back to sign-in if needed.
60
63
  * @param options The options passed from the CLI command.
61
64
  * @returns An object containing the normalized MAC, JWT token, and configured API instance.
62
65
  */
63
66
  const initializeCommand = (options) => __awaiter(void 0, void 0, void 0, function* () {
64
67
  const { username, password, mac, legacy = false } = options;
65
68
  const normalizedMac = mac.replace(/:/g, "");
66
- const pwd = password || (yield promptPassword());
67
- const jwtToken = yield signIn(username, pwd, legacy);
69
+ // Initialize file storage for session persistence
70
+ const storage = createFileStorage();
71
+ configureAmplify(storage);
72
+ let jwtToken;
73
+ try {
74
+ // Try to get existing session first
75
+ jwtToken = yield getSession(false, legacy);
76
+ }
77
+ catch (_a) {
78
+ // No session, need to sign in
79
+ if (!username) {
80
+ throw new Error("No session found. Please provide --username to sign in.");
81
+ }
82
+ const pwd = password || (yield promptPassword());
83
+ jwtToken = yield signIn(username, pwd, legacy);
84
+ }
68
85
  const apiUrl = legacy ? OLD_API_URL : NEW_API_URL;
69
86
  const api = configure(apiUrl);
70
87
  return { normalizedMac, jwtToken, api };
@@ -96,12 +113,28 @@ const createProgram = () => {
96
113
  .description("CLI tool for interacting with the Edilkamin API")
97
114
  .version(version);
98
115
  // Command: signIn
99
- addAuthOptions(program.command("signIn").description("Sign in and retrieve a JWT token")).action((options) => __awaiter(void 0, void 0, void 0, function* () {
116
+ program
117
+ .command("signIn")
118
+ .description("Sign in and retrieve a JWT token")
119
+ .requiredOption("-u, --username <username>", "Username")
120
+ .option("-p, --password <password>", "Password")
121
+ .action((options) => __awaiter(void 0, void 0, void 0, function* () {
100
122
  const { username, password } = options;
123
+ // Initialize file storage for session persistence
124
+ const storage = createFileStorage();
125
+ configureAmplify(storage);
101
126
  const pwd = password || (yield promptPassword());
102
127
  const jwtToken = yield signIn(username, pwd);
103
128
  console.log("JWT Token:", jwtToken);
104
129
  }));
130
+ // Command: logout
131
+ program
132
+ .command("logout")
133
+ .description("Clear stored session")
134
+ .action(() => __awaiter(void 0, void 0, void 0, function* () {
135
+ yield clearSession();
136
+ console.log("Session cleared successfully");
137
+ }));
105
138
  // Generic getter commands
106
139
  [
107
140
  {
@@ -153,8 +186,20 @@ const createProgram = () => {
153
186
  .action((options) => __awaiter(void 0, void 0, void 0, function* () {
154
187
  const { username, password, mac, serial, name, room, legacy = false, } = options;
155
188
  const normalizedMac = mac.replace(/:/g, "");
156
- const pwd = password || (yield promptPassword());
157
- const jwtToken = yield signIn(username, pwd, legacy);
189
+ // Initialize file storage for session persistence
190
+ const storage = createFileStorage();
191
+ configureAmplify(storage);
192
+ let jwtToken;
193
+ try {
194
+ jwtToken = yield getSession(false, legacy);
195
+ }
196
+ catch (_a) {
197
+ if (!username) {
198
+ throw new Error("No session found. Please provide --username to sign in.");
199
+ }
200
+ const pwd = password || (yield promptPassword());
201
+ jwtToken = yield signIn(username, pwd, legacy);
202
+ }
158
203
  const apiUrl = legacy ? OLD_API_URL : NEW_API_URL;
159
204
  const api = configure(apiUrl);
160
205
  const result = yield api.registerDevice(jwtToken, normalizedMac, serial, name, room);
@@ -162,14 +207,28 @@ const createProgram = () => {
162
207
  console.log(JSON.stringify(result, null, 2));
163
208
  }));
164
209
  // Command: editDevice
165
- addLegacyOption(addMacOption(addAuthOptions(program.command("editDevice").description("Update device name and room"))))
210
+ addLegacyOption(addMacOption(addAuthOptions(program
211
+ .command("editDevice")
212
+ .description("Update device name and room"))))
166
213
  .requiredOption("-n, --name <deviceName>", "Device name")
167
214
  .requiredOption("-r, --room <deviceRoom>", "Room name")
168
215
  .action((options) => __awaiter(void 0, void 0, void 0, function* () {
169
216
  const { username, password, mac, name, room, legacy = false } = options;
170
217
  const normalizedMac = mac.replace(/:/g, "");
171
- const pwd = password || (yield promptPassword());
172
- const jwtToken = yield signIn(username, pwd, legacy);
218
+ // Initialize file storage for session persistence
219
+ const storage = createFileStorage();
220
+ configureAmplify(storage);
221
+ let jwtToken;
222
+ try {
223
+ jwtToken = yield getSession(false, legacy);
224
+ }
225
+ catch (_a) {
226
+ if (!username) {
227
+ throw new Error("No session found. Please provide --username to sign in.");
228
+ }
229
+ const pwd = password || (yield promptPassword());
230
+ jwtToken = yield signIn(username, pwd, legacy);
231
+ }
173
232
  const apiUrl = legacy ? OLD_API_URL : NEW_API_URL;
174
233
  const api = configure(apiUrl);
175
234
  const result = yield api.editDevice(jwtToken, normalizedMac, name, room);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,37 @@
1
+ import { strict as assert } from "assert";
2
+ import sinon from "sinon";
3
+ import { configureAmplify } from "../src/library";
4
+ /**
5
+ * This test file specifically tests the configureAmplify function with custom storage.
6
+ * It tests line 61 in library.ts:
7
+ * cognitoUserPoolsTokenProvider.setKeyValueStorage(storage)
8
+ *
9
+ * IMPORTANT: This file is named to run BEFORE library.test.ts (alphabetically)
10
+ * to ensure amplifyConfigured is still false when these tests run.
11
+ */
12
+ describe("configureAmplify", () => {
13
+ it("should configure Amplify with custom storage", () => {
14
+ const mockStorage = {
15
+ setItem: sinon.stub().resolves(),
16
+ getItem: sinon.stub().resolves(null),
17
+ removeItem: sinon.stub().resolves(),
18
+ clear: sinon.stub().resolves(),
19
+ };
20
+ // Call configureAmplify with custom storage
21
+ // This is the first call in the test suite, so amplifyConfigured is false
22
+ // This should trigger line 61 in library.ts
23
+ configureAmplify(mockStorage);
24
+ // The test passes if no error is thrown
25
+ // Coverage confirms line 61 is executed
26
+ assert.ok(true, "configureAmplify with storage completed without error");
27
+ });
28
+ it("should only configure Amplify once (idempotent)", () => {
29
+ // Call configureAmplify multiple times without storage
30
+ configureAmplify();
31
+ configureAmplify();
32
+ configureAmplify();
33
+ // Should not throw or have any side effects
34
+ // The function returns early if already configured (line 58)
35
+ assert.ok(true, "Multiple calls to configureAmplify completed without error");
36
+ });
37
+ });
@@ -1,6 +1,6 @@
1
1
  export { decompressBuffer, isBuffer, processResponse } from "./buffer-utils";
2
2
  export { API_URL, NEW_API_URL, OLD_API_URL } from "./constants";
3
- export { configure, signIn } from "./library";
3
+ export { configure, getSession, signIn } from "./library";
4
4
  export { serialNumberDisplay, serialNumberFromHex, serialNumberToHex, } from "./serial-utils";
5
5
  export { BufferEncodedType, CommandsType, DeviceAssociationBody, DeviceAssociationResponse, DeviceInfoRawType, DeviceInfoType, EditDeviceAssociationBody, StatusType, TemperaturesType, UserParametersType, } from "./types";
6
- export declare const deviceInfo: (jwtToken: string, macAddress: string) => Promise<import("./types").DeviceInfoType>, registerDevice: (jwtToken: string, macAddress: string, serialNumber: string, deviceName?: string, deviceRoom?: string) => Promise<import("./types").DeviceAssociationResponse>, editDevice: (jwtToken: string, macAddress: string, deviceName?: string, deviceRoom?: string) => Promise<import("./types").DeviceAssociationResponse>, setPower: (jwtToken: string, macAddress: string, value: number) => Promise<import("axios").AxiosResponse<any, any, {}>>, setPowerOff: (jwtToken: string, macAddress: string) => Promise<import("axios").AxiosResponse<any, any, {}>>, setPowerOn: (jwtToken: string, macAddress: string) => Promise<import("axios").AxiosResponse<any, any, {}>>, getPower: (jwtToken: string, macAddress: string) => Promise<boolean>, getEnvironmentTemperature: (jwtToken: string, macAddress: string) => Promise<number>, getTargetTemperature: (jwtToken: string, macAddress: string) => Promise<number>, setTargetTemperature: (jwtToken: string, macAddress: string, temperature: number) => Promise<import("axios").AxiosResponse<any, any, {}>>;
6
+ export declare const deviceInfo: (jwtToken: string, macAddress: string) => Promise<import("./types").DeviceInfoType>, registerDevice: (jwtToken: string, macAddress: string, serialNumber: string, deviceName?: string, deviceRoom?: string) => Promise<import("./types").DeviceAssociationResponse>, editDevice: (jwtToken: string, macAddress: string, deviceName?: string, deviceRoom?: string) => Promise<import("./types").DeviceAssociationResponse>, setPower: (jwtToken: string, macAddress: string, value: number) => Promise<unknown>, setPowerOff: (jwtToken: string, macAddress: string) => Promise<unknown>, setPowerOn: (jwtToken: string, macAddress: string) => Promise<unknown>, getPower: (jwtToken: string, macAddress: string) => Promise<boolean>, getEnvironmentTemperature: (jwtToken: string, macAddress: string) => Promise<number>, getTargetTemperature: (jwtToken: string, macAddress: string) => Promise<number>, setTargetTemperature: (jwtToken: string, macAddress: string, temperature: number) => Promise<unknown>;
package/dist/esm/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { configure } from "./library";
2
2
  export { decompressBuffer, isBuffer, processResponse } from "./buffer-utils";
3
3
  export { API_URL, NEW_API_URL, OLD_API_URL } from "./constants";
4
- export { configure, signIn } from "./library";
4
+ export { configure, getSession, signIn } from "./library";
5
5
  export { serialNumberDisplay, serialNumberFromHex, serialNumberToHex, } from "./serial-utils";
6
6
  export const { deviceInfo, registerDevice, editDevice, setPower, setPowerOff, setPowerOn, getPower, getEnvironmentTemperature, getTargetTemperature, setTargetTemperature, } = configure();
@@ -8,6 +8,17 @@ import { DeviceAssociationResponse, 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.
@@ -15,8 +26,9 @@ declare const headers: (jwtToken: string) => {
15
26
  */
16
27
  declare const createAuthService: (auth: typeof amplifyAuth) => {
17
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, legacy?: boolean) => 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.
@@ -32,12 +44,12 @@ declare const configure: (baseURL?: string) => {
32
44
  deviceInfo: (jwtToken: string, macAddress: string) => Promise<DeviceInfoType>;
33
45
  registerDevice: (jwtToken: string, macAddress: string, serialNumber: string, deviceName?: string, deviceRoom?: string) => Promise<DeviceAssociationResponse>;
34
46
  editDevice: (jwtToken: string, macAddress: string, deviceName?: string, deviceRoom?: string) => Promise<DeviceAssociationResponse>;
35
- setPower: (jwtToken: string, macAddress: string, value: number) => Promise<import("axios").AxiosResponse<any, any, {}>>;
36
- setPowerOff: (jwtToken: string, macAddress: string) => Promise<import("axios").AxiosResponse<any, any, {}>>;
37
- setPowerOn: (jwtToken: string, macAddress: string) => Promise<import("axios").AxiosResponse<any, any, {}>>;
47
+ setPower: (jwtToken: string, macAddress: string, value: number) => Promise<unknown>;
48
+ setPowerOff: (jwtToken: string, macAddress: string) => Promise<unknown>;
49
+ setPowerOn: (jwtToken: string, macAddress: string) => Promise<unknown>;
38
50
  getPower: (jwtToken: string, macAddress: string) => Promise<boolean>;
39
51
  getEnvironmentTemperature: (jwtToken: string, macAddress: string) => Promise<number>;
40
52
  getTargetTemperature: (jwtToken: string, macAddress: string) => Promise<number>;
41
- setTargetTemperature: (jwtToken: string, macAddress: string, temperature: number) => Promise<import("axios").AxiosResponse<any, any, {}>>;
53
+ setTargetTemperature: (jwtToken: string, macAddress: string, temperature: number) => Promise<unknown>;
42
54
  };
43
- export { configure, createAuthService, headers, signIn };
55
+ export { configure, configureAmplify, createAuthService, getSession, headers, signIn, };