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.
- package/.github/workflows/cli-tests.yml +1 -1
- package/.github/workflows/documentation.yml +1 -1
- package/.github/workflows/publish.yml +5 -3
- package/.github/workflows/tests.yml +1 -1
- package/README.md +61 -7
- package/dist/esm/buffer-utils.d.ts +25 -0
- package/dist/esm/buffer-utils.js +70 -0
- package/dist/esm/buffer-utils.test.d.ts +1 -0
- package/dist/esm/buffer-utils.test.js +181 -0
- package/dist/esm/cli.js +110 -9
- package/dist/esm/constants.d.ts +3 -1
- package/dist/esm/constants.js +4 -2
- package/dist/esm/index.d.ts +7 -4
- package/dist/esm/index.js +6 -3
- package/dist/esm/library.d.ts +18 -4
- package/dist/esm/library.js +87 -9
- package/dist/esm/library.test.js +321 -1
- package/dist/esm/serial-utils.d.ts +33 -0
- package/dist/esm/serial-utils.js +45 -0
- package/dist/esm/serial-utils.test.d.ts +1 -0
- package/dist/esm/serial-utils.test.js +48 -0
- package/dist/esm/token-storage.d.ts +14 -0
- package/dist/esm/token-storage.js +81 -0
- package/dist/esm/types.d.ts +49 -1
- package/eslint.config.mjs +12 -1
- package/package.json +5 -3
- package/src/buffer-utils.test.ts +225 -0
- package/src/buffer-utils.ts +83 -0
- package/src/cli.ts +192 -33
- package/src/constants.ts +5 -2
- package/src/index.ts +16 -2
- package/src/library.test.ts +402 -5
- package/src/library.ts +140 -14
- package/src/serial-utils.test.ts +64 -0
- package/src/serial-utils.ts +50 -0
- package/src/token-storage.ts +78 -0
- package/src/types.ts +60 -0
|
@@ -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
|
+
});
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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": "^
|
|
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 };
|