homebridge-cync-app 0.0.2 → 0.1.1
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/CHANGELOG.md +9 -0
- package/README.md +7 -4
- package/dist/cync/config-client.d.ts +6 -0
- package/dist/cync/config-client.js +38 -0
- package/dist/cync/config-client.js.map +1 -1
- package/dist/cync/cync-client.d.ts +16 -11
- package/dist/cync/cync-client.js +175 -23
- package/dist/cync/cync-client.js.map +1 -1
- package/dist/cync/tcp-client.d.ts +45 -8
- package/dist/cync/tcp-client.js +359 -17
- package/dist/cync/tcp-client.js.map +1 -1
- package/dist/cync/token-store.d.ts +2 -0
- package/dist/cync/token-store.js.map +1 -1
- package/dist/platform.d.ts +5 -2
- package/dist/platform.js +103 -36
- package/dist/platform.js.map +1 -1
- package/docs/cync-api-notes.md +121 -7
- package/package.json +6 -1
- package/src/cync/config-client.ts +55 -0
- package/src/cync/cync-client.ts +221 -35
- package/src/cync/tcp-client.ts +488 -21
- package/src/cync/token-store.ts +3 -1
- package/src/platform.ts +176 -50
- package/homebridge-cync-app-v0.0.1.zip +0 -0
package/dist/platform.js
CHANGED
|
@@ -22,7 +22,70 @@ export class CyncAppPlatform {
|
|
|
22
22
|
api;
|
|
23
23
|
config;
|
|
24
24
|
client;
|
|
25
|
+
tcpClient;
|
|
25
26
|
cloudConfig = null;
|
|
27
|
+
deviceIdToAccessory = new Map();
|
|
28
|
+
handleLanUpdate(update) {
|
|
29
|
+
// We only care about parsed 0x83 frames that look like:
|
|
30
|
+
// { controllerId: number, on: boolean, level: number, deviceId?: string }
|
|
31
|
+
const payload = update;
|
|
32
|
+
if (!payload || typeof payload.deviceId !== 'string' || typeof payload.on !== 'boolean') {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const accessory = this.deviceIdToAccessory.get(payload.deviceId);
|
|
36
|
+
if (!accessory) {
|
|
37
|
+
this.log.debug('Cync: LAN update for unknown deviceId=%s; no accessory mapping', payload.deviceId);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const service = accessory.getService(this.api.hap.Service.Switch);
|
|
41
|
+
if (!service) {
|
|
42
|
+
this.log.debug('Cync: accessory %s has no Switch service for deviceId=%s', accessory.displayName, payload.deviceId);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
// Update cached context state
|
|
46
|
+
const ctx = accessory.context;
|
|
47
|
+
ctx.cync = ctx.cync ?? {
|
|
48
|
+
meshId: '',
|
|
49
|
+
deviceId: payload.deviceId,
|
|
50
|
+
};
|
|
51
|
+
ctx.cync.on = payload.on;
|
|
52
|
+
this.log.info('Cync: LAN update -> %s is now %s (deviceId=%s)', accessory.displayName, payload.on ? 'ON' : 'OFF', payload.deviceId);
|
|
53
|
+
// Push the new state into HomeKit
|
|
54
|
+
service.updateCharacteristic(this.api.hap.Characteristic.On, payload.on);
|
|
55
|
+
}
|
|
56
|
+
configureCyncSwitchAccessory(mesh, device, accessory, deviceName, deviceId) {
|
|
57
|
+
const service = accessory.getService(this.api.hap.Service.Switch) ||
|
|
58
|
+
accessory.addService(this.api.hap.Service.Switch, deviceName);
|
|
59
|
+
// Ensure context is initialized
|
|
60
|
+
const ctx = accessory.context;
|
|
61
|
+
ctx.cync = ctx.cync ?? {
|
|
62
|
+
meshId: mesh.id,
|
|
63
|
+
deviceId,
|
|
64
|
+
productId: device.product_id,
|
|
65
|
+
on: false,
|
|
66
|
+
};
|
|
67
|
+
// Remember mapping for LAN updates
|
|
68
|
+
this.deviceIdToAccessory.set(deviceId, accessory);
|
|
69
|
+
service
|
|
70
|
+
.getCharacteristic(this.api.hap.Characteristic.On)
|
|
71
|
+
.onGet(() => {
|
|
72
|
+
const currentOn = !!ctx.cync?.on;
|
|
73
|
+
this.log.info('Cync: On.get -> %s for %s (deviceId=%s)', String(currentOn), deviceName, deviceId);
|
|
74
|
+
return currentOn;
|
|
75
|
+
})
|
|
76
|
+
.onSet(async (value) => {
|
|
77
|
+
const cyncMeta = ctx.cync;
|
|
78
|
+
if (!cyncMeta?.deviceId) {
|
|
79
|
+
this.log.warn('Cync: On.set called for %s but no cync.deviceId in context', deviceName);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const on = value === true || value === 1;
|
|
83
|
+
this.log.info('Cync: On.set -> %s for %s (deviceId=%s)', String(on), deviceName, cyncMeta.deviceId);
|
|
84
|
+
// Optimistic local cache; LAN update will confirm
|
|
85
|
+
cyncMeta.on = on;
|
|
86
|
+
await this.tcpClient.setSwitchState(cyncMeta.deviceId, { on });
|
|
87
|
+
});
|
|
88
|
+
}
|
|
26
89
|
constructor(log, config, api) {
|
|
27
90
|
this.log = log;
|
|
28
91
|
this.config = config;
|
|
@@ -32,13 +95,18 @@ export class CyncAppPlatform {
|
|
|
32
95
|
const username = (cfg.username ?? cfg.email);
|
|
33
96
|
const password = cfg.password;
|
|
34
97
|
const twoFactor = cfg.twoFactor;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
this.client = new CyncClient(new ConfigClient(
|
|
98
|
+
const cyncLogger = toCyncLogger(this.log);
|
|
99
|
+
const tcpClient = new TcpClient(cyncLogger);
|
|
100
|
+
this.client = new CyncClient(new ConfigClient(cyncLogger), tcpClient, {
|
|
38
101
|
email: username ?? '',
|
|
39
102
|
password: password ?? '',
|
|
40
103
|
twoFactor,
|
|
41
|
-
}, this.api.user.storagePath(),
|
|
104
|
+
}, this.api.user.storagePath(), cyncLogger);
|
|
105
|
+
this.tcpClient = tcpClient;
|
|
106
|
+
// Bridge LAN updates into Homebridge
|
|
107
|
+
this.client.onLanDeviceUpdate((update) => {
|
|
108
|
+
this.handleLanUpdate(update);
|
|
109
|
+
});
|
|
42
110
|
this.log.info(this.config.name ?? PLATFORM_NAME, 'initialized');
|
|
43
111
|
this.api.on('didFinishLaunching', () => {
|
|
44
112
|
this.log.info(PLATFORM_NAME, 'didFinishLaunching');
|
|
@@ -71,7 +139,24 @@ export class CyncAppPlatform {
|
|
|
71
139
|
}
|
|
72
140
|
const cloudConfig = await this.client.loadConfiguration();
|
|
73
141
|
this.cloudConfig = cloudConfig;
|
|
74
|
-
this.log.info('Cync: cloud configuration loaded; mesh count=%d', cloudConfig
|
|
142
|
+
this.log.info('Cync: cloud configuration loaded; mesh count=%d', cloudConfig.meshes.length);
|
|
143
|
+
// Ask the CyncClient for the LAN login code derived from stored session.
|
|
144
|
+
// If it returns an empty blob, LAN is disabled but cloud still works.
|
|
145
|
+
let loginCode = new Uint8Array();
|
|
146
|
+
try {
|
|
147
|
+
loginCode = this.client.getLanLoginCode();
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
this.log.warn('Cync: getLanLoginCode() failed: %s', err.message ?? String(err));
|
|
151
|
+
}
|
|
152
|
+
if (loginCode.length > 0) {
|
|
153
|
+
this.log.info('Cync: LAN login code available (%d bytes); starting TCP transport…', loginCode.length);
|
|
154
|
+
// ### 🧩 LAN Transport Bootstrap: wire frame listeners via CyncClient
|
|
155
|
+
await this.client.startTransport(cloudConfig, loginCode);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
this.log.info('Cync: LAN login code unavailable; TCP control disabled (cloud-only).');
|
|
159
|
+
}
|
|
75
160
|
this.discoverDevices(cloudConfig);
|
|
76
161
|
}
|
|
77
162
|
catch (err) {
|
|
@@ -80,8 +165,7 @@ export class CyncAppPlatform {
|
|
|
80
165
|
}
|
|
81
166
|
/**
|
|
82
167
|
* Discover devices from the Cync cloud config and register them as
|
|
83
|
-
* Homebridge accessories.
|
|
84
|
-
* dummy Switch that logs state changes.
|
|
168
|
+
* Homebridge accessories.
|
|
85
169
|
*/
|
|
86
170
|
discoverDevices(cloudConfig) {
|
|
87
171
|
if (!cloudConfig.meshes?.length) {
|
|
@@ -97,45 +181,28 @@ export class CyncAppPlatform {
|
|
|
97
181
|
continue;
|
|
98
182
|
}
|
|
99
183
|
for (const device of devices) {
|
|
100
|
-
const deviceId =
|
|
101
|
-
device.
|
|
184
|
+
const deviceId = device.device_id ??
|
|
185
|
+
device.id ??
|
|
102
186
|
device.mac ??
|
|
103
187
|
device.sn ??
|
|
104
|
-
`${mesh.id}-${device.product_id ?? 'unknown'}
|
|
188
|
+
`${mesh.id}-${device.product_id ?? 'unknown'}`;
|
|
105
189
|
const preferredName = device.name ??
|
|
106
190
|
device.displayName ??
|
|
107
191
|
undefined;
|
|
108
192
|
const deviceName = preferredName || `Cync Device ${deviceId}`;
|
|
109
193
|
const uuidSeed = `cync-${mesh.id}-${deviceId}`;
|
|
110
194
|
const uuid = this.api.hap.uuid.generate(uuidSeed);
|
|
111
|
-
|
|
112
|
-
if (
|
|
195
|
+
let accessory = this.accessories.find(acc => acc.UUID === uuid);
|
|
196
|
+
if (accessory) {
|
|
113
197
|
this.log.info('Cync: using cached accessory for %s (%s)', deviceName, uuidSeed);
|
|
114
|
-
continue;
|
|
115
198
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
.onGet(() => {
|
|
124
|
-
this.log.info('Cync: On.get -> false for %s', deviceName);
|
|
125
|
-
return false;
|
|
126
|
-
})
|
|
127
|
-
.onSet((value) => {
|
|
128
|
-
this.log.info('Cync: On.set -> %s for %s', String(value), deviceName);
|
|
129
|
-
});
|
|
130
|
-
// Context for later TCP control
|
|
131
|
-
const ctx = accessory.context;
|
|
132
|
-
ctx.cync = {
|
|
133
|
-
meshId: mesh.id,
|
|
134
|
-
deviceId,
|
|
135
|
-
productId: device.product_id,
|
|
136
|
-
};
|
|
137
|
-
this.api.registerPlatformAccessories('homebridge-cync-app', 'CyncAppPlatform', [accessory]);
|
|
138
|
-
this.accessories.push(accessory);
|
|
199
|
+
else {
|
|
200
|
+
this.log.info('Cync: registering new accessory for %s (%s)', deviceName, uuidSeed);
|
|
201
|
+
accessory = new this.api.platformAccessory(deviceName, uuid);
|
|
202
|
+
this.api.registerPlatformAccessories('homebridge-cync-app', 'CyncAppPlatform', [accessory]);
|
|
203
|
+
this.accessories.push(accessory);
|
|
204
|
+
}
|
|
205
|
+
this.configureCyncSwitchAccessory(mesh, device, accessory, deviceName, deviceId);
|
|
139
206
|
}
|
|
140
207
|
}
|
|
141
208
|
}
|
package/dist/platform.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"platform.js","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAEvD,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAGjD,MAAM,YAAY,GAAG,CAAC,GAAW,EAAc,EAAE,CAAC,CAAC;IAClD,KAAK,EAAE,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC;IAC1B,IAAI,EAAE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;IACxB,IAAI,EAAE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;IACxB,KAAK,EAAE,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC;CAC1B,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"platform.js","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"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"}
|
package/docs/cync-api-notes.md
CHANGED
|
@@ -77,6 +77,19 @@ If successful, a verification code is emailed to the user.
|
|
|
77
77
|
|
|
78
78
|
On success, the response includes the same fields as primary authentication and allows creation of the TCP login code.
|
|
79
79
|
|
|
80
|
+
### **2.4 Token Persistence (Homebridge Plugin Behavior)**
|
|
81
|
+
|
|
82
|
+
After successful 2FA login, the Homebridge plugin stores:
|
|
83
|
+
|
|
84
|
+
- `access_token`
|
|
85
|
+
- `user_id`
|
|
86
|
+
- optional `refreshToken`
|
|
87
|
+
- optional `expiresAt`
|
|
88
|
+
|
|
89
|
+
Tokens are restored on startup via a local `cync-token.json` file.
|
|
90
|
+
The Cync API often omits `expiresAt`, so the plugin must treat tokens as
|
|
91
|
+
semi-permanent until an API error indicates otherwise.
|
|
92
|
+
|
|
80
93
|
---
|
|
81
94
|
|
|
82
95
|
## **3. Cloud Configuration Retrieval**
|
|
@@ -94,19 +107,93 @@ Each home entry includes:
|
|
|
94
107
|
* `product_id`
|
|
95
108
|
* `name`
|
|
96
109
|
|
|
97
|
-
|
|
110
|
+
#### Actual Response Structure (Observed)
|
|
98
111
|
|
|
99
|
-
|
|
112
|
+
Live queries show that `/subscribe/devices` returns **mesh-level objects**, *not*
|
|
113
|
+
device lists. These objects include metadata fields but no per-device entries.
|
|
100
114
|
|
|
101
|
-
|
|
115
|
+
Discovered keys:
|
|
102
116
|
|
|
103
|
-
|
|
104
|
-
|
|
117
|
+
- id
|
|
118
|
+
- name
|
|
119
|
+
- mac
|
|
120
|
+
- product_id
|
|
121
|
+
- access_key
|
|
122
|
+
- authorize_code
|
|
123
|
+
- role
|
|
124
|
+
- source
|
|
125
|
+
- is_active
|
|
126
|
+
- is_online
|
|
127
|
+
- subscribe_date
|
|
128
|
+
- active_date
|
|
129
|
+
- last_login
|
|
130
|
+
- mcu_version
|
|
131
|
+
- firmware_version
|
|
132
|
+
- groups
|
|
105
133
|
|
|
106
|
-
|
|
134
|
+
⚠️ **Important:**
|
|
135
|
+
These meshes do *not* contain devices. Device lists are obtained from
|
|
136
|
+
`/product/<product_id>/device/<mesh_id>/property` (see Section 3.2).
|
|
107
137
|
|
|
108
|
-
|
|
138
|
+
### **3.2 Retrieve Devices Within a Mesh**
|
|
139
|
+
|
|
140
|
+
`GET /v2/product/<product_id>/device/<mesh_id>/property`
|
|
141
|
+
|
|
142
|
+
This endpoint returns the **actual list of devices** in a mesh.
|
|
143
|
+
The top-level object includes fields such as:
|
|
144
|
+
|
|
145
|
+
- `groupsArray`
|
|
146
|
+
- `sceneArray`
|
|
147
|
+
- `schedules`
|
|
148
|
+
- `system`
|
|
149
|
+
- `deviceIdRecord`
|
|
150
|
+
- `ftsModel`
|
|
151
|
+
- `bulbsArray` (primary device list)
|
|
152
|
+
|
|
153
|
+
#### **Device Structure (from bulbsArray)**
|
|
154
|
+
|
|
155
|
+
Each entry in `bulbsArray` represents a real Cync device such as a plug, switch,
|
|
156
|
+
bulb, or sensor.
|
|
157
|
+
|
|
158
|
+
Observed device keys:
|
|
159
|
+
|
|
160
|
+
- `displayName`
|
|
161
|
+
- `deviceID`
|
|
162
|
+
- `wifiMac`
|
|
163
|
+
- `deviceType`
|
|
164
|
+
- `firmwareVersion`
|
|
165
|
+
- `fadeOn`, `fadeOff`
|
|
166
|
+
- `defaultBrightness`
|
|
167
|
+
- `ambientLightEnable`
|
|
168
|
+
- `ambientLightSensitivity`
|
|
169
|
+
- `occupancyEnable`
|
|
170
|
+
- `occupancyTimeoutPeriod`
|
|
171
|
+
- `followTheSun`
|
|
172
|
+
- `simpleModeEnabled`
|
|
173
|
+
- `wifiSsid`
|
|
174
|
+
- `wifiDisconnectIndicatorEnable`
|
|
175
|
+
|
|
176
|
+
These fields provide enough metadata for:
|
|
177
|
+
|
|
178
|
+
- Naming the accessory
|
|
179
|
+
- Generating stable UUIDs in Homebridge
|
|
180
|
+
- Determining capabilities (switch vs dimmer vs sensor)
|
|
181
|
+
|
|
182
|
+
### **3.3 Discovery Model Summary**
|
|
109
183
|
|
|
184
|
+
Cync cloud discovery requires combining two endpoints:
|
|
185
|
+
|
|
186
|
+
1. `/subscribe/devices`
|
|
187
|
+
Provides *mesh-level* information (homes/networks) but no per-device data.
|
|
188
|
+
|
|
189
|
+
2. `/product/<product_id>/device/<mesh_id>/property`
|
|
190
|
+
Provides the **actual device list** via `bulbsArray`.
|
|
191
|
+
|
|
192
|
+
Because of this structure:
|
|
193
|
+
|
|
194
|
+
- A mesh may appear empty in `/subscribe/devices` but still contain devices.
|
|
195
|
+
- Device names and IDs must be extracted solely from `bulbsArray`.
|
|
196
|
+
---
|
|
110
197
|
## **4. Configuration Assembly Process**
|
|
111
198
|
|
|
112
199
|
The integration processes cloud data into the following components:
|
|
@@ -122,6 +209,22 @@ The integration processes cloud data into the following components:
|
|
|
122
209
|
* `switchID_to_homeID`:
|
|
123
210
|
Reverse mapping from controller identifier → home identifier.
|
|
124
211
|
|
|
212
|
+
#### **UUID Strategy (Homebridge Implementation)**
|
|
213
|
+
|
|
214
|
+
To ensure stable and deterministic accessory identities, device UUIDs are
|
|
215
|
+
generated using:
|
|
216
|
+
`cync-${mesh.id}-${deviceID}`
|
|
217
|
+
|
|
218
|
+
Where:
|
|
219
|
+
|
|
220
|
+
- `deviceID` is taken from `deviceID`
|
|
221
|
+
- If missing, fallback is `wifiMac` (colons removed)
|
|
222
|
+
- Final fallback: `${mesh.id}-${product_id}`
|
|
223
|
+
|
|
224
|
+
This ensures:
|
|
225
|
+
- Accessories do not duplicate on restart
|
|
226
|
+
- Renaming devices in the Cync app does not break HomeKit bindings
|
|
227
|
+
|
|
125
228
|
### **4.2 Device Records**
|
|
126
229
|
|
|
127
230
|
Each device is assigned a normalized structure containing:
|
|
@@ -165,4 +268,15 @@ The upstream integration returns the following final structure:
|
|
|
165
268
|
|
|
166
269
|
This dataset defines the full set of controllable Cync devices and establishes the mesh addressing required for TCP commands.
|
|
167
270
|
|
|
271
|
+
### **TCP LAN Control (Not yet implemented)**
|
|
272
|
+
|
|
273
|
+
Cync cloud discovery provides:
|
|
274
|
+
|
|
275
|
+
- product_id
|
|
276
|
+
- access_key
|
|
277
|
+
- meshId
|
|
278
|
+
|
|
279
|
+
These values will be used to establish a LAN session using:
|
|
280
|
+
|
|
281
|
+
`/tcp-client.connect(loginCode, config)`
|
|
168
282
|
|
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.
|
|
5
|
+
"version": "0.1.1",
|
|
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,12 +19,17 @@
|
|
|
19
19
|
"homebridge-plugin",
|
|
20
20
|
"homebridge",
|
|
21
21
|
"cync",
|
|
22
|
+
"ge",
|
|
22
23
|
"ge cync",
|
|
24
|
+
"ge-lighting",
|
|
23
25
|
"smart plug",
|
|
24
26
|
"smart lights",
|
|
25
27
|
"C by GE"
|
|
26
28
|
],
|
|
27
29
|
"main": "dist/index.js",
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
28
33
|
"homebridge": {
|
|
29
34
|
"pluginType": "platform",
|
|
30
35
|
"platform": "CyncAppPlatform"
|
|
@@ -32,6 +32,7 @@ type CyncErrorBody = {
|
|
|
32
32
|
export interface CyncLoginSession {
|
|
33
33
|
accessToken: string;
|
|
34
34
|
userId: string;
|
|
35
|
+
authorize?: string;
|
|
35
36
|
raw: unknown;
|
|
36
37
|
}
|
|
37
38
|
|
|
@@ -42,6 +43,10 @@ export interface CyncDevice {
|
|
|
42
43
|
device_id?: string;
|
|
43
44
|
mac?: string;
|
|
44
45
|
sn?: string;
|
|
46
|
+
switch_id?: string;
|
|
47
|
+
switch_controller?: number | string;
|
|
48
|
+
mesh_id?: number | string;
|
|
49
|
+
|
|
45
50
|
[key: string]: unknown;
|
|
46
51
|
}
|
|
47
52
|
|
|
@@ -84,6 +89,7 @@ export class ConfigClient {
|
|
|
84
89
|
// These are populated after a successful 2FA login.
|
|
85
90
|
private accessToken: string | null = null;
|
|
86
91
|
private userId: string | null = null;
|
|
92
|
+
private authorize: string | null = null;
|
|
87
93
|
|
|
88
94
|
constructor(logger?: CyncLogger) {
|
|
89
95
|
this.log = logger ?? defaultLogger;
|
|
@@ -182,6 +188,7 @@ export class ConfigClient {
|
|
|
182
188
|
// Accept both snake_case and camelCase, and both string/number user_id.
|
|
183
189
|
const accessTokenRaw = obj.access_token ?? obj.accessToken;
|
|
184
190
|
const userIdRaw = obj.user_id ?? obj.userId;
|
|
191
|
+
const authorizeRaw = obj.authorize;
|
|
185
192
|
|
|
186
193
|
const accessToken =
|
|
187
194
|
typeof accessTokenRaw === 'string' && accessTokenRaw.length > 0
|
|
@@ -193,6 +200,11 @@ export class ConfigClient {
|
|
|
193
200
|
? String(userIdRaw)
|
|
194
201
|
: undefined;
|
|
195
202
|
|
|
203
|
+
const authorize =
|
|
204
|
+
typeof authorizeRaw === 'string' && authorizeRaw.length > 0
|
|
205
|
+
? authorizeRaw
|
|
206
|
+
: undefined;
|
|
207
|
+
|
|
196
208
|
if (!accessToken || !userId) {
|
|
197
209
|
this.log.error('Cync login missing access_token or user_id: %o', json);
|
|
198
210
|
throw new Error('Cync login response missing access_token or user_id');
|
|
@@ -200,12 +212,14 @@ export class ConfigClient {
|
|
|
200
212
|
|
|
201
213
|
this.accessToken = accessToken;
|
|
202
214
|
this.userId = userId;
|
|
215
|
+
this.authorize = authorize ?? null;
|
|
203
216
|
|
|
204
217
|
this.log.info('Cync login successful; userId=%s', userId);
|
|
205
218
|
|
|
206
219
|
return {
|
|
207
220
|
accessToken,
|
|
208
221
|
userId,
|
|
222
|
+
authorize,
|
|
209
223
|
raw: json,
|
|
210
224
|
};
|
|
211
225
|
}
|
|
@@ -358,6 +372,47 @@ export class ConfigClient {
|
|
|
358
372
|
throw new Error('Cync session not initialised. Call loginWithTwoFactor() first.');
|
|
359
373
|
}
|
|
360
374
|
}
|
|
375
|
+
// ### 🧩 LAN Login Blob Builder: Generates the auth_code payload used by Cync LAN TCP
|
|
376
|
+
public static buildLanLoginCode(userId: string, authorize: string): Uint8Array {
|
|
377
|
+
const authBytes = Buffer.from(authorize, 'ascii');
|
|
378
|
+
const lengthByte = 10 + authBytes.length;
|
|
379
|
+
|
|
380
|
+
if (lengthByte > 0xff) {
|
|
381
|
+
throw new Error('Cync LAN authorize token too long to encode.');
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const userIdNum = Number.parseInt(userId, 10);
|
|
385
|
+
if (!Number.isFinite(userIdNum) || userIdNum < 0) {
|
|
386
|
+
throw new Error(`Invalid Cync userId for LAN auth: ${userId}`);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const header = Buffer.from('13000000', 'hex');
|
|
390
|
+
|
|
391
|
+
const lenBuf = Buffer.alloc(1);
|
|
392
|
+
lenBuf.writeUInt8(lengthByte & 0xff, 0);
|
|
393
|
+
|
|
394
|
+
const cmdBuf = Buffer.from('03', 'hex');
|
|
395
|
+
|
|
396
|
+
const userIdBuf = Buffer.alloc(4);
|
|
397
|
+
userIdBuf.writeUInt32BE(userIdNum >>> 0, 0);
|
|
398
|
+
|
|
399
|
+
const authLenBuf = Buffer.alloc(2);
|
|
400
|
+
authLenBuf.writeUInt16BE(authBytes.length, 0);
|
|
401
|
+
|
|
402
|
+
const tail = Buffer.from('0000b4', 'hex');
|
|
403
|
+
|
|
404
|
+
const loginBuf = Buffer.concat([
|
|
405
|
+
header,
|
|
406
|
+
lenBuf,
|
|
407
|
+
cmdBuf,
|
|
408
|
+
userIdBuf,
|
|
409
|
+
authLenBuf,
|
|
410
|
+
authBytes,
|
|
411
|
+
tail,
|
|
412
|
+
]);
|
|
413
|
+
|
|
414
|
+
return new Uint8Array(loginBuf);
|
|
415
|
+
}
|
|
361
416
|
|
|
362
417
|
private static randomLoginResource(): string {
|
|
363
418
|
const chars = 'abcdefghijklmnopqrstuvwxyz';
|