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.
@@ -0,0 +1,48 @@
1
+ import { strict as assert } from "assert";
2
+ import { serialNumberDisplay, serialNumberFromHex, serialNumberToHex, } from "./serial-utils";
3
+ describe("serial-utils", () => {
4
+ describe("serialNumberToHex", () => {
5
+ it("should convert ASCII string to hex", () => {
6
+ assert.equal(serialNumberToHex("EDK123"), "45444b313233");
7
+ });
8
+ it("should handle empty string", () => {
9
+ assert.equal(serialNumberToHex(""), "");
10
+ });
11
+ it("should convert string with non-printable chars", () => {
12
+ const input = "EDK\x00123";
13
+ const hex = serialNumberToHex(input);
14
+ assert.equal(hex, "45444b00313233");
15
+ });
16
+ });
17
+ describe("serialNumberFromHex", () => {
18
+ it("should convert hex back to ASCII string", () => {
19
+ assert.equal(serialNumberFromHex("45444b313233"), "EDK123");
20
+ });
21
+ it("should handle empty string", () => {
22
+ assert.equal(serialNumberFromHex(""), "");
23
+ });
24
+ it("should round-trip with toHex", () => {
25
+ const original = "EDK\x00123\x1F";
26
+ const hex = serialNumberToHex(original);
27
+ const restored = serialNumberFromHex(hex);
28
+ assert.equal(restored, original);
29
+ });
30
+ });
31
+ describe("serialNumberDisplay", () => {
32
+ it("should remove non-printable characters", () => {
33
+ assert.equal(serialNumberDisplay("EDK\x00123\x1F"), "EDK123");
34
+ });
35
+ it("should collapse whitespace", () => {
36
+ assert.equal(serialNumberDisplay("EDK 123"), "EDK 123");
37
+ });
38
+ it("should trim leading and trailing whitespace", () => {
39
+ assert.equal(serialNumberDisplay(" EDK123 "), "EDK123");
40
+ });
41
+ it("should handle empty string", () => {
42
+ assert.equal(serialNumberDisplay(""), "");
43
+ });
44
+ it("should preserve normal serial numbers", () => {
45
+ assert.equal(serialNumberDisplay("EDK12345678"), "EDK12345678");
46
+ });
47
+ });
48
+ });
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Custom storage adapter for AWS Amplify that persists to file system.
3
+ * Used for CLI to maintain sessions between invocations.
4
+ */
5
+ export declare const createFileStorage: () => {
6
+ setItem: (key: string, value: string) => Promise<void>;
7
+ getItem: (key: string) => Promise<string | null>;
8
+ removeItem: (key: string) => Promise<void>;
9
+ clear: () => Promise<void>;
10
+ };
11
+ /**
12
+ * Clears all stored session data.
13
+ */
14
+ export declare const clearSession: () => Promise<void>;
@@ -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
+ });
@@ -1,3 +1,11 @@
1
+ /**
2
+ * Represents a Node.js Buffer object serialized to JSON.
3
+ * This format is used by the Edilkamin API for gzip-compressed fields.
4
+ */
5
+ interface BufferEncodedType {
6
+ type: "Buffer";
7
+ data: number[];
8
+ }
1
9
  interface CommandsType {
2
10
  power: boolean;
3
11
  }
@@ -22,4 +30,44 @@ interface DeviceInfoType {
22
30
  user_parameters: UserParametersType;
23
31
  };
24
32
  }
25
- export type { CommandsType, DeviceInfoType, StatusType, TemperaturesType, UserParametersType, };
33
+ /**
34
+ * Raw device info response that may contain Buffer-encoded compressed fields.
35
+ * Used internally before processing; external callers receive DeviceInfoType.
36
+ */
37
+ interface DeviceInfoRawType {
38
+ status: StatusType | BufferEncodedType;
39
+ nvm: {
40
+ user_parameters: UserParametersType;
41
+ } | BufferEncodedType;
42
+ component_info?: BufferEncodedType | Record<string, unknown>;
43
+ }
44
+ /**
45
+ * Request body for registering a device with a user account.
46
+ * All fields are required by the API.
47
+ */
48
+ interface DeviceAssociationBody {
49
+ macAddress: string;
50
+ deviceName: string;
51
+ deviceRoom: string;
52
+ serialNumber: string;
53
+ }
54
+ /**
55
+ * Request body for editing a device's name and room.
56
+ * MAC address is specified in the URL path, not the body.
57
+ * Serial number cannot be changed after registration.
58
+ */
59
+ interface EditDeviceAssociationBody {
60
+ deviceName: string;
61
+ deviceRoom: string;
62
+ }
63
+ /**
64
+ * Response from device registration endpoint.
65
+ * Structure based on Android app behavior - may need adjustment after testing.
66
+ */
67
+ interface DeviceAssociationResponse {
68
+ macAddress: string;
69
+ deviceName: string;
70
+ deviceRoom: string;
71
+ serialNumber: string;
72
+ }
73
+ export type { BufferEncodedType, CommandsType, DeviceAssociationBody, DeviceAssociationResponse, DeviceInfoRawType, DeviceInfoType, EditDeviceAssociationBody, StatusType, TemperaturesType, UserParametersType, };
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,6 +1,6 @@
1
1
  {
2
2
  "name": "edilkamin",
3
- "version": "1.6.1",
3
+ "version": "1.7.2",
4
4
  "description": "",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -42,13 +42,15 @@
42
42
  },
