edilkamin 1.6.2 → 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 +31 -7
- package/dist/esm/cli.js +69 -10
- package/dist/esm/index.d.ts +2 -1
- package/dist/esm/index.js +2 -1
- package/dist/esm/library.d.ts +14 -2
- package/dist/esm/library.js +28 -4
- package/dist/esm/library.test.js +63 -2
- package/dist/esm/token-storage.d.ts +14 -0
- package/dist/esm/token-storage.js +81 -0
- package/eslint.config.mjs +12 -1
- package/package.json +2 -2
- package/src/buffer-utils.test.ts +1 -1
- package/src/cli.ts +113 -40
- package/src/index.ts +2 -1
- package/src/library.test.ts +85 -10
- package/src/library.ts +52 -11
- package/src/token-storage.ts +78 -0
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
|
-
.
|
|
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
|
|
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
|
-
|
|
75
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
245
|
-
|
|
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
|
|
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
|
-
|
|
273
|
-
|
|
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);
|
package/src/index.ts
CHANGED
|
@@ -2,12 +2,13 @@ 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,
|
|
9
9
|
serialNumberToHex,
|
|
10
10
|
} from "./serial-utils";
|
|
11
|
+
export { clearSession } from "./token-storage";
|
|
11
12
|
export {
|
|
12
13
|
BufferEncodedType,
|
|
13
14
|
CommandsType,
|
package/src/library.test.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { strict as assert } from "assert";
|
|
2
|
+
import * as amplifyAuth from "aws-amplify/auth";
|
|
2
3
|
import axios from "axios";
|
|
3
4
|
import pako from "pako";
|
|
4
5
|
import sinon from "sinon";
|
|
@@ -10,7 +11,7 @@ import { API_URL } from "./constants";
|
|
|
10
11
|
* Helper to create a gzip-compressed Buffer object for testing.
|
|
11
12
|
*/
|
|
12
13
|
const createGzippedBuffer = (
|
|
13
|
-
data: unknown
|
|
14
|
+
data: unknown,
|
|
14
15
|
): { type: "Buffer"; data: number[] } => {
|
|
15
16
|
const json = JSON.stringify(data);
|
|
16
17
|
const compressed = pako.gzip(json);
|
|
@@ -57,7 +58,7 @@ describe("library", () => {
|
|
|
57
58
|
const authService = createAuthService(authStub as any);
|
|
58
59
|
const token = await authService.signIn(
|
|
59
60
|
expectedUsername,
|
|
60
|
-
expectedPassword
|
|
61
|
+
expectedPassword,
|
|
61
62
|
);
|
|
62
63
|
assert.deepEqual(authStub.signOut.args, [[]]);
|
|
63
64
|
assert.deepEqual(signIn.args, [
|
|
@@ -87,7 +88,7 @@ describe("library", () => {
|
|
|
87
88
|
const token = await authService.signIn(
|
|
88
89
|
expectedUsername,
|
|
89
90
|
expectedPassword,
|
|
90
|
-
true // legacy mode
|
|
91
|
+
true, // legacy mode
|
|
91
92
|
);
|
|
92
93
|
assert.equal(token, expectedToken);
|
|
93
94
|
});
|
|
@@ -114,8 +115,82 @@ describe("library", () => {
|
|
|
114
115
|
{
|
|
115
116
|
name: "AssertionError",
|
|
116
117
|
message: "Sign-in failed",
|
|
117
|
-
}
|
|
118
|
+
},
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe("getSession", () => {
|
|
124
|
+
it("should return idToken by default", async () => {
|
|
125
|
+
const mockAuth = {
|
|
126
|
+
signIn: sinon.stub().resolves({ isSignedIn: true }),
|
|
127
|
+
signOut: sinon.stub().resolves(),
|
|
128
|
+
fetchAuthSession: sinon.stub().resolves({
|
|
129
|
+
tokens: {
|
|
130
|
+
idToken: { toString: () => "mock-id-token" },
|
|
131
|
+
accessToken: { toString: () => "mock-access-token" },
|
|
132
|
+
},
|
|
133
|
+
}),
|
|
134
|
+
};
|
|
135
|
+
const { getSession, signIn } = createAuthService(
|
|
136
|
+
mockAuth as unknown as typeof amplifyAuth,
|
|
137
|
+
);
|
|
138
|
+
await signIn("user", "pass");
|
|
139
|
+
const token = await getSession();
|
|
140
|
+
assert.equal(token, "mock-id-token");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("should return accessToken when legacy=true", async () => {
|
|
144
|
+
const mockAuth = {
|
|
145
|
+
signIn: sinon.stub().resolves({ isSignedIn: true }),
|
|
146
|
+
signOut: sinon.stub().resolves(),
|
|
147
|
+
fetchAuthSession: sinon.stub().resolves({
|
|
148
|
+
tokens: {
|
|
149
|
+
idToken: { toString: () => "mock-id-token" },
|
|
150
|
+
accessToken: { toString: () => "mock-access-token" },
|
|
151
|
+
},
|
|
152
|
+
}),
|
|
153
|
+
};
|
|
154
|
+
const { getSession, signIn } = createAuthService(
|
|
155
|
+
mockAuth as unknown as typeof amplifyAuth,
|
|
156
|
+
);
|
|
157
|
+
await signIn("user", "pass");
|
|
158
|
+
const token = await getSession(false, true);
|
|
159
|
+
assert.equal(token, "mock-access-token");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("should throw error when no session exists", async () => {
|
|
163
|
+
const mockAuth = {
|
|
164
|
+
signIn: sinon.stub().resolves({ isSignedIn: true }),
|
|
165
|
+
signOut: sinon.stub().resolves(),
|
|
166
|
+
fetchAuthSession: sinon.stub().resolves({ tokens: null }),
|
|
167
|
+
};
|
|
168
|
+
const { getSession } = createAuthService(
|
|
169
|
+
mockAuth as unknown as typeof amplifyAuth,
|
|
170
|
+
);
|
|
171
|
+
await assert.rejects(async () => getSession(), {
|
|
172
|
+
name: "AssertionError",
|
|
173
|
+
message: "No session found - please sign in first",
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("should pass forceRefresh to fetchAuthSession", async () => {
|
|
178
|
+
const mockAuth = {
|
|
179
|
+
signIn: sinon.stub().resolves({ isSignedIn: true }),
|
|
180
|
+
signOut: sinon.stub().resolves(),
|
|
181
|
+
fetchAuthSession: sinon.stub().resolves({
|
|
182
|
+
tokens: {
|
|
183
|
+
idToken: { toString: () => "mock-id-token" },
|
|
184
|
+
accessToken: { toString: () => "mock-access-token" },
|
|
185
|
+
},
|
|
186
|
+
}),
|
|
187
|
+
};
|
|
188
|
+
const { getSession, signIn } = createAuthService(
|
|
189
|
+
mockAuth as unknown as typeof amplifyAuth,
|
|
118
190
|
);
|
|
191
|
+
await signIn("user", "pass");
|
|
192
|
+
await getSession(true);
|
|
193
|
+
assert.ok(mockAuth.fetchAuthSession.calledWith({ forceRefresh: true }));
|
|
119
194
|
});
|
|
120
195
|
});
|
|
121
196
|
|
|
@@ -278,7 +353,7 @@ describe("library", () => {
|
|
|
278
353
|
api: ReturnType<typeof configure>,
|
|
279
354
|
token: string,
|
|
280
355
|
mac: string,
|
|
281
|
-
value: number
|
|
356
|
+
value: number,
|
|
282
357
|
) => api.setTargetTemperature(token, mac, value),
|
|
283
358
|
payload: {
|
|
284
359
|
name: "enviroment_1_temperature",
|
|
@@ -298,7 +373,7 @@ describe("library", () => {
|
|
|
298
373
|
api,
|
|
299
374
|
expectedToken,
|
|
300
375
|
"mockMacAddress",
|
|
301
|
-
payload.value
|
|
376
|
+
payload.value,
|
|
302
377
|
);
|
|
303
378
|
|
|
304
379
|
assert.deepEqual(mockAxios.put.args, [
|
|
@@ -339,7 +414,7 @@ describe("library", () => {
|
|
|
339
414
|
"AA:BB:CC:DD:EE:FF",
|
|
340
415
|
"EDK123",
|
|
341
416
|
"Test Stove",
|
|
342
|
-
"Living Room"
|
|
417
|
+
"Living Room",
|
|
343
418
|
);
|
|
344
419
|
|
|
345
420
|
assert.deepEqual(mockAxios.post.args, [
|
|
@@ -407,7 +482,7 @@ describe("library", () => {
|
|
|
407
482
|
expectedToken,
|
|
408
483
|
"AA:BB:CC:DD:EE:FF",
|
|
409
484
|
"Updated Name",
|
|
410
|
-
"Basement"
|
|
485
|
+
"Basement",
|
|
411
486
|
);
|
|
412
487
|
|
|
413
488
|
assert.deepEqual(mockAxios.put.args, [
|
|
@@ -564,7 +639,7 @@ describe("library", () => {
|
|
|
564
639
|
|
|
565
640
|
const result = await api.getEnvironmentTemperature(
|
|
566
641
|
expectedToken,
|
|
567
|
-
"mockMacAddress"
|
|
642
|
+
"mockMacAddress",
|
|
568
643
|
);
|
|
569
644
|
|
|
570
645
|
assert.equal(result, 19);
|
|
@@ -589,7 +664,7 @@ describe("library", () => {
|
|
|
589
664
|
|
|
590
665
|
const result = await api.getTargetTemperature(
|
|
591
666
|
expectedToken,
|
|
592
|
-
"mockMacAddress"
|
|
667
|
+
"mockMacAddress",
|
|
593
668
|
);
|
|
594
669
|
|
|
595
670
|
assert.equal(result, 22);
|
package/src/library.ts
CHANGED
|
@@ -1,6 +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 { cognitoUserPoolsTokenProvider } from "aws-amplify/auth/cognito";
|
|
4
5
|
import axios, { AxiosInstance } from "axios";
|
|
5
6
|
|
|
6
7
|
import { processResponse } from "./buffer-utils";
|
|
@@ -31,10 +32,19 @@ let amplifyConfigured = false;
|
|
|
31
32
|
/**
|
|
32
33
|
* Configures Amplify if not already configured.
|
|
33
34
|
* Uses a local flag to avoid calling getConfig() which prints a warning.
|
|
35
|
+
* @param {object} [storage] - Optional custom storage adapter for token persistence
|
|
34
36
|
*/
|
|
35
|
-
const configureAmplify = (
|
|
37
|
+
const configureAmplify = (storage?: {
|
|
38
|
+
setItem: (key: string, value: string) => Promise<void>;
|
|
39
|
+
getItem: (key: string) => Promise<string | null>;
|
|
40
|
+
removeItem: (key: string) => Promise<void>;
|
|
41
|
+
clear: () => Promise<void>;
|
|
42
|
+
}) => {
|
|
36
43
|
if (amplifyConfigured) return;
|
|
37
44
|
Amplify.configure(amplifyconfiguration);
|
|
45
|
+
if (storage) {
|
|
46
|
+
cognitoUserPoolsTokenProvider.setKeyValueStorage(storage);
|
|
47
|
+
}
|
|
38
48
|
amplifyConfigured = true;
|
|
39
49
|
};
|
|
40
50
|
|
|
@@ -55,7 +65,7 @@ const createAuthService = (auth: typeof amplifyAuth) => {
|
|
|
55
65
|
const signIn = async (
|
|
56
66
|
username: string,
|
|
57
67
|
password: string,
|
|
58
|
-
legacy: boolean = false
|
|
68
|
+
legacy: boolean = false,
|
|
59
69
|
): Promise<string> => {
|
|
60
70
|
configureAmplify();
|
|
61
71
|
await auth.signOut(); // Ensure the user is signed out first
|
|
@@ -70,11 +80,35 @@ const createAuthService = (auth: typeof amplifyAuth) => {
|
|
|
70
80
|
assert.ok(tokens.idToken, "No ID token found");
|
|
71
81
|
return tokens.idToken.toString();
|
|
72
82
|
};
|
|
73
|
-
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Retrieves the current session, refreshing tokens if necessary.
|
|
86
|
+
* Requires a prior successful signIn() call.
|
|
87
|
+
* @param {boolean} [forceRefresh=false] - Force token refresh even if valid
|
|
88
|
+
* @param {boolean} [legacy=false] - If true, returns accessToken for legacy API
|
|
89
|
+
* @returns {Promise<string>} - The JWT token (idToken or accessToken)
|
|
90
|
+
* @throws {Error} - If no session exists (user needs to sign in)
|
|
91
|
+
*/
|
|
92
|
+
const getSession = async (
|
|
93
|
+
forceRefresh: boolean = false,
|
|
94
|
+
legacy: boolean = false,
|
|
95
|
+
): Promise<string> => {
|
|
96
|
+
configureAmplify();
|
|
97
|
+
const { tokens } = await auth.fetchAuthSession({ forceRefresh });
|
|
98
|
+
assert.ok(tokens, "No session found - please sign in first");
|
|
99
|
+
if (legacy) {
|
|
100
|
+
assert.ok(tokens.accessToken, "No access token found");
|
|
101
|
+
return tokens.accessToken.toString();
|
|
102
|
+
}
|
|
103
|
+
assert.ok(tokens.idToken, "No ID token found");
|
|
104
|
+
return tokens.idToken.toString();
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
return { signIn, getSession };
|
|
74
108
|
};
|
|
75
109
|
|
|
76
110
|
// Create the default auth service using amplifyAuth
|
|
77
|
-
const { signIn } = createAuthService(amplifyAuth);
|
|
111
|
+
const { signIn, getSession } = createAuthService(amplifyAuth);
|
|
78
112
|
|
|
79
113
|
const deviceInfo =
|
|
80
114
|
(axiosInstance: AxiosInstance) =>
|
|
@@ -91,7 +125,7 @@ const deviceInfo =
|
|
|
91
125
|
`device/${macAddress}/info`,
|
|
92
126
|
{
|
|
93
127
|
headers: headers(jwtToken),
|
|
94
|
-
}
|
|
128
|
+
},
|
|
95
129
|
);
|
|
96
130
|
// Process response to decompress any gzipped Buffer fields
|
|
97
131
|
return processResponse(response.data) as DeviceInfoType;
|
|
@@ -104,7 +138,7 @@ const mqttCommand =
|
|
|
104
138
|
axiosInstance.put(
|
|
105
139
|
"mqtt/command",
|
|
106
140
|
{ mac_address: macAddress, ...payload },
|
|
107
|
-
{ headers: headers(jwtToken) }
|
|
141
|
+
{ headers: headers(jwtToken) },
|
|
108
142
|
);
|
|
109
143
|
|
|
110
144
|
const setPower =
|
|
@@ -228,7 +262,7 @@ const registerDevice =
|
|
|
228
262
|
macAddress: string,
|
|
229
263
|
serialNumber: string,
|
|
230
264
|
deviceName: string = "",
|
|
231
|
-
deviceRoom: string = ""
|
|
265
|
+
deviceRoom: string = "",
|
|
232
266
|
): Promise<DeviceAssociationResponse> => {
|
|
233
267
|
const body: DeviceAssociationBody = {
|
|
234
268
|
macAddress: macAddress.replace(/:/g, ""),
|
|
@@ -239,7 +273,7 @@ const registerDevice =
|
|
|
239
273
|
const response = await axiosInstance.post<DeviceAssociationResponse>(
|
|
240
274
|
"device",
|
|
241
275
|
body,
|
|
242
|
-
{ headers: headers(jwtToken) }
|
|
276
|
+
{ headers: headers(jwtToken) },
|
|
243
277
|
);
|
|
244
278
|
return response.data;
|
|
245
279
|
};
|
|
@@ -259,7 +293,7 @@ const editDevice =
|
|
|
259
293
|
jwtToken: string,
|
|
260
294
|
macAddress: string,
|
|
261
295
|
deviceName: string = "",
|
|
262
|
-
deviceRoom: string = ""
|
|
296
|
+
deviceRoom: string = "",
|
|
263
297
|
): Promise<DeviceAssociationResponse> => {
|
|
264
298
|
const normalizedMac = macAddress.replace(/:/g, "");
|
|
265
299
|
const body: EditDeviceAssociationBody = {
|
|
@@ -269,7 +303,7 @@ const editDevice =
|
|
|
269
303
|
const response = await axiosInstance.put<DeviceAssociationResponse>(
|
|
270
304
|
`device/${normalizedMac}`,
|
|
271
305
|
body,
|
|
272
|
-
{ headers: headers(jwtToken) }
|
|
306
|
+
{ headers: headers(jwtToken) },
|
|
273
307
|
);
|
|
274
308
|
return response.data;
|
|
275
309
|
};
|
|
@@ -312,4 +346,11 @@ const configure = (baseURL: string = API_URL) => {
|
|
|
312
346
|
};
|
|
313
347
|
};
|
|
314
348
|
|
|
315
|
-
export {
|
|
349
|
+
export {
|
|
350
|
+
configure,
|
|
351
|
+
configureAmplify,
|
|
352
|
+
createAuthService,
|
|
353
|
+
getSession,
|
|
354
|
+
headers,
|
|
355
|
+
signIn,
|
|
356
|
+
};
|