homebridge-cync-app 0.1.3 → 0.1.5

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.
@@ -1,97 +1,62 @@
1
1
  // homebridge-ui/server.js
2
2
  import { HomebridgePluginUiServer } from '@homebridge/plugin-ui-utils';
3
- import fetch from 'node-fetch';
3
+ import { ConfigClient } from '../dist/cync/config-client.js';
4
+ import { CyncTokenStore } from '../dist/cync/token-store.js';
4
5
 
5
- class PluginUiServer extends HomebridgePluginUiServer {
6
+ class CyncUiServer extends HomebridgePluginUiServer {
6
7
  constructor() {
7
8
  super();
8
9
 
9
- this.onRequest('/requestCode', this.handleRequestCode.bind(this));
10
- this.onRequest('/login', this.handleLogin.bind(this));
10
+ this.configClient = new ConfigClient({
11
+ debug: (...a) => console.debug('[cync-ui-config]', ...a),
12
+ info: (...a) => console.info('[cync-ui-config]', ...a),
13
+ warn: (...a) => console.warn('[cync-ui-config]', ...a),
14
+ error: (...a) => console.error('[cync-ui-config]', ...a),
15
+ });
16
+
17
+ this.tokenStore = new CyncTokenStore(this.homebridgeStoragePath);
18
+
19
+ this.onRequest('/request-otp', this.handleRequestOtp.bind(this));
20
+ this.onRequest('/sign-out', this.handleSignOut.bind(this));
21
+ this.onRequest('/status', this.handleStatus.bind(this));
11
22
 
12
- // Tell Homebridge UI that we’re ready
13
23
  this.ready();
14
24
  }
15
25
 
16
- // Request a 2FA code via email
17
- async handleRequestCode(payload) {
18
- const email = (payload?.emailAddress || '').trim();
26
+ async handleRequestOtp(payload) {
27
+ const email = typeof payload?.email === 'string' ? payload.email.trim() : '';
19
28
  if (!email) {
20
- throw new Error('Email address is required to request a 2FA code.');
29
+ return { ok: false, error: 'Missing email' };
21
30
  }
22
31
 
23
- const requestBody = {
24
- corp_id: '1007d2ad150c4000',
25
- email,
26
- local_lang: 'en-us',
27
- };
28
-
29
- await fetch(
30
- 'https://api.gelighting.com/v2/two_factor/email/verifycode',
31
- {
32
- method: 'POST',
33
- body: JSON.stringify(requestBody),
34
- headers: { 'Content-Type': 'application/json' },
35
- },
36
- );
32
+ await this.configClient.sendTwoFactorCode(email);
33
+ return { ok: true };
37
34
  }
38
35
 
39
- // Validate email + password + 2FA against Cync and return platform config
40
- async handleLogin(payload) {
41
- const email = (payload?.emailAddress || '').trim();
42
- const password = (payload?.password || '').trim();
43
- const mfaCode = (payload?.mfaCode || '').trim();
44
-
45
- if (!email || !password || !mfaCode) {
46
- return {
47
- error: 'Email, password, and 2FA code are required.',
48
- };
49
- }
50
-
51
- const requestBody = {
52
- corp_id: '1007d2ad150c4000',
53
- email,
54
- password,
55
- two_factor: mfaCode,
56
- resource: 'abcdefghijk',
57
- };
36
+ // Delete token file
37
+ async handleSignOut() {
38
+ await this.tokenStore.clear();
39
+ return { ok: true };
40
+ }
58
41
 
42
+ // Report whether a token exists
43
+ async handleStatus() {
59
44
  try {
60
- const response = await fetch(
61
- 'https://api.gelighting.com/v2/user_auth/two_factor',
62
- {
63
- method: 'POST',
64
- body: JSON.stringify(requestBody),
65
- headers: { 'Content-Type': 'application/json' },
66
- },
67
- );
68
-
69
- const data = await response.json();
70
- if (data && data.error) {
71
- return {
72
- error:
73
- 'Login failed. Please check your password and 2FA code.',
74
- };
45
+ const token = await this.tokenStore.load();
46
+ if (!token) {
47
+ return { ok: true, hasToken: false };
75
48
  }
76
- } catch (err) {
77
- console.error('[cync-ui] Login request failed:', err);
78
49
  return {
79
- error:
80
- 'Login failed due to a network or server error. Please try again.',
50
+ ok: true,
51
+ hasToken: true,
52
+ userId: token.userId,
53
+ expiresAt: token.expiresAt ?? null,
81
54
  };
55
+ } catch {
56
+ // On error, just say "no token"
57
+ return { ok: true, hasToken: false };
82
58
  }
83
-
84
- // At this point, Cync accepted the credentials.
85
- // Return the platform config that your platform.ts expects.
86
- return {
87
- platform: 'CyncAppPlatform',
88
- name: 'Cync App',
89
- username: email,
90
- password,
91
- twoFactor: mfaCode,
92
- };
93
59
  }
94
60
  }
95
61
 
96
- // Start the instance
97
- (() => new PluginUiServer())();
62
+ (() => new CyncUiServer())();
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "homebridge-cync-app",
3
3
  "displayName": "Homebridge Cync App",
4
4
  "type": "module",
5
- "version": "0.1.3",
5
+ "version": "0.1.5",
6
6
  "private": false,
7
7
  "description": "Homebridge plugin that integrates your GE Cync account (via the Cync app/API) and exposes all supported devices: plugs, lights, switches, etc",
8
8
  "author": "Dustin Newell",
@@ -19,17 +19,12 @@
19
19
  "homebridge-plugin",
20
20
  "homebridge",
21
21
  "cync",
22
- "ge",
23
22
  "ge cync",
24
- "ge-lighting",
25
23
  "smart plug",
26
24
  "smart lights",
27
25
  "C by GE"
28
26
  ],
29
27
  "main": "dist/index.js",
30
- "publishConfig": {
31
- "access": "public"
32
- },
33
28
  "homebridge": {
34
29
  "pluginType": "platform",
35
30
  "platform": "CyncAppPlatform"
@@ -45,9 +40,8 @@
45
40
  "watch": "npm run build && npm link && nodemon"
46
41
  },
47
42
  "dependencies": {
48
- "homebridge-lib": "^7.1.12",
49
- "node-fetch": "^3.3.2",
50
- "@homebridge/plugin-ui-utils": "^1.0.0"
43
+ "@homebridge/plugin-ui-utils": "^2.1.2",
44
+ "homebridge-lib": "^7.1.12"
51
45
  },
52
46
  "devDependencies": {
53
47
  "@eslint/js": "^9.39.1",
@@ -8,6 +8,7 @@
8
8
  const CYNC_API_BASE = 'https://api.gelighting.com/v2/';
9
9
  const CORP_ID = '1007d2ad150c4000';
10
10
 
11
+ // Minimal fetch/response typing for Node 18+, without depending on DOM lib types.
11
12
  type FetchLike = (input: unknown, init?: unknown) => Promise<unknown>;
12
13
 
13
14
  declare const fetch: FetchLike;
@@ -64,7 +65,10 @@ export interface CyncCloudConfig {
64
65
  meshes: CyncDeviceMesh[];
65
66
  }
66
67
 
67
-
68
+ /**
69
+ * Very small logger interface so we can accept either the Homebridge log
70
+ * object or console.* functions in tests.
71
+ */
68
72
  export interface CyncLogger {
69
73
  debug(message: string, ...args: unknown[]): void;
70
74
  info(message: string, ...args: unknown[]): void;
@@ -147,6 +151,7 @@ export class ConfigClient {
147
151
  email,
148
152
  password,
149
153
  two_factor: otpCode,
154
+ // Matches the reference implementations: random 16-char string.
150
155
  resource: ConfigClient.randomLoginResource(),
151
156
  };
152
157
 
@@ -42,7 +42,12 @@ export class CyncClient {
42
42
  private switchIdToHomeId: Record<number, string> = {};
43
43
 
44
44
  // Credentials from config.json, used to drive 2FA bootstrap.
45
- private readonly loginConfig: { email: string; password: string; twoFactor?: string };
45
+ //
46
+ // Canonical keys (must match platform + config):
47
+ // - username: login identifier (email address used in Cync app)
48
+ // - password: account password
49
+ // - twoFactor: 6-digit OTP, optional; when present we complete 2FA on restart.
50
+ private readonly loginConfig: { username: string; password: string; twoFactor?: string };
46
51
 
47
52
  // Optional LAN update hook for the platform
48
53
  private lanUpdateHandler: ((update: unknown) => void) | null = null;
@@ -70,7 +75,7 @@ export class CyncClient {
70
75
  constructor(
71
76
  configClient: ConfigClient,
72
77
  tcpClient: TcpClient,
73
- loginConfig: { email: string; password: string; twoFactor?: string },
78
+ loginConfig: { username: string; password: string; twoFactor?: string },
74
79
  storagePath: string,
75
80
  logger?: CyncLogger,
76
81
  ) {
@@ -80,7 +85,18 @@ export class CyncClient {
80
85
 
81
86
  this.loginConfig = loginConfig;
82
87
  this.tokenStore = new CyncTokenStore(storagePath);
88
+
89
+ // One-time sanity log so we can see exactly what was passed in from platform/config.
90
+ this.log.debug(
91
+ 'CyncClient: constructed with loginConfig=%o',
92
+ {
93
+ username: loginConfig.username,
94
+ hasPassword: !!loginConfig.password,
95
+ twoFactor: loginConfig.twoFactor,
96
+ },
97
+ );
83
98
  }
99
+
84
100
  // ### 🧩 LAN Login Code Builder
85
101
  private buildLanLoginCode(authorize: string, userId: number): Uint8Array {
86
102
  const authorizeBytes = Buffer.from(authorize, 'ascii');
@@ -133,17 +149,20 @@ export class CyncClient {
133
149
  }
134
150
 
135
151
  // 2) No stored token – run 2FA bootstrap
136
- const { email, password, twoFactor } = this.loginConfig;
152
+ const { username, password, twoFactor } = this.loginConfig;
137
153
 
138
- if (!email || !password) {
139
- this.log.error('CyncClient: email and password are required to obtain a new token.');
154
+ if (!username || !password) {
155
+ this.log.error('CyncClient: username and password are required to obtain a new token.');
140
156
  return false;
141
157
  }
142
158
 
143
- if (!twoFactor || String(twoFactor).trim() === '') {
159
+ const trimmedCode = typeof twoFactor === 'string' ? twoFactor.trim() : '';
160
+ const hasTwoFactor = trimmedCode.length > 0;
161
+
162
+ if (!hasTwoFactor) {
144
163
  // No 2FA code – request one
145
- this.log.info('Cync: starting 2FA handshake for %s', email);
146
- await this.requestTwoFactorCode(email);
164
+ this.log.info('Cync: starting 2FA handshake for %s', username);
165
+ await this.requestTwoFactorCode(username);
147
166
  this.log.info(
148
167
  'Cync: 2FA code sent to your email. Enter the code as "twoFactor" in the plugin config and restart Homebridge to complete login.',
149
168
  );
@@ -151,11 +170,11 @@ export class CyncClient {
151
170
  }
152
171
 
153
172
  // We have a 2FA code – complete login and persist token
154
- this.log.info('Cync: completing 2FA login for %s', email);
173
+ this.log.info('Cync: completing 2FA login for %s', username);
155
174
  const loginResult = await this.completeTwoFactorLogin(
156
- email,
175
+ username,
157
176
  password,
158
- String(twoFactor).trim(),
177
+ trimmedCode,
159
178
  );
160
179
 
161
180
  // Build LAN login code
@@ -191,25 +210,22 @@ export class CyncClient {
191
210
  this.applyAccessToken(tokenData);
192
211
 
193
212
  this.log.info('Cync login successful; userId=%s (token stored)', tokenData.userId);
194
- this.log.info(
195
- 'Cync: 2FA login complete and a token has been stored. You may now clear the "twoFactor" code from the plugin config; ' +
196
- 'it will only be needed again if the stored token expires or is removed.',
197
- );
198
213
  return true;
199
214
  }
200
215
 
201
216
 
202
217
  /**
203
- * Internal helper: request a 2FA email code using existing authenticate().
204
- */
205
- private async requestTwoFactorCode(email: string): Promise<void> {
206
- await this.authenticate(email);
218
+ * Internal helper: request a 2FA email code using existing authenticate().
219
+ * Accepts the same username value we store in loginConfig (email address for Cync).
220
+ */
221
+ private async requestTwoFactorCode(username: string): Promise<void> {
222
+ await this.authenticate(username);
207
223
  }
208
224
 
209
225
  /**
210
- * Internal helper: complete 2FA login using existing submitTwoFactor().
211
- * This converts CyncLoginSession into the richer shape we want for token storage.
212
- */
226
+ * Internal helper: complete 2FA login using existing submitTwoFactor().
227
+ * This converts CyncLoginSession into the richer shape we want for token storage.
228
+ */
213
229
  private async completeTwoFactorLogin(
214
230
  email: string,
215
231
  password: string,
@@ -222,12 +238,15 @@ export class CyncClient {
222
238
  }
223
239
  > {
224
240
  const session = await this.submitTwoFactor(email, password, code);
241
+
225
242
  // Extract authorize field from session.raw (Cync returns it)
226
243
  const raw = session.raw as Record<string, unknown>;
227
244
  const authorize = typeof raw?.authorize === 'string' ? raw.authorize : undefined;
228
245
 
229
246
  if (!authorize) {
230
- throw new Error('CyncClient: missing "authorize" field from login response; LAN login cannot be generated.');
247
+ throw new Error(
248
+ 'CyncClient: missing "authorize" field from login response; LAN login cannot be generated.',
249
+ );
231
250
  }
232
251
 
233
252
  const s = session as unknown as SessionWithPossibleTokens;
@@ -320,11 +339,11 @@ export class CyncClient {
320
339
  * in the same process, so it works across Homebridge restarts.
321
340
  */
322
341
  public async submitTwoFactor(
323
- email: string,
342
+ username: string,
324
343
  password: string,
325
344
  code: string,
326
345
  ): Promise<CyncLoginSession> {
327
- const trimmedEmail = email.trim();
346
+ const trimmedEmail = username.trim();
328
347
  const trimmedCode = code.trim();
329
348
 
330
349
  this.log.info('CyncClient: completing 2FA login for %s', trimmedEmail);
@@ -351,7 +370,6 @@ export class CyncClient {
351
370
  return session;
352
371
  }
353
372
 
354
-
355
373
  /**
356
374
  * Fetch and cache the cloud configuration (meshes/devices) for the logged-in user.
357
375
  * Also builds HA-style LAN topology mappings:
@@ -14,13 +14,6 @@ const defaultLogger: CyncLogger = {
14
14
  export type DeviceUpdateCallback = (payload: unknown) => void;
15
15
  export type RawFrameListener = (frame: Buffer) => void;
16
16
 
17
- interface QueuedPowerPacket {
18
- deviceId: string;
19
- on: boolean;
20
- seq: number;
21
- packet: Buffer;
22
- }
23
-
24
17
  export class TcpClient {
25
18
  public registerSwitchMapping(controllerId: number, deviceId: string): void {
26
19
  if (!Number.isFinite(controllerId)) {
@@ -28,6 +21,7 @@ export class TcpClient {
28
21
  }
29
22
  this.controllerToDevice.set(controllerId, deviceId);
30
23
  }
24
+ private commandChain: Promise<void> = Promise.resolve();
31
25
  private homeDevices: Record<string, string[]> = {};
32
26
  private switchIdToHomeId = new Map<number, string>();
33
27
  private readonly log: CyncLogger;
@@ -44,9 +38,38 @@ export class TcpClient {
44
38
  private heartbeatTimer: NodeJS.Timeout | null = null;
45
39
  private rawFrameListeners: RawFrameListener[] = [];
46
40
  private controllerToDevice = new Map<number, string>();
47
- private connecting: Promise<boolean> | null = null;
48
- private readonly sendQueue: QueuedPowerPacket[] = [];
49
- private sending = false;
41
+ /**
42
+ * ### 🧩 LAN Command Serializer: Ensures TCP commands are sent one at a time
43
+ *
44
+ * Wraps any TCP command in a promise chain so commands are serialized
45
+ * and cannot be written to the socket concurrently.
46
+ */
47
+ private enqueueCommand<T>(fn: () => Promise<T>): Promise<T> {
48
+ let resolveWrapper: (value: T | PromiseLike<T>) => void;
49
+ let rejectWrapper: (reason?: unknown) => void;
50
+
51
+ const p = new Promise<T>((resolve, reject) => {
52
+ resolveWrapper = resolve;
53
+ rejectWrapper = reject;
54
+ });
55
+
56
+ // Chain onto the existing promise
57
+ this.commandChain = this.commandChain
58
+ .then(async () => {
59
+ try {
60
+ const result = await fn();
61
+ resolveWrapper(result);
62
+ } catch (err) {
63
+ rejectWrapper(err);
64
+ }
65
+ })
66
+ .catch(() => {
67
+ // Swallow errors in the chain so a failed command
68
+ // doesn't permanently block the queue.
69
+ });
70
+
71
+ return p;
72
+ }
50
73
  private parseSwitchStateFrame(frame: Buffer): { controllerId: number; on: boolean; level: number } | null {
51
74
  if (frame.length < 16) {
52
75
  return null;
@@ -180,8 +203,7 @@ export class TcpClient {
180
203
 
181
204
  /**
182
205
  * Ensure we have an open, logged-in socket.
183
- * If multiple callers invoke this concurrently, they will share a single
184
- * connection attempt via `this.connecting`.
206
+ * If the socket is closed or missing, attempt to reconnect.
185
207
  */
186
208
  private async ensureConnected(): Promise<boolean> {
187
209
  if (this.socket && !this.socket.destroyed) {
@@ -195,28 +217,8 @@ export class TcpClient {
195
217
  return false;
196
218
  }
197
219
 
198
- if (!this.connecting) {
199
- this.connecting = (async () => {
200
- await this.establishSocket();
201
- const ok = !!(this.socket && !this.socket.destroyed);
202
- if (!ok) {
203
- this.log.warn(
204
- '[Cync TCP] establishSocket() completed but socket is not usable.',
205
- );
206
- }
207
- this.connecting = null;
208
- return ok;
209
- })().catch((err) => {
210
- this.log.error(
211
- '[Cync TCP] ensureConnected() connect attempt failed: %s',
212
- String(err),
213
- );
214
- this.connecting = null;
215
- return false;
216
- });
217
- }
218
-
219
- return this.connecting;
220
+ await this.establishSocket();
221
+ return !!(this.socket && !this.socket.destroyed);
220
222
  }
221
223
 
222
224
  /**
@@ -325,45 +327,6 @@ export class TcpClient {
325
327
  return this.seq;
326
328
  }
327
329
 
328
- private async flushQueue(): Promise<void> {
329
- if (this.sending) {
330
- return;
331
- }
332
- this.sending = true;
333
-
334
- try {
335
- while (this.sendQueue.length > 0) {
336
- const item = this.sendQueue.shift();
337
- if (!item) {
338
- break;
339
- }
340
-
341
- const connected = await this.ensureConnected();
342
- if (!connected || !this.socket || this.socket.destroyed) {
343
- this.log.warn(
344
- '[Cync TCP] Socket not available while flushing queue; dropping remaining %d packets.',
345
- this.sendQueue.length + 1,
346
- );
347
- this.sendQueue.length = 0;
348
- break;
349
- }
350
-
351
- this.socket.write(item.packet);
352
- this.log.info(
353
- '[Cync TCP] Sent power packet: device=%s on=%s seq=%d',
354
- item.deviceId,
355
- String(item.on),
356
- item.seq,
357
- );
358
-
359
- // Small delay so we don't hammer the bridge/device with a burst
360
- await new Promise((resolve) => setTimeout(resolve, 50));
361
- }
362
- } finally {
363
- this.sending = false;
364
- }
365
- }
366
-
367
330
  private buildPowerPacket(
368
331
  controllerId: number,
369
332
  meshId: number,
@@ -414,9 +377,6 @@ export class TcpClient {
414
377
  this.socket.destroy();
415
378
  this.socket = null;
416
379
  }
417
- this.sendQueue.length = 0;
418
- this.sending = false;
419
- this.connecting = null;
420
380
  }
421
381
 
422
382
  public onDeviceUpdate(cb: DeviceUpdateCallback): void {
@@ -442,49 +402,58 @@ export class TcpClient {
442
402
 
443
403
  /**
444
404
  * High-level API to change switch state.
445
- * Now enqueues a power packet so multiple HomeKit scene writes are
446
- * serialized over a single TCP session.
405
+ * Ensures we have a live socket before sending and serializes commands.
447
406
  */
448
407
  public async setSwitchState(
449
408
  deviceId: string,
450
409
  params: { on: boolean },
451
410
  ): Promise<void> {
452
- if (!this.config) {
453
- this.log.warn('[Cync TCP] No config available.');
454
- return;
455
- }
411
+ return this.enqueueCommand(async () => {
412
+ if (!this.config) {
413
+ this.log.warn('[Cync TCP] No config available.');
414
+ return;
415
+ }
456
416
 
457
- const device = this.findDevice(deviceId);
458
- if (!device) {
459
- this.log.warn('[Cync TCP] Unknown deviceId=%s', deviceId);
460
- return;
461
- }
417
+ const connected = await this.ensureConnected();
418
+ if (!connected || !this.socket || this.socket.destroyed) {
419
+ this.log.warn(
420
+ '[Cync TCP] Cannot send, socket not ready even after reconnect attempt.',
421
+ );
422
+ return;
423
+ }
424
+
425
+ const device = this.findDevice(deviceId);
426
+ if (!device) {
427
+ this.log.warn('[Cync TCP] Unknown deviceId=%s', deviceId);
428
+ return;
429
+ }
462
430
 
463
- const controllerId = Number((device as Record<string, unknown>).switch_controller);
464
- const meshIndex = Number((device as Record<string, unknown>).mesh_id);
431
+ const record = device as Record<string, unknown>;
432
+ const controllerId = Number(record.switch_controller);
433
+ const meshIndex = Number(record.mesh_id);
465
434
 
466
- if (!Number.isFinite(controllerId) || !Number.isFinite(meshIndex)) {
467
- this.log.warn(
468
- '[Cync TCP] Device %s is missing LAN fields (switch_controller=%o mesh_id=%o)',
469
- deviceId,
470
- (device as Record<string, unknown>).switch_controller,
471
- (device as Record<string, unknown>).mesh_id,
472
- );
473
- return;
474
- }
435
+ if (!Number.isFinite(controllerId) || !Number.isFinite(meshIndex)) {
436
+ this.log.warn(
437
+ '[Cync TCP] Device %s is missing LAN fields (switch_controller=%o mesh_id=%o)',
438
+ deviceId,
439
+ record.switch_controller,
440
+ record.mesh_id,
441
+ );
442
+ return;
443
+ }
475
444
 
476
- const seq = this.nextSeq();
477
- const packet = this.buildPowerPacket(controllerId, meshIndex, params.on, seq);
445
+ const seq = this.nextSeq();
446
+ const packet = this.buildPowerPacket(controllerId, meshIndex, params.on, seq);
478
447
 
479
- this.sendQueue.push({
480
- deviceId,
481
- on: params.on,
482
- seq,
483
- packet,
448
+ // At this point socket has been validated above
449
+ this.socket.write(packet);
450
+ this.log.info(
451
+ '[Cync TCP] Sent power packet: device=%s on=%s seq=%d',
452
+ deviceId,
453
+ String(params.on),
454
+ seq,
455
+ );
484
456
  });
485
-
486
- // Async fire-and-forget; actual sends are serialized in flushQueue()
487
- void this.flushQueue();
488
457
  }
489
458
 
490
459
  private findDevice(deviceId: string) {
@@ -515,9 +484,7 @@ export class TcpClient {
515
484
 
516
485
  socket.on('close', () => {
517
486
  this.log.warn('[Cync TCP] Socket closed.');
518
- if (this.socket === socket) {
519
- this.socket = null;
520
- }
487
+ this.socket = null;
521
488
  });
522
489
 
523
490
  socket.on('error', (err) => {
@@ -13,17 +13,12 @@ export interface CyncTokenData {
13
13
 
14
14
  /**
15
15
  * Simple JSON token store under the Homebridge storage path.
16
- *
17
- * Files are stored at:
18
- * <storagePath>/homebridge-cync-app/cync-tokens.json
19
16
  */
20
17
  export class CyncTokenStore {
21
- private readonly dirPath: string;
22
18
  private readonly filePath: string;
23
19
 
24
20
  public constructor(storagePath: string) {
25
- this.dirPath = path.join(storagePath, 'homebridge-cync-app');
26
- this.filePath = path.join(this.dirPath, 'cync-tokens.json');
21
+ this.filePath = path.join(storagePath, 'cync-tokens.json');
27
22
  }
28
23
 
29
24
  public async load(): Promise<CyncTokenData | null> {
@@ -38,16 +33,12 @@ export class CyncTokenStore {
38
33
 
39
34
  return data;
40
35
  } catch {
41
- // file missing or unreadable → treat as no token
42
36
  return null;
43
37
  }
44
38
  }
45
39
 
46
40
  public async save(data: CyncTokenData): Promise<void> {
47
41
  const json = JSON.stringify(data, null, 2);
48
-
49
- // Ensure directory exists before writing
50
- await fs.mkdir(this.dirPath, { recursive: true });
51
42
  await fs.writeFile(this.filePath, json, 'utf8');
52
43
  }
53
44
 
@@ -55,7 +46,7 @@ export class CyncTokenStore {
55
46
  try {
56
47
  await fs.unlink(this.filePath);
57
48
  } catch {
58
- // ignore if missing or already removed
49
+ // ignore if missing
59
50
  }
60
51
  }
61
52
  }