43
43
  "dependencies": {
44
44
  "aws-amplify": "^6.10.0",
45
- "axios": "^1.13.2"
45
+ "axios": "^1.13.2",
46
+ "pako": "^2.1.0"
46
47
  },
47
48
  "devDependencies": {
48
49
  "@aws-amplify/cli": "^7.6.21",
49
50
  "@eslint/eslintrc": "^3.2.0",
50
51
  "@eslint/js": "^9.16.0",
51
52
  "@types/mocha": "^10.0.10",
53
+ "@types/pako": "^2.0.4",
52
54
  "@types/sinon": "^17.0.3",
53
55
  "@typescript-eslint/eslint-plugin": "^8.17.0",
54
56
  "@typescript-eslint/parser": "^8.17.0",
@@ -58,7 +60,7 @@
58
60
  "eslint-plugin-simple-import-sort": "^12.1.1",
59
61
  "mocha": "^11.7.5",
60
62
  "nyc": "^17.1.0",
61
- "prettier": "^2.5.1",
63
+ "prettier": "^3.7.4",
62
64
  "sinon": "^19.0.2",
63
65
  "ts-node": "^10.9.1",
64
66
  "typedoc": "^0.28.15",
@@ -0,0 +1,225 @@
1
+ import { strict as assert } from "assert";
2
+ import pako from "pako";
3
+ import sinon from "sinon";
4
+
5
+ import { decompressBuffer, isBuffer, processResponse } from "./buffer-utils";
6
+
7
+ /**
8
+ * Helper to create a gzip-compressed Buffer object for testing.
9
+ */
10
+ const createGzippedBuffer = (
11
+ data: unknown,
12
+ ): { type: "Buffer"; data: number[] } => {
13
+ const json = JSON.stringify(data);
14
+ const compressed = pako.gzip(json);
15
+ return {
16
+ type: "Buffer",
17
+ data: Array.from(compressed),
18
+ };
19
+ };
20
+
21
+ describe("buffer-utils", () => {
22
+ afterEach(() => {
23
+ sinon.restore();
24
+ });
25
+
26
+ describe("isBuffer", () => {
27
+ it("should detect valid Buffer objects", () => {
28
+ const buffer = { type: "Buffer", data: [31, 139, 8, 0] };
29
+ assert.ok(isBuffer(buffer));
30
+ });
31
+
32
+ it("should detect empty Buffer objects", () => {
33
+ const buffer = { type: "Buffer", data: [] };
34
+ assert.ok(isBuffer(buffer));
35
+ });
36
+
37
+ it("should reject non-Buffer objects with wrong type", () => {
38
+ assert.ok(!isBuffer({ type: "NotBuffer", data: [] }));
39
+ });
40
+
41
+ it("should reject objects without type field", () => {
42
+ assert.ok(!isBuffer({ data: [1, 2, 3] }));
43
+ });
44
+
45
+ it("should reject objects without data field", () => {
46
+ assert.ok(!isBuffer({ type: "Buffer" }));
47
+ });
48
+
49
+ it("should reject objects with non-array data", () => {
50
+ assert.ok(!isBuffer({ type: "Buffer", data: "not an array" }));
51
+ });
52
+
53
+ it("should reject null", () => {
54
+ assert.ok(!isBuffer(null));
55
+ });
56
+
57
+ it("should reject undefined", () => {
58
+ assert.ok(!isBuffer(undefined));
59
+ });
60
+
61
+ it("should reject primitives", () => {
62
+ assert.ok(!isBuffer("string"));
63
+ assert.ok(!isBuffer(123));
64
+ assert.ok(!isBuffer(true));
65
+ });
66
+ });
67
+
68
+ describe("decompressBuffer", () => {
69
+ it("should decompress gzipped JSON buffer", () => {
70
+ const originalData = { test: "value", nested: { key: 123 } };
71
+ const bufferObj = createGzippedBuffer(originalData);
72
+
73
+ const result = decompressBuffer(bufferObj);
74
+ assert.deepEqual(result, originalData);
75
+ });
76
+
77
+ it("should handle gzipped arrays", () => {
78
+ const originalData = [1, 2, 3, "test"];
79
+ const bufferObj = createGzippedBuffer(originalData);
80
+
81
+ const result = decompressBuffer(bufferObj);
82
+ assert.deepEqual(result, originalData);
83
+ });
84
+
85
+ it("should handle gzipped strings", () => {
86
+ const originalData = "test string";
87
+ const bufferObj = createGzippedBuffer(originalData);
88
+
89
+ const result = decompressBuffer(bufferObj);
90
+ assert.equal(result, originalData);
91
+ });
92
+
93
+ it("should return original value if decompression fails", () => {
94
+ const consoleWarnStub = sinon.stub(console, "warn");
95
+ const invalidBuffer = { type: "Buffer" as const, data: [1, 2, 3] };
96
+
97
+ const result = decompressBuffer(invalidBuffer);
98
+
99
+ assert.deepEqual(result, invalidBuffer);
100
+ assert.ok(consoleWarnStub.calledOnce);
101
+ });
102
+
103
+ it("should return original value if JSON parsing fails", () => {
104
+ const consoleWarnStub = sinon.stub(console, "warn");
105
+ // Create valid gzip but invalid JSON
106
+ const invalidJson = "not valid json {";
107
+ const compressed = pako.gzip(invalidJson);
108
+ const bufferObj = {
109
+ type: "Buffer" as const,
110
+ data: Array.from(compressed),
111
+ };
112
+
113
+ const result = decompressBuffer(bufferObj);
114
+
115
+ assert.deepEqual(result, bufferObj);
116
+ assert.ok(consoleWarnStub.calledOnce);
117
+ });
118
+ });
119
+
120
+ describe("processResponse", () => {
121
+ it("should pass through null", () => {
122
+ assert.equal(processResponse(null), null);
123
+ });
124
+
125
+ it("should pass through undefined", () => {
126
+ assert.equal(processResponse(undefined), undefined);
127
+ });
128
+
129
+ it("should pass through primitives", () => {
130
+ assert.equal(processResponse("string"), "string");
131
+ assert.equal(processResponse(123), 123);
132
+ assert.equal(processResponse(true), true);
133
+ });
134
+
135
+ it("should pass through plain objects", () => {
136
+ const obj = { key: "value", nested: { num: 42 } };
137
+ assert.deepEqual(processResponse(obj), obj);
138
+ });
139
+
140
+ it("should pass through plain arrays", () => {
141
+ const arr = [1, "two", { three: 3 }];
142
+ assert.deepEqual(processResponse(arr), arr);
143
+ });
144
+
145
+ it("should decompress Buffer at root level", () => {
146
+ const originalData = { decompressed: true };
147
+ const buffer = createGzippedBuffer(originalData);
148
+
149
+ const result = processResponse(buffer);
150
+ assert.deepEqual(result, originalData);
151
+ });
152
+
153
+ it("should decompress nested Buffer fields", () => {
154
+ const statusData = { commands: { power: true } };
155
+ const response = {
156
+ plain: "data",
157
+ status: createGzippedBuffer(statusData),
158
+ };
159
+
160
+ const result = processResponse(response);
161
+ assert.equal(result.plain, "data");
162
+ assert.deepEqual(result.status, statusData);
163
+ });
164
+
165
+ it("should recursively decompress deeply nested Buffers", () => {
166
+ const innerData = { value: 42 };
167
+ const middleData = { inner: createGzippedBuffer(innerData) };
168
+ const response = {
169
+ outer: createGzippedBuffer(middleData),
170
+ };
171
+
172
+ const result = processResponse(response);
173
+ assert.deepEqual(result, { outer: { inner: { value: 42 } } });
174
+ });
175
+
176
+ it("should handle arrays containing Buffers", () => {
177
+ const itemData = { id: 1 };
178
+ const response = {
179
+ items: [createGzippedBuffer(itemData), { id: 2 }],
180
+ };
181
+
182
+ const result = processResponse(response);
183
+ assert.deepEqual(result.items, [{ id: 1 }, { id: 2 }]);
184
+ });
185
+
186
+ it("should handle mixed compressed and uncompressed fields", () => {
187
+ const compressedStatus = { commands: { power: true } };
188
+ const response = {
189
+ status: createGzippedBuffer(compressedStatus),
190
+ nvm: { user_parameters: { temperature: 22 } },
191
+ plain_field: "unchanged",
192
+ };
193
+
194
+ const result = processResponse(response);
195
+ assert.deepEqual(result.status, compressedStatus);
196
+ assert.deepEqual(result.nvm, { user_parameters: { temperature: 22 } });
197
+ assert.equal(result.plain_field, "unchanged");
198
+ });
199
+
200
+ it("should handle real-world DeviceInfo structure with compressed status", () => {
201
+ const statusData = {
202
+ commands: { power: true },
203
+ temperatures: { board: 25, enviroment: 20 },
204
+ };
205
+ const nvmData = {
206
+ user_parameters: {
207
+ enviroment_1_temperature: 22,
208
+ enviroment_2_temperature: 0,
209
+ enviroment_3_temperature: 0,
210
+ is_auto: false,
211
+ is_sound_active: true,
212
+ },
213
+ };
214
+
215
+ const response = {
216
+ status: createGzippedBuffer(statusData),
217
+ nvm: createGzippedBuffer(nvmData),
218
+ };
219
+
220
+ const result = processResponse(response);
221
+ assert.deepEqual(result.status, statusData);
222
+ assert.deepEqual(result.nvm, nvmData);
223
+ });
224
+ });
225
+ });
@@ -0,0 +1,83 @@
1
+ import pako from "pako";
2
+
3
+ import { BufferEncodedType } from "./types";
4
+
5
+ /**
6
+ * Type guard to check if a value is a serialized Node.js Buffer.
7
+ * Node.js Buffers serialize to JSON as: {type: "Buffer", data: [...]}
8
+ *
9
+ * @param value - The value to check
10
+ * @returns True if the value is a Buffer-encoded object
11
+ */
12
+ const isBuffer = (value: unknown): value is BufferEncodedType => {
13
+ return (
14
+ typeof value === "object" &&
15
+ value !== null &&
16
+ "type" in value &&
17
+ (value as Record<string, unknown>).type === "Buffer" &&
18
+ "data" in value &&
19
+ Array.isArray((value as Record<string, unknown>).data)
20
+ );
21
+ };
22
+
23
+ /**
24
+ * Decompresses a Buffer-encoded gzip object and parses the resulting JSON.
25
+ *
26
+ * @param bufferObj - A serialized Buffer object containing gzip data
27
+ * @returns The decompressed and parsed JSON data, or the original object on failure
28
+ */
29
+ const decompressBuffer = (bufferObj: BufferEncodedType): unknown => {
30
+ try {
31
+ // Convert data array to Uint8Array for pako
32
+ const compressed = new Uint8Array(bufferObj.data);
33
+
34
+ // Decompress with gzip
35
+ const decompressed = pako.ungzip(compressed, { to: "string" });
36
+
37
+ // Parse JSON
38
+ return JSON.parse(decompressed);
39
+ } catch (error) {
40
+ // Log warning but return original to maintain backward compatibility
41
+ console.warn("Failed to decompress buffer:", error);
42
+ return bufferObj;
43
+ }
44
+ };
45
+
46
+ /**
47
+ * Recursively processes an API response to decompress any Buffer-encoded fields.
48
+ * Handles nested objects and arrays, preserving structure while decompressing.
49
+ *
50
+ * @param data - The API response data to process
51
+ * @returns The processed data with all Buffer fields decompressed
52
+ */
53
+ const processResponse = <T>(data: T): T => {
54
+ if (data === null || data === undefined) {
55
+ return data;
56
+ }
57
+
58
+ // Check if this is a Buffer object
59
+ if (isBuffer(data)) {
60
+ const decompressed = decompressBuffer(data);
61
+ // Recursively process the decompressed result (may contain nested buffers)
62
+ return processResponse(decompressed) as T;
63
+ }
64
+
65
+ // Recursively process arrays
66
+ if (Array.isArray(data)) {
67
+ return data.map((item) => processResponse(item)) as T;
68
+ }
69
+
70
+ // Recursively process objects
71
+ if (typeof data === "object") {
72
+ const processed: Record<string, unknown> = {};
73
+ for (const [key, value] of Object.entries(data)) {
74
+ processed[key] = processResponse(value);
75
+ }
76
+ return processed as T;
77
+ }
78
+
79
+ // Primitive value, return as-is
80
+ return data;
81
+ };
82
+
83
+ export { decompressBuffer, isBuffer, processResponse };