@mega-yfue/eufy-sdk 0.4.0-beta.1 → 0.4.0-beta.11
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/dist/client/eufy-mega.d.ts +4 -1
- package/dist/core/contracts.d.ts +2 -2
- package/dist/index.js +638 -575
- package/dist/index.js.map +4 -4
- package/dist/transport/http/passport.d.ts +21 -0
- package/dist/transport/http/solix-client.d.ts +5 -6
- package/dist/transport/mqtt/secure-mqtt.d.ts +20 -13
- package/dist/transport/p2p/p2p-session.d.ts +21 -2
- package/dist/transport/p2p/shared-live-source.d.ts +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1070,6 +1070,401 @@ function normalizePushImage(data, p2pDid) {
|
|
|
1070
1070
|
return decodeImageV1(data, p2pDid) ?? data;
|
|
1071
1071
|
}
|
|
1072
1072
|
|
|
1073
|
+
// dist/core/types.js
|
|
1074
|
+
var PRINTER_CATEGORY_RE = /ankermake|eufymake|fdm|3d_?print/i;
|
|
1075
|
+
function classifyDevice(raw) {
|
|
1076
|
+
const category = raw.category ?? "";
|
|
1077
|
+
const hasP2p = typeof raw.p2p_did === "string" && raw.p2p_did.length > 0;
|
|
1078
|
+
if (category === "eufy_security" || hasP2p)
|
|
1079
|
+
return { category, api: "mega", realtime: "p2p" };
|
|
1080
|
+
if (PRINTER_CATEGORY_RE.test(category))
|
|
1081
|
+
return { category, api: "ankermake", realtime: "ankermake-mqtt" };
|
|
1082
|
+
return { category, api: "mega", realtime: "smqtt" };
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// dist/core/contracts.js
|
|
1086
|
+
var StoredSnapshotUnavailableError = class extends Error {
|
|
1087
|
+
reason;
|
|
1088
|
+
constructor(reason, message, options) {
|
|
1089
|
+
super(message, options);
|
|
1090
|
+
this.reason = reason;
|
|
1091
|
+
this.name = "StoredSnapshotUnavailableError";
|
|
1092
|
+
}
|
|
1093
|
+
};
|
|
1094
|
+
var RETRYABLE_LIVE_SNAPSHOT_REASONS = [
|
|
1095
|
+
"no-keyframe",
|
|
1096
|
+
"source-failed",
|
|
1097
|
+
"undecodable-burst"
|
|
1098
|
+
];
|
|
1099
|
+
var LiveSnapshotUnavailableError = class extends Error {
|
|
1100
|
+
reason;
|
|
1101
|
+
/** Whether another attempt could plausibly succeed without the host changing anything. */
|
|
1102
|
+
retryable;
|
|
1103
|
+
constructor(reason, message, options) {
|
|
1104
|
+
super(message, options);
|
|
1105
|
+
this.reason = reason;
|
|
1106
|
+
this.name = "LiveSnapshotUnavailableError";
|
|
1107
|
+
this.retryable = RETRYABLE_LIVE_SNAPSHOT_REASONS.includes(reason);
|
|
1108
|
+
}
|
|
1109
|
+
};
|
|
1110
|
+
var CameraDisabledError = class extends Error {
|
|
1111
|
+
camera;
|
|
1112
|
+
constructor(camera, options) {
|
|
1113
|
+
super(`camera${camera ? ` ${camera}` : ""} is disabled \u2014 no live stream possible`, options);
|
|
1114
|
+
this.camera = camera;
|
|
1115
|
+
this.name = "CameraDisabledError";
|
|
1116
|
+
}
|
|
1117
|
+
};
|
|
1118
|
+
var StationKeyUnavailableError = class extends Error {
|
|
1119
|
+
stationSn;
|
|
1120
|
+
/** Always true: the negotiation is per connection, so a later one may still produce a key. */
|
|
1121
|
+
retryable = true;
|
|
1122
|
+
constructor(stationSn, options) {
|
|
1123
|
+
super(`station ${stationSn} did not provide its session key, so nothing that requires one could be sent`, options);
|
|
1124
|
+
this.stationSn = stationSn;
|
|
1125
|
+
this.name = "StationKeyUnavailableError";
|
|
1126
|
+
}
|
|
1127
|
+
};
|
|
1128
|
+
var StationUnreachableError = class extends Error {
|
|
1129
|
+
stationSn;
|
|
1130
|
+
waitedMs;
|
|
1131
|
+
/** Always true: a station unreachable now may answer on a later attempt. */
|
|
1132
|
+
retryable = true;
|
|
1133
|
+
constructor(stationSn, waitedMs, options) {
|
|
1134
|
+
super(`station ${stationSn}'s P2P session did not connect within ${waitedMs}ms, so nothing could be sent to it`, options);
|
|
1135
|
+
this.stationSn = stationSn;
|
|
1136
|
+
this.waitedMs = waitedMs;
|
|
1137
|
+
this.name = "StationUnreachableError";
|
|
1138
|
+
}
|
|
1139
|
+
};
|
|
1140
|
+
var DeviceChannelUnresolvedError = class extends Error {
|
|
1141
|
+
sn;
|
|
1142
|
+
stationSn;
|
|
1143
|
+
constructor(sn, stationSn, options) {
|
|
1144
|
+
super(`${sn} has no usable channel on station ${stationSn}, so nothing was sent to it`, options);
|
|
1145
|
+
this.sn = sn;
|
|
1146
|
+
this.stationSn = stationSn;
|
|
1147
|
+
this.name = "DeviceChannelUnresolvedError";
|
|
1148
|
+
}
|
|
1149
|
+
};
|
|
1150
|
+
var LiveStreamStartError = class extends Error {
|
|
1151
|
+
reason;
|
|
1152
|
+
stage;
|
|
1153
|
+
timeoutMs;
|
|
1154
|
+
attempts;
|
|
1155
|
+
constructor(failure) {
|
|
1156
|
+
super(`live stream failed to start (${failure.reason} at ${failure.stage} after ${failure.attempts} attempts; ${failure.timeoutMs}ms deadline)`, failure.cause === void 0 ? void 0 : { cause: failure.cause });
|
|
1157
|
+
this.name = "LiveStreamStartError";
|
|
1158
|
+
this.reason = failure.reason;
|
|
1159
|
+
this.stage = failure.stage;
|
|
1160
|
+
this.timeoutMs = failure.timeoutMs;
|
|
1161
|
+
this.attempts = failure.attempts;
|
|
1162
|
+
}
|
|
1163
|
+
};
|
|
1164
|
+
var StateConvergenceError = class extends Error {
|
|
1165
|
+
sn;
|
|
1166
|
+
property;
|
|
1167
|
+
param;
|
|
1168
|
+
expected;
|
|
1169
|
+
observed;
|
|
1170
|
+
timeoutMs;
|
|
1171
|
+
constructor(failure) {
|
|
1172
|
+
super(`device ${failure.sn} did not report ${failure.property} as ${String(failure.expected)} within ${failure.timeoutMs}ms (param ${failure.param} read ${String(failure.observed)})`);
|
|
1173
|
+
this.name = "StateConvergenceError";
|
|
1174
|
+
this.sn = failure.sn;
|
|
1175
|
+
this.property = failure.property;
|
|
1176
|
+
this.param = failure.param;
|
|
1177
|
+
this.expected = failure.expected;
|
|
1178
|
+
this.observed = failure.observed;
|
|
1179
|
+
this.timeoutMs = failure.timeoutMs;
|
|
1180
|
+
}
|
|
1181
|
+
};
|
|
1182
|
+
var COMMAND_OBSERVATION = /* @__PURE__ */ Symbol("command-observation");
|
|
1183
|
+
function observeCommand(command, observation) {
|
|
1184
|
+
return Object.defineProperty(command, COMMAND_OBSERVATION, { configurable: true, value: observation });
|
|
1185
|
+
}
|
|
1186
|
+
function commandObservation(command) {
|
|
1187
|
+
return command[COMMAND_OBSERVATION];
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
// dist/core/raw-dp-writer.js
|
|
1191
|
+
var WIRE = { VARINT: 0, LENGTH_DELIMITED: 2 };
|
|
1192
|
+
function encodeVarint(value) {
|
|
1193
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
1194
|
+
throw new Error(`varint: ${value} is not a non-negative safe integer`);
|
|
1195
|
+
}
|
|
1196
|
+
const out = [];
|
|
1197
|
+
let v = value;
|
|
1198
|
+
while (v > 127) {
|
|
1199
|
+
out.push(v % 128 + 128);
|
|
1200
|
+
v = Math.floor(v / 128);
|
|
1201
|
+
}
|
|
1202
|
+
out.push(v);
|
|
1203
|
+
return out;
|
|
1204
|
+
}
|
|
1205
|
+
function zigzag(value) {
|
|
1206
|
+
if (!Number.isSafeInteger(value))
|
|
1207
|
+
throw new Error(`zigzag: ${value} is not a safe integer`);
|
|
1208
|
+
return value < 0 ? -2 * value - 1 : 2 * value;
|
|
1209
|
+
}
|
|
1210
|
+
var RawDpWriter = class _RawDpWriter {
|
|
1211
|
+
body = [];
|
|
1212
|
+
/** A varint-valued field (`int32`, `uint32`, `bool`, an enum member). */
|
|
1213
|
+
int(field, value) {
|
|
1214
|
+
this.body.push(...encodeVarint(tag(field, WIRE.VARINT)), ...encodeVarint(value));
|
|
1215
|
+
return this;
|
|
1216
|
+
}
|
|
1217
|
+
/** A signed varint field (`sint32`, `sint64`), ZigZag-encoded — see {@link zigzag}. */
|
|
1218
|
+
sint(field, value) {
|
|
1219
|
+
return this.int(field, zigzag(value));
|
|
1220
|
+
}
|
|
1221
|
+
/** A boolean field, on protobuf's own terms: `1` or `0` in a varint. */
|
|
1222
|
+
bool(field, value) {
|
|
1223
|
+
return this.int(field, value ? 1 : 0);
|
|
1224
|
+
}
|
|
1225
|
+
/** A length-delimited field carrying raw bytes — a string, a blob, or a message built elsewhere. */
|
|
1226
|
+
bytes(field, value) {
|
|
1227
|
+
const bytes2 = value instanceof Uint8Array ? Array.from(value) : [...value];
|
|
1228
|
+
this.body.push(...encodeVarint(tag(field, WIRE.LENGTH_DELIMITED)), ...encodeVarint(bytes2.length), ...bytes2);
|
|
1229
|
+
return this;
|
|
1230
|
+
}
|
|
1231
|
+
/**
|
|
1232
|
+
* A nested sub-message, built by `build` into a writer of its own.
|
|
1233
|
+
*
|
|
1234
|
+
* The sub-message is emitted even when `build` writes nothing into it. That is not an oversight: an
|
|
1235
|
+
* EMPTY sub-message is how these protocols say "this subsystem exists and is in its zero state", as
|
|
1236
|
+
* opposed to saying nothing about it, and the readers on the other side of this file depend on the
|
|
1237
|
+
* distinction. Omitting the call is what says nothing about it.
|
|
1238
|
+
*/
|
|
1239
|
+
sub(field, build) {
|
|
1240
|
+
const inner = new _RawDpWriter();
|
|
1241
|
+
build(inner);
|
|
1242
|
+
return this.bytes(field, inner.toBytes());
|
|
1243
|
+
}
|
|
1244
|
+
/** The message body on its own, unframed — for nesting into another writer. */
|
|
1245
|
+
toBytes() {
|
|
1246
|
+
return [...this.body];
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* The finished DP value: `varint(bodyLength) ++ body`, base64-encoded.
|
|
1250
|
+
*
|
|
1251
|
+
* This is the envelope `RawDpCodec.decode` unwraps, so a value produced here and read back through the
|
|
1252
|
+
* codec is a genuine round-trip rather than two implementations of the same guess.
|
|
1253
|
+
*/
|
|
1254
|
+
finish() {
|
|
1255
|
+
return Buffer.from([...encodeVarint(this.body.length), ...this.body]).toString("base64");
|
|
1256
|
+
}
|
|
1257
|
+
};
|
|
1258
|
+
function tag(field, wire) {
|
|
1259
|
+
if (!Number.isSafeInteger(field) || field < 1)
|
|
1260
|
+
throw new Error(`field number: ${field} is not a positive integer`);
|
|
1261
|
+
return field * 8 + wire;
|
|
1262
|
+
}
|
|
1263
|
+
function rawDp(build) {
|
|
1264
|
+
const w = new RawDpWriter();
|
|
1265
|
+
build(w);
|
|
1266
|
+
return w.finish();
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// dist/core/raw-dp-hex.js
|
|
1270
|
+
function hexToRawDp(hex) {
|
|
1271
|
+
if (hex.length === 0 || hex.length % 2 !== 0 || !/^[0-9a-fA-F]+$/.test(hex))
|
|
1272
|
+
return void 0;
|
|
1273
|
+
return Buffer.from(hex, "hex").toString("base64");
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
// dist/core/util.js
|
|
1277
|
+
var Timer = class {
|
|
1278
|
+
handle;
|
|
1279
|
+
/** (Re)arm to run `fn` after `ms`, cancelling any already-pending fire. */
|
|
1280
|
+
arm(ms, fn) {
|
|
1281
|
+
this.cancel();
|
|
1282
|
+
this.handle = setTimeout(() => {
|
|
1283
|
+
this.handle = void 0;
|
|
1284
|
+
fn();
|
|
1285
|
+
}, ms);
|
|
1286
|
+
this.handle.unref?.();
|
|
1287
|
+
}
|
|
1288
|
+
/** Cancel a pending fire. No-op if not armed. */
|
|
1289
|
+
cancel() {
|
|
1290
|
+
if (this.handle) {
|
|
1291
|
+
clearTimeout(this.handle);
|
|
1292
|
+
this.handle = void 0;
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
/** Whether a fire is currently scheduled. */
|
|
1296
|
+
get pending() {
|
|
1297
|
+
return this.handle !== void 0;
|
|
1298
|
+
}
|
|
1299
|
+
};
|
|
1300
|
+
function assertNever(value) {
|
|
1301
|
+
throw new Error(`unhandled variant: ${JSON.stringify(value)}`);
|
|
1302
|
+
}
|
|
1303
|
+
function asBool(value) {
|
|
1304
|
+
if (typeof value === "string")
|
|
1305
|
+
return value === "1" || value.toLowerCase() === "true";
|
|
1306
|
+
return value === true || value === 1;
|
|
1307
|
+
}
|
|
1308
|
+
function clamp(value, min, max) {
|
|
1309
|
+
if (Number.isNaN(value))
|
|
1310
|
+
return min;
|
|
1311
|
+
return Math.max(min, Math.min(max, Math.round(value)));
|
|
1312
|
+
}
|
|
1313
|
+
function structuralEqual(a, b) {
|
|
1314
|
+
if (Object.is(a, b))
|
|
1315
|
+
return true;
|
|
1316
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null)
|
|
1317
|
+
return false;
|
|
1318
|
+
const aArr = Array.isArray(a);
|
|
1319
|
+
const bArr = Array.isArray(b);
|
|
1320
|
+
if (aArr !== bArr)
|
|
1321
|
+
return false;
|
|
1322
|
+
if (aArr && bArr) {
|
|
1323
|
+
if (a.length !== b.length)
|
|
1324
|
+
return false;
|
|
1325
|
+
for (let i = 0; i < a.length; i++)
|
|
1326
|
+
if (!structuralEqual(a[i], b[i]))
|
|
1327
|
+
return false;
|
|
1328
|
+
return true;
|
|
1329
|
+
}
|
|
1330
|
+
const aObj = a;
|
|
1331
|
+
const bObj = b;
|
|
1332
|
+
const aKeys = Object.keys(aObj);
|
|
1333
|
+
const bKeys = Object.keys(bObj);
|
|
1334
|
+
if (aKeys.length !== bKeys.length)
|
|
1335
|
+
return false;
|
|
1336
|
+
for (const k of aKeys) {
|
|
1337
|
+
if (!Object.prototype.hasOwnProperty.call(bObj, k))
|
|
1338
|
+
return false;
|
|
1339
|
+
if (!structuralEqual(aObj[k], bObj[k]))
|
|
1340
|
+
return false;
|
|
1341
|
+
}
|
|
1342
|
+
return true;
|
|
1343
|
+
}
|
|
1344
|
+
function coerceEnumValue(enumObj, value) {
|
|
1345
|
+
const n = Number(value);
|
|
1346
|
+
return Number.isInteger(n) && Object.values(enumObj).includes(n) ? n : void 0;
|
|
1347
|
+
}
|
|
1348
|
+
function enumValues(enumObj) {
|
|
1349
|
+
return Object.values(enumObj).join("/");
|
|
1350
|
+
}
|
|
1351
|
+
function enumLabels(enumObj) {
|
|
1352
|
+
return Object.fromEntries(Object.entries(enumObj).map(([name, value]) => [value, name]));
|
|
1353
|
+
}
|
|
1354
|
+
function jsonObject(text2) {
|
|
1355
|
+
if (typeof text2 !== "string")
|
|
1356
|
+
return void 0;
|
|
1357
|
+
try {
|
|
1358
|
+
const v = JSON.parse(text2);
|
|
1359
|
+
return v && typeof v === "object" && !Array.isArray(v) ? v : void 0;
|
|
1360
|
+
} catch {
|
|
1361
|
+
return void 0;
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
function u16le(n) {
|
|
1365
|
+
const b = Buffer.alloc(2);
|
|
1366
|
+
b.writeUInt16LE(n & 65535);
|
|
1367
|
+
return b;
|
|
1368
|
+
}
|
|
1369
|
+
function u16be(n) {
|
|
1370
|
+
const b = Buffer.alloc(2);
|
|
1371
|
+
b.writeUInt16BE(n & 65535);
|
|
1372
|
+
return b;
|
|
1373
|
+
}
|
|
1374
|
+
function u32le(n) {
|
|
1375
|
+
const b = Buffer.alloc(4);
|
|
1376
|
+
b.writeUInt32LE(n >>> 0);
|
|
1377
|
+
return b;
|
|
1378
|
+
}
|
|
1379
|
+
function u32be(n) {
|
|
1380
|
+
const b = Buffer.alloc(4);
|
|
1381
|
+
b.writeUInt32BE(n >>> 0);
|
|
1382
|
+
return b;
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
// dist/core/lz4-block.js
|
|
1386
|
+
var MAX_OUTPUT = 64 * 1024 * 1024;
|
|
1387
|
+
function lz4BlockDecompress(block, size) {
|
|
1388
|
+
if (!Number.isSafeInteger(size) || size < 0 || size > MAX_OUTPUT)
|
|
1389
|
+
return void 0;
|
|
1390
|
+
const out = Buffer.alloc(size);
|
|
1391
|
+
const end = block.length;
|
|
1392
|
+
let pos = 0;
|
|
1393
|
+
let written = 0;
|
|
1394
|
+
const extend2 = (base) => {
|
|
1395
|
+
let len = base;
|
|
1396
|
+
if (base !== 15)
|
|
1397
|
+
return len;
|
|
1398
|
+
for (; ; ) {
|
|
1399
|
+
if (pos >= end)
|
|
1400
|
+
return void 0;
|
|
1401
|
+
const extra = block[pos++];
|
|
1402
|
+
len += extra;
|
|
1403
|
+
if (extra !== 255)
|
|
1404
|
+
return len;
|
|
1405
|
+
}
|
|
1406
|
+
};
|
|
1407
|
+
while (pos < end) {
|
|
1408
|
+
const token = block[pos++];
|
|
1409
|
+
const literals = extend2(token >> 4);
|
|
1410
|
+
if (literals === void 0)
|
|
1411
|
+
return void 0;
|
|
1412
|
+
if (pos + literals > end || written + literals > size)
|
|
1413
|
+
return void 0;
|
|
1414
|
+
for (let i = 0; i < literals; i++)
|
|
1415
|
+
out[written++] = block[pos++];
|
|
1416
|
+
if (pos === end)
|
|
1417
|
+
break;
|
|
1418
|
+
if (pos + 2 > end)
|
|
1419
|
+
return void 0;
|
|
1420
|
+
const offset = block[pos] | block[pos + 1] << 8;
|
|
1421
|
+
pos += 2;
|
|
1422
|
+
if (offset === 0 || offset > written)
|
|
1423
|
+
return void 0;
|
|
1424
|
+
const match = extend2(token & 15);
|
|
1425
|
+
if (match === void 0)
|
|
1426
|
+
return void 0;
|
|
1427
|
+
const length = match + 4;
|
|
1428
|
+
if (written + length > size)
|
|
1429
|
+
return void 0;
|
|
1430
|
+
let from = written - offset;
|
|
1431
|
+
for (let i = 0; i < length; i++)
|
|
1432
|
+
out[written++] = out[from++];
|
|
1433
|
+
}
|
|
1434
|
+
return written === size ? out : void 0;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
// dist/transport/http/passport.js
|
|
1438
|
+
function loginCredentials(email, password, country) {
|
|
1439
|
+
const { clientPublicKeyHex, encryptedPassword } = encryptLoginPassword(password);
|
|
1440
|
+
return { email, password: encryptedPassword, ab: country, client_secret_info: { public_key: clientPublicKeyHex } };
|
|
1441
|
+
}
|
|
1442
|
+
function readLoginReply(data) {
|
|
1443
|
+
const userId = data.ap_cloud_user_id ?? data.user_id ?? data.userId;
|
|
1444
|
+
const authToken = data.auth_token ?? data.token;
|
|
1445
|
+
if (!userId || !authToken)
|
|
1446
|
+
return void 0;
|
|
1447
|
+
return {
|
|
1448
|
+
userId,
|
|
1449
|
+
accountUserId: data.user_id ?? data.userId,
|
|
1450
|
+
authToken,
|
|
1451
|
+
geoKey: data.geo_key,
|
|
1452
|
+
tokenExpiresAt: Number(data.token_expires_at ?? 0) || 0,
|
|
1453
|
+
twoFactorPending: !!(data.fa_info ?? {}).info
|
|
1454
|
+
};
|
|
1455
|
+
}
|
|
1456
|
+
function signedHeaders(entry, encBody) {
|
|
1457
|
+
const ts = nowSec();
|
|
1458
|
+
const once = genId();
|
|
1459
|
+
return {
|
|
1460
|
+
"x-encryption-info": "algo_ecdh",
|
|
1461
|
+
"x-key-ident": entry.keyIdent,
|
|
1462
|
+
"x-request-ts": ts,
|
|
1463
|
+
"x-request-once": once,
|
|
1464
|
+
"x-signature": signRequest(entry.shareKey, ts, once, encBody)
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1073
1468
|
// dist/transport/http/mega-client.js
|
|
1074
1469
|
function parseMaybeJson(text2) {
|
|
1075
1470
|
if (text2.length === 0)
|
|
@@ -1437,17 +1832,11 @@ ${this.auth_?.authToken ?? ""}`;
|
|
|
1437
1832
|
const order = preferred === "text/plain" ? ["text/plain", "application/json"] : ["application/json", "text/plain"];
|
|
1438
1833
|
let last;
|
|
1439
1834
|
for (const contentType of order) {
|
|
1440
|
-
const ts = nowSec();
|
|
1441
|
-
const once = genId();
|
|
1442
1835
|
const headers = {
|
|
1443
1836
|
...this.baseHeaders(),
|
|
1444
1837
|
"content-type": contentType,
|
|
1445
|
-
|
|
1838
|
+
...signedHeaders(entry, encBody),
|
|
1446
1839
|
"x-replay-info": "replay",
|
|
1447
|
-
"x-key-ident": entry.keyIdent,
|
|
1448
|
-
"x-request-ts": ts,
|
|
1449
|
-
"x-request-once": once,
|
|
1450
|
-
"x-signature": signRequest(entry.shareKey, ts, once, encBody),
|
|
1451
1840
|
// Per-call header overrides (e.g. app-name=eufy_security to request a security-scoped
|
|
1452
1841
|
// MQTT cert on an existing eufy_mega session — see EufyMega.getUserMqttInfo).
|
|
1453
1842
|
...headerOverrides
|
|
@@ -1746,13 +2135,7 @@ ${this.auth_?.authToken ?? ""}`;
|
|
|
1746
2135
|
}
|
|
1747
2136
|
/** Build the /passport/login body (verify_code empty unless 2FA). */
|
|
1748
2137
|
loginBody(opts) {
|
|
1749
|
-
const
|
|
1750
|
-
const base = {
|
|
1751
|
-
email: this.cfg.email,
|
|
1752
|
-
password: encryptedPassword,
|
|
1753
|
-
ab: this.cfg.countryCode,
|
|
1754
|
-
client_secret_info: { public_key: clientPublicKeyHex }
|
|
1755
|
-
};
|
|
2138
|
+
const base = loginCredentials(this.cfg.email, this.cfg.password, this.cfg.countryCode);
|
|
1756
2139
|
if (opts.verifyCode)
|
|
1757
2140
|
return { ...base, verify_code: opts.verifyCode, login_id: "" };
|
|
1758
2141
|
if (opts.captchaId)
|
|
@@ -1990,15 +2373,12 @@ ${this.auth_?.authToken ?? ""}`;
|
|
|
1990
2373
|
if (cap.length)
|
|
1991
2374
|
this.logger.debug("[mega] captcha/fa:", JSON.stringify(Object.fromEntries(cap.map((k) => [k, res[k]]))));
|
|
1992
2375
|
}
|
|
1993
|
-
const
|
|
1994
|
-
|
|
1995
|
-
const authToken = res.auth_token ?? res.token;
|
|
1996
|
-
if (!userId || !authToken)
|
|
2376
|
+
const reply = readLoginReply(res);
|
|
2377
|
+
if (!reply)
|
|
1997
2378
|
throw new Error(`login returned no session: ${JSON.stringify(res).slice(0, 200)}`);
|
|
1998
|
-
const
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
this.auth_ = { userId, accountUserId, authToken, geoKey: res.geo_key };
|
|
2379
|
+
const { userId, accountUserId, authToken, geoKey } = reply;
|
|
2380
|
+
if (!isVerify && reply.twoFactorPending) {
|
|
2381
|
+
this.auth_ = { userId, accountUserId, authToken, geoKey };
|
|
2002
2382
|
this.pendingCaptchaId = void 0;
|
|
2003
2383
|
this.pending2fa = true;
|
|
2004
2384
|
await this.sendVerifyCode(messageType);
|
|
@@ -2006,8 +2386,8 @@ ${this.auth_?.authToken ?? ""}`;
|
|
|
2006
2386
|
}
|
|
2007
2387
|
this.pendingCaptchaId = void 0;
|
|
2008
2388
|
this.pending2fa = false;
|
|
2009
|
-
this.auth_ = { userId, accountUserId, authToken, geoKey
|
|
2010
|
-
this.tokenExpiresAt =
|
|
2389
|
+
this.auth_ = { userId, accountUserId, authToken, geoKey };
|
|
2390
|
+
this.tokenExpiresAt = reply.tokenExpiresAt;
|
|
2011
2391
|
this.sessionKey = void 0;
|
|
2012
2392
|
await this.ensureSessionKey();
|
|
2013
2393
|
this.persist();
|
|
@@ -2125,6 +2505,10 @@ function solixUserTopics(appName, userId) {
|
|
|
2125
2505
|
|
|
2126
2506
|
// dist/transport/mqtt/secure-mqtt.js
|
|
2127
2507
|
var SUBACK_FAILURE = 128;
|
|
2508
|
+
function isRefusal(err) {
|
|
2509
|
+
const granted = err?.packet?.granted;
|
|
2510
|
+
return Array.isArray(granted) && granted.some((g) => typeof g === "number" && (g & SUBACK_FAILURE) !== 0);
|
|
2511
|
+
}
|
|
2128
2512
|
function isNotAuthorized(err) {
|
|
2129
2513
|
return err instanceof Error && /connection refused/i.test(err.message) && /not authori[sz]ed/i.test(err.message);
|
|
2130
2514
|
}
|
|
@@ -2227,19 +2611,16 @@ var SecureMqtt = class extends EventEmitter {
|
|
|
2227
2611
|
}
|
|
2228
2612
|
/**
|
|
2229
2613
|
* Subscribe every inbound leg this device's line uses (see `topics.ts` — one `/res` for most lines,
|
|
2230
|
-
* four topics for `eufy_life`).
|
|
2614
|
+
* four topics for `eufy_life` and the clean line).
|
|
2231
2615
|
*
|
|
2232
|
-
* The grants are INSPECTED, not assumed: AWS IoT
|
|
2233
|
-
* SUBACK_FAILURE
|
|
2234
|
-
*
|
|
2235
|
-
*
|
|
2236
|
-
* line that grants its state channel but refuses (say) the OTA leg still works.
|
|
2616
|
+
* The grants are INSPECTED, not assumed: AWS IoT refuses a policy-denied filter with a
|
|
2617
|
+
* {@link SUBACK_FAILURE} grant rather than failing the connection. A denied topic is reported via
|
|
2618
|
+
* `error` naming the credential scope; only an all-denied device throws, so a line that grants its
|
|
2619
|
+
* state channel but refuses (say) a business leg still works.
|
|
2237
2620
|
*/
|
|
2238
2621
|
async subscribeDevice(device) {
|
|
2239
|
-
if (!this.client)
|
|
2240
|
-
throw new Error("SecureMqtt not connected");
|
|
2241
2622
|
const topics = [...subscribeTopics(device)];
|
|
2242
|
-
const { denied } =
|
|
2623
|
+
const { denied } = await this.subscribeEach(topics);
|
|
2243
2624
|
const scope = this.o.credentials.app_name ?? "default";
|
|
2244
2625
|
if (denied.length === topics.length) {
|
|
2245
2626
|
throw new Error(`subscribe ${device.sn}: every topic denied on credential scope "${scope}" \u2014 this line's topics are granted to a different scope (${denied.join(", ")})`);
|
|
@@ -2250,25 +2631,43 @@ var SecureMqtt = class extends EventEmitter {
|
|
|
2250
2631
|
}
|
|
2251
2632
|
/**
|
|
2252
2633
|
* Subscribe to explicit topic filters, returning the topics that were granted. A scope-denied filter
|
|
2253
|
-
*
|
|
2254
|
-
*
|
|
2255
|
-
*
|
|
2634
|
+
* is dropped from the result instead of throwing — callers that need every leg check the returned
|
|
2635
|
+
* list. Used by lines whose topic vocabulary isn't the eufy `subscribeTopics` shape (e.g. Anker Solix
|
|
2636
|
+
* `dt/{app}/{pn}/{sn}`).
|
|
2256
2637
|
*/
|
|
2257
2638
|
async subscribe(topics) {
|
|
2258
|
-
|
|
2259
|
-
throw new Error("SecureMqtt not connected");
|
|
2260
|
-
return this.partitionGrants(await this.client.subscribeAsync(topics, { qos: 1 })).granted;
|
|
2639
|
+
return (await this.subscribeEach(topics)).granted;
|
|
2261
2640
|
}
|
|
2262
2641
|
/**
|
|
2263
|
-
*
|
|
2264
|
-
*
|
|
2265
|
-
*
|
|
2642
|
+
* Subscribe each filter in a SUBSCRIBE of its own and split the outcome into granted vs refused.
|
|
2643
|
+
*
|
|
2644
|
+
* One request per filter because the MQTT engine treats a SUBACK as all-or-nothing: one refused grant
|
|
2645
|
+
* rejects the whole request, and it forgets EVERY filter of that request for resubscription after a
|
|
2646
|
+
* reconnect — so a batch holding one denied leg would lose the granted legs on the next drop. Alone,
|
|
2647
|
+
* a refused filter costs only itself. A rejection that carries no SUBACK is a transport failure and
|
|
2648
|
+
* is thrown.
|
|
2649
|
+
*
|
|
2650
|
+
* The cost is one SUBSCRIBE and one SUBACK per filter instead of one per device — four for a
|
|
2651
|
+
* four-leg line. They are sent concurrently, so the wall-clock cost is one round trip. Folding them
|
|
2652
|
+
* back into one request brings back the lost resubscription.
|
|
2266
2653
|
*/
|
|
2267
|
-
|
|
2654
|
+
async subscribeEach(topics) {
|
|
2655
|
+
const client = this.client;
|
|
2656
|
+
if (!client)
|
|
2657
|
+
throw new Error("SecureMqtt not connected");
|
|
2658
|
+
const outcomes = await Promise.allSettled(topics.map((topic) => client.subscribeAsync(topic, { qos: 1 })));
|
|
2268
2659
|
const granted = [];
|
|
2269
2660
|
const denied = [];
|
|
2270
|
-
|
|
2271
|
-
|
|
2661
|
+
outcomes.forEach((outcome, i) => {
|
|
2662
|
+
const topic = topics[i];
|
|
2663
|
+
if (outcome.status === "fulfilled") {
|
|
2664
|
+
granted.push(topic);
|
|
2665
|
+
return;
|
|
2666
|
+
}
|
|
2667
|
+
if (!isRefusal(outcome.reason))
|
|
2668
|
+
throw outcome.reason;
|
|
2669
|
+
denied.push(topic);
|
|
2670
|
+
});
|
|
2272
2671
|
return { granted, denied };
|
|
2273
2672
|
}
|
|
2274
2673
|
/**
|
|
@@ -2276,141 +2675,32 @@ var SecureMqtt = class extends EventEmitter {
|
|
|
2276
2675
|
* is a pre-built envelope the caller supplies (the command router builds it). QoS 1 by default (the
|
|
2277
2676
|
* broker acks). This is the outbound leg — commands, alongside the receive-only `/res` subscriptions.
|
|
2278
2677
|
*
|
|
2279
|
-
* NOTE: which cert scope is used does NOT gate publishing to a security device's topic — confirmed
|
|
2280
|
-
* live, an `eufy_mega`-scoped cert and an `eufy_security`-scoped cert get the identical grant/deny
|
|
2281
|
-
* pattern. What actually matters is landing on the broker instance that currently holds the device's
|
|
2282
|
-
* session (`ensureSecurityMqttFor`/`broker-discovery.ts` — the NLB fronts several instances that
|
|
2283
|
-
* don't share subscribe/publish routing).
|
|
2284
|
-
*/
|
|
2285
|
-
async publish(topic, body, opts = {}) {
|
|
2286
|
-
if (!this.client)
|
|
2287
|
-
throw new Error("SecureMqtt not connected");
|
|
2288
|
-
await this.client.publishAsync(topic, body, { qos: opts.qos ?? 1 });
|
|
2289
|
-
}
|
|
2290
|
-
async disconnect() {
|
|
2291
|
-
this.connecting = void 0;
|
|
2292
|
-
await this.client?.endAsync(true);
|
|
2293
|
-
this.client = void 0;
|
|
2294
|
-
}
|
|
2295
|
-
};
|
|
2296
|
-
|
|
2297
|
-
// dist/transport/mqtt/app-client-id.js
|
|
2298
|
-
import { createHash as createHash4 } from "node:crypto";
|
|
2299
|
-
function buildAppShapedClientId(input) {
|
|
2300
|
-
const ts = input.timestamp ?? Math.floor(Date.now() / 1e3);
|
|
2301
|
-
return `android-${input.appName}-${input.uid}-${input.mqttUuid}-${ts}`;
|
|
2302
|
-
}
|
|
2303
|
-
function mqttUuidFrom(installId) {
|
|
2304
|
-
return createHash4("sha256").update(installId).digest("hex").slice(0, 16);
|
|
2305
|
-
}
|
|
2306
|
-
|
|
2307
|
-
// dist/core/util.js
|
|
2308
|
-
var Timer = class {
|
|
2309
|
-
handle;
|
|
2310
|
-
/** (Re)arm to run `fn` after `ms`, cancelling any already-pending fire. */
|
|
2311
|
-
arm(ms, fn) {
|
|
2312
|
-
this.cancel();
|
|
2313
|
-
this.handle = setTimeout(() => {
|
|
2314
|
-
this.handle = void 0;
|
|
2315
|
-
fn();
|
|
2316
|
-
}, ms);
|
|
2317
|
-
this.handle.unref?.();
|
|
2318
|
-
}
|
|
2319
|
-
/** Cancel a pending fire. No-op if not armed. */
|
|
2320
|
-
cancel() {
|
|
2321
|
-
if (this.handle) {
|
|
2322
|
-
clearTimeout(this.handle);
|
|
2323
|
-
this.handle = void 0;
|
|
2324
|
-
}
|
|
2325
|
-
}
|
|
2326
|
-
/** Whether a fire is currently scheduled. */
|
|
2327
|
-
get pending() {
|
|
2328
|
-
return this.handle !== void 0;
|
|
2329
|
-
}
|
|
2330
|
-
};
|
|
2331
|
-
function assertNever(value) {
|
|
2332
|
-
throw new Error(`unhandled variant: ${JSON.stringify(value)}`);
|
|
2333
|
-
}
|
|
2334
|
-
function asBool(value) {
|
|
2335
|
-
if (typeof value === "string")
|
|
2336
|
-
return value === "1" || value.toLowerCase() === "true";
|
|
2337
|
-
return value === true || value === 1;
|
|
2338
|
-
}
|
|
2339
|
-
function clamp(value, min, max) {
|
|
2340
|
-
if (Number.isNaN(value))
|
|
2341
|
-
return min;
|
|
2342
|
-
return Math.max(min, Math.min(max, Math.round(value)));
|
|
2343
|
-
}
|
|
2344
|
-
function structuralEqual(a, b) {
|
|
2345
|
-
if (Object.is(a, b))
|
|
2346
|
-
return true;
|
|
2347
|
-
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null)
|
|
2348
|
-
return false;
|
|
2349
|
-
const aArr = Array.isArray(a);
|
|
2350
|
-
const bArr = Array.isArray(b);
|
|
2351
|
-
if (aArr !== bArr)
|
|
2352
|
-
return false;
|
|
2353
|
-
if (aArr && bArr) {
|
|
2354
|
-
if (a.length !== b.length)
|
|
2355
|
-
return false;
|
|
2356
|
-
for (let i = 0; i < a.length; i++)
|
|
2357
|
-
if (!structuralEqual(a[i], b[i]))
|
|
2358
|
-
return false;
|
|
2359
|
-
return true;
|
|
2360
|
-
}
|
|
2361
|
-
const aObj = a;
|
|
2362
|
-
const bObj = b;
|
|
2363
|
-
const aKeys = Object.keys(aObj);
|
|
2364
|
-
const bKeys = Object.keys(bObj);
|
|
2365
|
-
if (aKeys.length !== bKeys.length)
|
|
2366
|
-
return false;
|
|
2367
|
-
for (const k of aKeys) {
|
|
2368
|
-
if (!Object.prototype.hasOwnProperty.call(bObj, k))
|
|
2369
|
-
return false;
|
|
2370
|
-
if (!structuralEqual(aObj[k], bObj[k]))
|
|
2371
|
-
return false;
|
|
2372
|
-
}
|
|
2373
|
-
return true;
|
|
2374
|
-
}
|
|
2375
|
-
function coerceEnumValue(enumObj, value) {
|
|
2376
|
-
const n = Number(value);
|
|
2377
|
-
return Number.isInteger(n) && Object.values(enumObj).includes(n) ? n : void 0;
|
|
2378
|
-
}
|
|
2379
|
-
function enumValues(enumObj) {
|
|
2380
|
-
return Object.values(enumObj).join("/");
|
|
2381
|
-
}
|
|
2382
|
-
function enumLabels(enumObj) {
|
|
2383
|
-
return Object.fromEntries(Object.entries(enumObj).map(([name, value]) => [value, name]));
|
|
2384
|
-
}
|
|
2385
|
-
function jsonObject(text2) {
|
|
2386
|
-
if (typeof text2 !== "string")
|
|
2387
|
-
return void 0;
|
|
2388
|
-
try {
|
|
2389
|
-
const v = JSON.parse(text2);
|
|
2390
|
-
return v && typeof v === "object" && !Array.isArray(v) ? v : void 0;
|
|
2391
|
-
} catch {
|
|
2392
|
-
return void 0;
|
|
2678
|
+
* NOTE: which cert scope is used does NOT gate publishing to a security device's topic — confirmed
|
|
2679
|
+
* live, an `eufy_mega`-scoped cert and an `eufy_security`-scoped cert get the identical grant/deny
|
|
2680
|
+
* pattern. What actually matters is landing on the broker instance that currently holds the device's
|
|
2681
|
+
* session (`ensureSecurityMqttFor`/`broker-discovery.ts` — the NLB fronts several instances that
|
|
2682
|
+
* don't share subscribe/publish routing).
|
|
2683
|
+
*/
|
|
2684
|
+
async publish(topic, body, opts = {}) {
|
|
2685
|
+
if (!this.client)
|
|
2686
|
+
throw new Error("SecureMqtt not connected");
|
|
2687
|
+
await this.client.publishAsync(topic, body, { qos: opts.qos ?? 1 });
|
|
2393
2688
|
}
|
|
2689
|
+
async disconnect() {
|
|
2690
|
+
this.connecting = void 0;
|
|
2691
|
+
await this.client?.endAsync(true);
|
|
2692
|
+
this.client = void 0;
|
|
2693
|
+
}
|
|
2694
|
+
};
|
|
2695
|
+
|
|
2696
|
+
// dist/transport/mqtt/app-client-id.js
|
|
2697
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
2698
|
+
function buildAppShapedClientId(input) {
|
|
2699
|
+
const ts = input.timestamp ?? Math.floor(Date.now() / 1e3);
|
|
2700
|
+
return `android-${input.appName}-${input.uid}-${input.mqttUuid}-${ts}`;
|
|
2394
2701
|
}
|
|
2395
|
-
function
|
|
2396
|
-
|
|
2397
|
-
b.writeUInt16LE(n & 65535);
|
|
2398
|
-
return b;
|
|
2399
|
-
}
|
|
2400
|
-
function u16be(n) {
|
|
2401
|
-
const b = Buffer.alloc(2);
|
|
2402
|
-
b.writeUInt16BE(n & 65535);
|
|
2403
|
-
return b;
|
|
2404
|
-
}
|
|
2405
|
-
function u32le(n) {
|
|
2406
|
-
const b = Buffer.alloc(4);
|
|
2407
|
-
b.writeUInt32LE(n >>> 0);
|
|
2408
|
-
return b;
|
|
2409
|
-
}
|
|
2410
|
-
function u32be(n) {
|
|
2411
|
-
const b = Buffer.alloc(4);
|
|
2412
|
-
b.writeUInt32BE(n >>> 0);
|
|
2413
|
-
return b;
|
|
2702
|
+
function mqttUuidFrom(installId) {
|
|
2703
|
+
return createHash4("sha256").update(installId).digest("hex").slice(0, 16);
|
|
2414
2704
|
}
|
|
2415
2705
|
|
|
2416
2706
|
// dist/transport/mqtt/availability.js
|
|
@@ -2537,13 +2827,6 @@ function parseAiotDpReport(raw) {
|
|
|
2537
2827
|
return Object.keys(out).length ? out : void 0;
|
|
2538
2828
|
}
|
|
2539
2829
|
|
|
2540
|
-
// dist/core/raw-dp-hex.js
|
|
2541
|
-
function hexToRawDp(hex) {
|
|
2542
|
-
if (hex.length === 0 || hex.length % 2 !== 0 || !/^[0-9a-fA-F]+$/.test(hex))
|
|
2543
|
-
return void 0;
|
|
2544
|
-
return Buffer.from(hex, "hex").toString("base64");
|
|
2545
|
-
}
|
|
2546
|
-
|
|
2547
2830
|
// dist/transport/mqtt/biz-stream.js
|
|
2548
2831
|
var BIZ_PROTOCOL = {
|
|
2549
2832
|
/** Map-stream data: the frames this module parses. */
|
|
@@ -2727,18 +3010,6 @@ var DeviceType = {
|
|
|
2727
3010
|
CAMERA_C35: 10035
|
|
2728
3011
|
};
|
|
2729
3012
|
|
|
2730
|
-
// dist/core/types.js
|
|
2731
|
-
var PRINTER_CATEGORY_RE = /ankermake|eufymake|fdm|3d_?print/i;
|
|
2732
|
-
function classifyDevice(raw) {
|
|
2733
|
-
const category = raw.category ?? "";
|
|
2734
|
-
const hasP2p = typeof raw.p2p_did === "string" && raw.p2p_did.length > 0;
|
|
2735
|
-
if (category === "eufy_security" || hasP2p)
|
|
2736
|
-
return { category, api: "mega", realtime: "p2p" };
|
|
2737
|
-
if (PRINTER_CATEGORY_RE.test(category))
|
|
2738
|
-
return { category, api: "ankermake", realtime: "ankermake-mqtt" };
|
|
2739
|
-
return { category, api: "mega", realtime: "smqtt" };
|
|
2740
|
-
}
|
|
2741
|
-
|
|
2742
3013
|
// dist/model/classify.js
|
|
2743
3014
|
var STATION_TYPES = /* @__PURE__ */ new Set([
|
|
2744
3015
|
DeviceType.STATION,
|
|
@@ -2778,208 +3049,103 @@ var SENSOR_TYPES = /* @__PURE__ */ new Set([
|
|
|
2778
3049
|
DeviceType.SMOKE_SENSOR,
|
|
2779
3050
|
DeviceType.SIREN_SENSOR_E20,
|
|
2780
3051
|
DeviceType.ENTRY_SENSOR_E20,
|
|
2781
|
-
DeviceType.PIR_SENSOR_E20
|
|
2782
|
-
]);
|
|
2783
|
-
var KEYPAD_TYPES = /* @__PURE__ */ new Set([DeviceType.KEYPAD]);
|
|
2784
|
-
function codecForType(deviceType) {
|
|
2785
|
-
if (!Number.isFinite(deviceType))
|
|
2786
|
-
return void 0;
|
|
2787
|
-
if (STATION_TYPES.has(deviceType))
|
|
2788
|
-
return "station";
|
|
2789
|
-
if (LOCK_TYPES.has(deviceType))
|
|
2790
|
-
return "lock";
|
|
2791
|
-
if (SENSOR_TYPES.has(deviceType))
|
|
2792
|
-
return "sensor";
|
|
2793
|
-
if (KEYPAD_TYPES.has(deviceType))
|
|
2794
|
-
return "keypad";
|
|
2795
|
-
if (isKnownSecurityType(deviceType))
|
|
2796
|
-
return "camera";
|
|
2797
|
-
return void 0;
|
|
2798
|
-
}
|
|
2799
|
-
function isKnownSecurityType(deviceType) {
|
|
2800
|
-
if (deviceType === DeviceType.SMART_TRACK_LINK || deviceType === DeviceType.SMART_TRACK_CARD)
|
|
2801
|
-
return false;
|
|
2802
|
-
if (deviceType >= 0 && deviceType <= 210)
|
|
2803
|
-
return true;
|
|
2804
|
-
if (deviceType >= 300 && deviceType <= 301)
|
|
2805
|
-
return true;
|
|
2806
|
-
if (deviceType >= 1e4 && deviceType <= 10100)
|
|
2807
|
-
return true;
|
|
2808
|
-
return false;
|
|
2809
|
-
}
|
|
2810
|
-
function codecFromModel(model) {
|
|
2811
|
-
if (!model)
|
|
2812
|
-
return void 0;
|
|
2813
|
-
const m = model.trim().toUpperCase();
|
|
2814
|
-
if (m.length === 0)
|
|
2815
|
-
return void 0;
|
|
2816
|
-
if (/^T2(80B|880B?|801B?)$/.test(m))
|
|
2817
|
-
return "mower";
|
|
2818
|
-
if (/^T2/.test(m))
|
|
2819
|
-
return "vacuum";
|
|
2820
|
-
if (/^T1/.test(m))
|
|
2821
|
-
return "vacuum";
|
|
2822
|
-
if (/^T8(00[0-9]|010|023|025|030)/.test(m))
|
|
2823
|
-
return "station";
|
|
2824
|
-
if (/^T8N0/.test(m) || /^T8E0/.test(m))
|
|
2825
|
-
return "station";
|
|
2826
|
-
if (/^T9000/.test(m))
|
|
2827
|
-
return "station";
|
|
2828
|
-
if (/^T74/.test(m))
|
|
2829
|
-
return "lock";
|
|
2830
|
-
if (/^T85/.test(m))
|
|
2831
|
-
return "lock";
|
|
2832
|
-
if (/^T8960/.test(m))
|
|
2833
|
-
return "keypad";
|
|
2834
|
-
if (/^T89/.test(m))
|
|
2835
|
-
return "sensor";
|
|
2836
|
-
if (/^T8L/.test(m))
|
|
2837
|
-
return "light";
|
|
2838
|
-
if (/^T87A/.test(m))
|
|
2839
|
-
return "display";
|
|
2840
|
-
if (/^T8/.test(m))
|
|
2841
|
-
return "camera";
|
|
2842
|
-
return void 0;
|
|
2843
|
-
}
|
|
2844
|
-
function classify(rec) {
|
|
2845
|
-
const byModel = codecFromModel(rec.model);
|
|
2846
|
-
if (rec.category && PRINTER_CATEGORY_RE.test(rec.category))
|
|
2847
|
-
return "printer";
|
|
2848
|
-
if (byModel === "mower")
|
|
2849
|
-
return "mower";
|
|
2850
|
-
if (rec.category && /clean|vacuum|robovac/i.test(rec.category))
|
|
2851
|
-
return "vacuum";
|
|
2852
|
-
if (byModel === "vacuum")
|
|
2853
|
-
return "vacuum";
|
|
2854
|
-
if (byModel === "light" || byModel === "display")
|
|
2855
|
-
return byModel;
|
|
2856
|
-
if (rec.deviceType !== void 0) {
|
|
2857
|
-
const byType = codecForType(rec.deviceType);
|
|
2858
|
-
if (byType !== void 0)
|
|
2859
|
-
return byType;
|
|
2860
|
-
}
|
|
2861
|
-
if (byModel !== void 0)
|
|
2862
|
-
return byModel;
|
|
2863
|
-
return "camera";
|
|
2864
|
-
}
|
|
2865
|
-
|
|
2866
|
-
// dist/model/infer.js
|
|
2867
|
-
function inferName(rec) {
|
|
2868
|
-
const model = rec?.model;
|
|
2869
|
-
if (typeof model !== "string")
|
|
2870
|
-
return void 0;
|
|
2871
|
-
const trimmed = model.trim();
|
|
2872
|
-
if (trimmed.length === 0)
|
|
2873
|
-
return void 0;
|
|
2874
|
-
const match = trimmed.match(/\b([A-Za-z]\d{3,5}[A-Za-z0-9]{0,3})\b/);
|
|
2875
|
-
if (match)
|
|
2876
|
-
return match[1].toUpperCase();
|
|
2877
|
-
return void 0;
|
|
2878
|
-
}
|
|
2879
|
-
|
|
2880
|
-
// dist/core/contracts.js
|
|
2881
|
-
var StoredSnapshotUnavailableError = class extends Error {
|
|
2882
|
-
reason;
|
|
2883
|
-
constructor(reason, message, options) {
|
|
2884
|
-
super(message, options);
|
|
2885
|
-
this.reason = reason;
|
|
2886
|
-
this.name = "StoredSnapshotUnavailableError";
|
|
2887
|
-
}
|
|
2888
|
-
};
|
|
2889
|
-
var RETRYABLE_LIVE_SNAPSHOT_REASONS = [
|
|
2890
|
-
"no-keyframe",
|
|
2891
|
-
"source-failed",
|
|
2892
|
-
"undecodable-burst"
|
|
2893
|
-
];
|
|
2894
|
-
var LiveSnapshotUnavailableError = class extends Error {
|
|
2895
|
-
reason;
|
|
2896
|
-
/** Whether another attempt could plausibly succeed without the host changing anything. */
|
|
2897
|
-
retryable;
|
|
2898
|
-
constructor(reason, message, options) {
|
|
2899
|
-
super(message, options);
|
|
2900
|
-
this.reason = reason;
|
|
2901
|
-
this.name = "LiveSnapshotUnavailableError";
|
|
2902
|
-
this.retryable = RETRYABLE_LIVE_SNAPSHOT_REASONS.includes(reason);
|
|
2903
|
-
}
|
|
2904
|
-
};
|
|
2905
|
-
var CameraDisabledError = class extends Error {
|
|
2906
|
-
camera;
|
|
2907
|
-
constructor(camera, options) {
|
|
2908
|
-
super(`camera${camera ? ` ${camera}` : ""} is disabled \u2014 no live stream possible`, options);
|
|
2909
|
-
this.camera = camera;
|
|
2910
|
-
this.name = "CameraDisabledError";
|
|
2911
|
-
}
|
|
2912
|
-
};
|
|
2913
|
-
var StationKeyUnavailableError = class extends Error {
|
|
2914
|
-
stationSn;
|
|
2915
|
-
/** Always true: the negotiation is per connection, so a later one may still produce a key. */
|
|
2916
|
-
retryable = true;
|
|
2917
|
-
constructor(stationSn, options) {
|
|
2918
|
-
super(`station ${stationSn} did not provide its session key, so nothing that requires one could be sent`, options);
|
|
2919
|
-
this.stationSn = stationSn;
|
|
2920
|
-
this.name = "StationKeyUnavailableError";
|
|
2921
|
-
}
|
|
2922
|
-
};
|
|
2923
|
-
var StationUnreachableError = class extends Error {
|
|
2924
|
-
stationSn;
|
|
2925
|
-
waitedMs;
|
|
2926
|
-
/** Always true: a station unreachable now may answer on a later attempt. */
|
|
2927
|
-
retryable = true;
|
|
2928
|
-
constructor(stationSn, waitedMs, options) {
|
|
2929
|
-
super(`station ${stationSn}'s P2P session did not connect within ${waitedMs}ms, so nothing could be sent to it`, options);
|
|
2930
|
-
this.stationSn = stationSn;
|
|
2931
|
-
this.waitedMs = waitedMs;
|
|
2932
|
-
this.name = "StationUnreachableError";
|
|
2933
|
-
}
|
|
2934
|
-
};
|
|
2935
|
-
var DeviceChannelUnresolvedError = class extends Error {
|
|
2936
|
-
sn;
|
|
2937
|
-
stationSn;
|
|
2938
|
-
constructor(sn, stationSn, options) {
|
|
2939
|
-
super(`${sn} has no usable channel on station ${stationSn}, so nothing was sent to it`, options);
|
|
2940
|
-
this.sn = sn;
|
|
2941
|
-
this.stationSn = stationSn;
|
|
2942
|
-
this.name = "DeviceChannelUnresolvedError";
|
|
2943
|
-
}
|
|
2944
|
-
};
|
|
2945
|
-
var LiveStreamStartError = class extends Error {
|
|
2946
|
-
reason;
|
|
2947
|
-
stage;
|
|
2948
|
-
timeoutMs;
|
|
2949
|
-
attempts;
|
|
2950
|
-
constructor(failure) {
|
|
2951
|
-
super(`live stream failed to start (${failure.reason} at ${failure.stage} after ${failure.attempts} attempts; ${failure.timeoutMs}ms deadline)`, failure.cause === void 0 ? void 0 : { cause: failure.cause });
|
|
2952
|
-
this.name = "LiveStreamStartError";
|
|
2953
|
-
this.reason = failure.reason;
|
|
2954
|
-
this.stage = failure.stage;
|
|
2955
|
-
this.timeoutMs = failure.timeoutMs;
|
|
2956
|
-
this.attempts = failure.attempts;
|
|
2957
|
-
}
|
|
2958
|
-
};
|
|
2959
|
-
var StateConvergenceError = class extends Error {
|
|
2960
|
-
sn;
|
|
2961
|
-
property;
|
|
2962
|
-
param;
|
|
2963
|
-
expected;
|
|
2964
|
-
observed;
|
|
2965
|
-
timeoutMs;
|
|
2966
|
-
constructor(failure) {
|
|
2967
|
-
super(`device ${failure.sn} did not report ${failure.property} as ${String(failure.expected)} within ${failure.timeoutMs}ms (param ${failure.param} read ${String(failure.observed)})`);
|
|
2968
|
-
this.name = "StateConvergenceError";
|
|
2969
|
-
this.sn = failure.sn;
|
|
2970
|
-
this.property = failure.property;
|
|
2971
|
-
this.param = failure.param;
|
|
2972
|
-
this.expected = failure.expected;
|
|
2973
|
-
this.observed = failure.observed;
|
|
2974
|
-
this.timeoutMs = failure.timeoutMs;
|
|
3052
|
+
DeviceType.PIR_SENSOR_E20
|
|
3053
|
+
]);
|
|
3054
|
+
var KEYPAD_TYPES = /* @__PURE__ */ new Set([DeviceType.KEYPAD]);
|
|
3055
|
+
function codecForType(deviceType) {
|
|
3056
|
+
if (!Number.isFinite(deviceType))
|
|
3057
|
+
return void 0;
|
|
3058
|
+
if (STATION_TYPES.has(deviceType))
|
|
3059
|
+
return "station";
|
|
3060
|
+
if (LOCK_TYPES.has(deviceType))
|
|
3061
|
+
return "lock";
|
|
3062
|
+
if (SENSOR_TYPES.has(deviceType))
|
|
3063
|
+
return "sensor";
|
|
3064
|
+
if (KEYPAD_TYPES.has(deviceType))
|
|
3065
|
+
return "keypad";
|
|
3066
|
+
if (isKnownSecurityType(deviceType))
|
|
3067
|
+
return "camera";
|
|
3068
|
+
return void 0;
|
|
3069
|
+
}
|
|
3070
|
+
function isKnownSecurityType(deviceType) {
|
|
3071
|
+
if (deviceType === DeviceType.SMART_TRACK_LINK || deviceType === DeviceType.SMART_TRACK_CARD)
|
|
3072
|
+
return false;
|
|
3073
|
+
if (deviceType >= 0 && deviceType <= 210)
|
|
3074
|
+
return true;
|
|
3075
|
+
if (deviceType >= 300 && deviceType <= 301)
|
|
3076
|
+
return true;
|
|
3077
|
+
if (deviceType >= 1e4 && deviceType <= 10100)
|
|
3078
|
+
return true;
|
|
3079
|
+
return false;
|
|
3080
|
+
}
|
|
3081
|
+
function codecFromModel(model) {
|
|
3082
|
+
if (!model)
|
|
3083
|
+
return void 0;
|
|
3084
|
+
const m = model.trim().toUpperCase();
|
|
3085
|
+
if (m.length === 0)
|
|
3086
|
+
return void 0;
|
|
3087
|
+
if (/^T2(80B|880B?|801B?)$/.test(m))
|
|
3088
|
+
return "mower";
|
|
3089
|
+
if (/^T2/.test(m))
|
|
3090
|
+
return "vacuum";
|
|
3091
|
+
if (/^T1/.test(m))
|
|
3092
|
+
return "vacuum";
|
|
3093
|
+
if (/^T8(00[0-9]|010|023|025|030)/.test(m))
|
|
3094
|
+
return "station";
|
|
3095
|
+
if (/^T8N0/.test(m) || /^T8E0/.test(m))
|
|
3096
|
+
return "station";
|
|
3097
|
+
if (/^T9000/.test(m))
|
|
3098
|
+
return "station";
|
|
3099
|
+
if (/^T74/.test(m))
|
|
3100
|
+
return "lock";
|
|
3101
|
+
if (/^T85/.test(m))
|
|
3102
|
+
return "lock";
|
|
3103
|
+
if (/^T8960/.test(m))
|
|
3104
|
+
return "keypad";
|
|
3105
|
+
if (/^T89/.test(m))
|
|
3106
|
+
return "sensor";
|
|
3107
|
+
if (/^T8L/.test(m))
|
|
3108
|
+
return "light";
|
|
3109
|
+
if (/^T87A/.test(m))
|
|
3110
|
+
return "display";
|
|
3111
|
+
if (/^T8/.test(m))
|
|
3112
|
+
return "camera";
|
|
3113
|
+
return void 0;
|
|
3114
|
+
}
|
|
3115
|
+
function classify(rec) {
|
|
3116
|
+
const byModel = codecFromModel(rec.model);
|
|
3117
|
+
if (rec.category && PRINTER_CATEGORY_RE.test(rec.category))
|
|
3118
|
+
return "printer";
|
|
3119
|
+
if (byModel === "mower")
|
|
3120
|
+
return "mower";
|
|
3121
|
+
if (rec.category && /clean|vacuum|robovac/i.test(rec.category))
|
|
3122
|
+
return "vacuum";
|
|
3123
|
+
if (byModel === "vacuum")
|
|
3124
|
+
return "vacuum";
|
|
3125
|
+
if (byModel === "light" || byModel === "display")
|
|
3126
|
+
return byModel;
|
|
3127
|
+
if (rec.deviceType !== void 0) {
|
|
3128
|
+
const byType = codecForType(rec.deviceType);
|
|
3129
|
+
if (byType !== void 0)
|
|
3130
|
+
return byType;
|
|
2975
3131
|
}
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
return Object.defineProperty(command, COMMAND_OBSERVATION, { configurable: true, value: observation });
|
|
3132
|
+
if (byModel !== void 0)
|
|
3133
|
+
return byModel;
|
|
3134
|
+
return "camera";
|
|
2980
3135
|
}
|
|
2981
|
-
|
|
2982
|
-
|
|
3136
|
+
|
|
3137
|
+
// dist/model/infer.js
|
|
3138
|
+
function inferName(rec) {
|
|
3139
|
+
const model = rec?.model;
|
|
3140
|
+
if (typeof model !== "string")
|
|
3141
|
+
return void 0;
|
|
3142
|
+
const trimmed = model.trim();
|
|
3143
|
+
if (trimmed.length === 0)
|
|
3144
|
+
return void 0;
|
|
3145
|
+
const match = trimmed.match(/\b([A-Za-z]\d{3,5}[A-Za-z0-9]{0,3})\b/);
|
|
3146
|
+
if (match)
|
|
3147
|
+
return match[1].toUpperCase();
|
|
3148
|
+
return void 0;
|
|
2983
3149
|
}
|
|
2984
3150
|
|
|
2985
3151
|
// dist/model/capabilities/access.js
|
|
@@ -4326,7 +4492,7 @@ function decodePowerSource(raw) {
|
|
|
4326
4492
|
function recordSetting(param, value, ctx) {
|
|
4327
4493
|
return setStationScalar(param, value, ctx.channel);
|
|
4328
4494
|
}
|
|
4329
|
-
var MAINS_CAMERA_MODELS = ["T8425", "T8419", "T8410", "T84A1"];
|
|
4495
|
+
var MAINS_CAMERA_MODELS = ["T8425", "T8419", "T8410", "T84A1", "T8423"];
|
|
4330
4496
|
function cameraPowerTier(model, capabilities) {
|
|
4331
4497
|
return capabilities.has("battery") && notMainsCamera({ model }) ? "battery" : "wired";
|
|
4332
4498
|
}
|
|
@@ -7562,85 +7728,6 @@ var STORAGE = {
|
|
|
7562
7728
|
detection: { codecs: ["station"] }
|
|
7563
7729
|
};
|
|
7564
7730
|
|
|
7565
|
-
// dist/core/raw-dp-writer.js
|
|
7566
|
-
var WIRE = { VARINT: 0, LENGTH_DELIMITED: 2 };
|
|
7567
|
-
function encodeVarint(value) {
|
|
7568
|
-
if (!Number.isSafeInteger(value) || value < 0) {
|
|
7569
|
-
throw new Error(`varint: ${value} is not a non-negative safe integer`);
|
|
7570
|
-
}
|
|
7571
|
-
const out = [];
|
|
7572
|
-
let v = value;
|
|
7573
|
-
while (v > 127) {
|
|
7574
|
-
out.push(v % 128 + 128);
|
|
7575
|
-
v = Math.floor(v / 128);
|
|
7576
|
-
}
|
|
7577
|
-
out.push(v);
|
|
7578
|
-
return out;
|
|
7579
|
-
}
|
|
7580
|
-
function zigzag(value) {
|
|
7581
|
-
if (!Number.isSafeInteger(value))
|
|
7582
|
-
throw new Error(`zigzag: ${value} is not a safe integer`);
|
|
7583
|
-
return value < 0 ? -2 * value - 1 : 2 * value;
|
|
7584
|
-
}
|
|
7585
|
-
var RawDpWriter = class _RawDpWriter {
|
|
7586
|
-
body = [];
|
|
7587
|
-
/** A varint-valued field (`int32`, `uint32`, `bool`, an enum member). */
|
|
7588
|
-
int(field, value) {
|
|
7589
|
-
this.body.push(...encodeVarint(tag(field, WIRE.VARINT)), ...encodeVarint(value));
|
|
7590
|
-
return this;
|
|
7591
|
-
}
|
|
7592
|
-
/** A signed varint field (`sint32`, `sint64`), ZigZag-encoded — see {@link zigzag}. */
|
|
7593
|
-
sint(field, value) {
|
|
7594
|
-
return this.int(field, zigzag(value));
|
|
7595
|
-
}
|
|
7596
|
-
/** A boolean field, on protobuf's own terms: `1` or `0` in a varint. */
|
|
7597
|
-
bool(field, value) {
|
|
7598
|
-
return this.int(field, value ? 1 : 0);
|
|
7599
|
-
}
|
|
7600
|
-
/** A length-delimited field carrying raw bytes — a string, a blob, or a message built elsewhere. */
|
|
7601
|
-
bytes(field, value) {
|
|
7602
|
-
const bytes2 = value instanceof Uint8Array ? Array.from(value) : [...value];
|
|
7603
|
-
this.body.push(...encodeVarint(tag(field, WIRE.LENGTH_DELIMITED)), ...encodeVarint(bytes2.length), ...bytes2);
|
|
7604
|
-
return this;
|
|
7605
|
-
}
|
|
7606
|
-
/**
|
|
7607
|
-
* A nested sub-message, built by `build` into a writer of its own.
|
|
7608
|
-
*
|
|
7609
|
-
* The sub-message is emitted even when `build` writes nothing into it. That is not an oversight: an
|
|
7610
|
-
* EMPTY sub-message is how these protocols say "this subsystem exists and is in its zero state", as
|
|
7611
|
-
* opposed to saying nothing about it, and the readers on the other side of this file depend on the
|
|
7612
|
-
* distinction. Omitting the call is what says nothing about it.
|
|
7613
|
-
*/
|
|
7614
|
-
sub(field, build) {
|
|
7615
|
-
const inner = new _RawDpWriter();
|
|
7616
|
-
build(inner);
|
|
7617
|
-
return this.bytes(field, inner.toBytes());
|
|
7618
|
-
}
|
|
7619
|
-
/** The message body on its own, unframed — for nesting into another writer. */
|
|
7620
|
-
toBytes() {
|
|
7621
|
-
return [...this.body];
|
|
7622
|
-
}
|
|
7623
|
-
/**
|
|
7624
|
-
* The finished DP value: `varint(bodyLength) ++ body`, base64-encoded.
|
|
7625
|
-
*
|
|
7626
|
-
* This is the envelope `RawDpCodec.decode` unwraps, so a value produced here and read back through the
|
|
7627
|
-
* codec is a genuine round-trip rather than two implementations of the same guess.
|
|
7628
|
-
*/
|
|
7629
|
-
finish() {
|
|
7630
|
-
return Buffer.from([...encodeVarint(this.body.length), ...this.body]).toString("base64");
|
|
7631
|
-
}
|
|
7632
|
-
};
|
|
7633
|
-
function tag(field, wire) {
|
|
7634
|
-
if (!Number.isSafeInteger(field) || field < 1)
|
|
7635
|
-
throw new Error(`field number: ${field} is not a positive integer`);
|
|
7636
|
-
return field * 8 + wire;
|
|
7637
|
-
}
|
|
7638
|
-
function rawDp(build) {
|
|
7639
|
-
const w = new RawDpWriter();
|
|
7640
|
-
build(w);
|
|
7641
|
-
return w.finish();
|
|
7642
|
-
}
|
|
7643
|
-
|
|
7644
7731
|
// dist/model/capabilities/vacuum-dock.js
|
|
7645
7732
|
var VACUUM_DOCK_DP = 173;
|
|
7646
7733
|
var VACUUM_DOCK_INFO_DP = 169;
|
|
@@ -13345,58 +13432,6 @@ function parseCleanRecordDetail(blob, codec) {
|
|
|
13345
13432
|
};
|
|
13346
13433
|
}
|
|
13347
13434
|
|
|
13348
|
-
// dist/core/lz4-block.js
|
|
13349
|
-
var MAX_OUTPUT = 64 * 1024 * 1024;
|
|
13350
|
-
function lz4BlockDecompress(block, size) {
|
|
13351
|
-
if (!Number.isSafeInteger(size) || size < 0 || size > MAX_OUTPUT)
|
|
13352
|
-
return void 0;
|
|
13353
|
-
const out = Buffer.alloc(size);
|
|
13354
|
-
const end = block.length;
|
|
13355
|
-
let pos = 0;
|
|
13356
|
-
let written = 0;
|
|
13357
|
-
const extend2 = (base) => {
|
|
13358
|
-
let len = base;
|
|
13359
|
-
if (base !== 15)
|
|
13360
|
-
return len;
|
|
13361
|
-
for (; ; ) {
|
|
13362
|
-
if (pos >= end)
|
|
13363
|
-
return void 0;
|
|
13364
|
-
const extra = block[pos++];
|
|
13365
|
-
len += extra;
|
|
13366
|
-
if (extra !== 255)
|
|
13367
|
-
return len;
|
|
13368
|
-
}
|
|
13369
|
-
};
|
|
13370
|
-
while (pos < end) {
|
|
13371
|
-
const token = block[pos++];
|
|
13372
|
-
const literals = extend2(token >> 4);
|
|
13373
|
-
if (literals === void 0)
|
|
13374
|
-
return void 0;
|
|
13375
|
-
if (pos + literals > end || written + literals > size)
|
|
13376
|
-
return void 0;
|
|
13377
|
-
for (let i = 0; i < literals; i++)
|
|
13378
|
-
out[written++] = block[pos++];
|
|
13379
|
-
if (pos === end)
|
|
13380
|
-
break;
|
|
13381
|
-
if (pos + 2 > end)
|
|
13382
|
-
return void 0;
|
|
13383
|
-
const offset = block[pos] | block[pos + 1] << 8;
|
|
13384
|
-
pos += 2;
|
|
13385
|
-
if (offset === 0 || offset > written)
|
|
13386
|
-
return void 0;
|
|
13387
|
-
const match = extend2(token & 15);
|
|
13388
|
-
if (match === void 0)
|
|
13389
|
-
return void 0;
|
|
13390
|
-
const length = match + 4;
|
|
13391
|
-
if (written + length > size)
|
|
13392
|
-
return void 0;
|
|
13393
|
-
let from = written - offset;
|
|
13394
|
-
for (let i = 0; i < length; i++)
|
|
13395
|
-
out[written++] = out[from++];
|
|
13396
|
-
}
|
|
13397
|
-
return written === size ? out : void 0;
|
|
13398
|
-
}
|
|
13399
|
-
|
|
13400
13435
|
// dist/model/vacuum-map.js
|
|
13401
13436
|
var DOCK_KINDS = ["charger", "station"];
|
|
13402
13437
|
var MAP_QUALITIES = ["incomplete", "rough", "effective", "listFull"];
|
|
@@ -15159,6 +15194,7 @@ var LOCAL_LOOKUP_PORT = 32108;
|
|
|
15159
15194
|
var HEARTBEAT_MS = 5e3;
|
|
15160
15195
|
var PATH_SILENCE_MS = HEARTBEAT_MS * 3;
|
|
15161
15196
|
var LOOKUP_RETRY_MS = 1e3;
|
|
15197
|
+
var RECEIVE_BUFFER_BYTES = 4 * 1024 * 1024;
|
|
15162
15198
|
var CONNECT_TIMEOUT_MS = 15e3;
|
|
15163
15199
|
var STATION_CHANNEL3 = 255;
|
|
15164
15200
|
var CMD_GATEWAYINFO = 1100;
|
|
@@ -15188,6 +15224,7 @@ var REORDER_MAX_DATAGRAMS = 128;
|
|
|
15188
15224
|
var MAX_TRACED_DATAGRAM_GAPS = 8;
|
|
15189
15225
|
var CMD_SET_PAYLOAD = 1350;
|
|
15190
15226
|
var CMD_NOTIFY_PAYLOAD = 1351;
|
|
15227
|
+
var PADDED_RESULT_BYTES = 132;
|
|
15191
15228
|
var CMD_CAMERA_INFO = 1103;
|
|
15192
15229
|
var CMD_DATABASE_IMAGE = 1308;
|
|
15193
15230
|
var CMD_DATABASE = 1306;
|
|
@@ -15213,6 +15250,8 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
15213
15250
|
connected = false;
|
|
15214
15251
|
connecting = false;
|
|
15215
15252
|
closed = false;
|
|
15253
|
+
/** Whether a short receive buffer has been reported, so a reconnect does not repeat the same warning. */
|
|
15254
|
+
receiveBufferReported = false;
|
|
15216
15255
|
connectAddress;
|
|
15217
15256
|
seqNumber = 0;
|
|
15218
15257
|
/**
|
|
@@ -15252,7 +15291,10 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
15252
15291
|
cloudLookup;
|
|
15253
15292
|
/** Local outbound IPv4 reported inside LOOKUP_WITH_KEY requests. */
|
|
15254
15293
|
selfHost;
|
|
15255
|
-
/**
|
|
15294
|
+
/**
|
|
15295
|
+
* In-flight multi-datagram frame per data channel (see reassemble): the payload gathered so far under its
|
|
15296
|
+
* parsed header, or, without a header, the start of a frame header cut by the datagram boundary.
|
|
15297
|
+
*/
|
|
15256
15298
|
pendingByDataType = /* @__PURE__ */ new Map();
|
|
15257
15299
|
/** Last delivered datagram sequence number per data type. */
|
|
15258
15300
|
lastSeqByType = /* @__PURE__ */ new Map();
|
|
@@ -15532,6 +15574,26 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
15532
15574
|
get isConnected() {
|
|
15533
15575
|
return this.connected;
|
|
15534
15576
|
}
|
|
15577
|
+
/**
|
|
15578
|
+
* Ask the OS for {@link RECEIVE_BUFFER_BYTES} on a bound socket, and warn once per session when it grants
|
|
15579
|
+
* less or refuses.
|
|
15580
|
+
*
|
|
15581
|
+
* The request is made here rather than through `createSocket`'s `recvBufferSize`: Node applies that option
|
|
15582
|
+
* inside the bind callback, where a refusal is thrown out of reach of this session and ends the process.
|
|
15583
|
+
*/
|
|
15584
|
+
requestReceiveBuffer(socket) {
|
|
15585
|
+
let granted;
|
|
15586
|
+
try {
|
|
15587
|
+
socket.setRecvBufferSize(RECEIVE_BUFFER_BYTES);
|
|
15588
|
+
granted = socket.getRecvBufferSize();
|
|
15589
|
+
} catch {
|
|
15590
|
+
granted = 0;
|
|
15591
|
+
}
|
|
15592
|
+
if (granted >= RECEIVE_BUFFER_BYTES || this.receiveBufferReported)
|
|
15593
|
+
return;
|
|
15594
|
+
this.receiveBufferReported = true;
|
|
15595
|
+
this.logger.warn(`[p2p] ${this.cfg.stationSn} UDP receive buffer below the ${RECEIVE_BUFFER_BYTES} bytes requested` + (granted > 0 ? ` (granted ${granted})` : " (request refused)") + `; live video can lose keyframes. Raise the OS limit (net.core.rmem_max on Linux, kern.ipc.maxsockbuf on BSD) to at least ${RECEIVE_BUFFER_BYTES}.`);
|
|
15596
|
+
}
|
|
15535
15597
|
/** Open the socket and start the lookup → hole-punch handshake. */
|
|
15536
15598
|
async connect() {
|
|
15537
15599
|
if (this.connecting || this.connected)
|
|
@@ -15555,6 +15617,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
15555
15617
|
socket.setBroadcast(true);
|
|
15556
15618
|
} catch {
|
|
15557
15619
|
}
|
|
15620
|
+
this.requestReceiveBuffer(socket);
|
|
15558
15621
|
resolve();
|
|
15559
15622
|
});
|
|
15560
15623
|
});
|
|
@@ -16627,7 +16690,13 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16627
16690
|
clearTimeout(reorder.timer);
|
|
16628
16691
|
reorder.timer = void 0;
|
|
16629
16692
|
}
|
|
16630
|
-
/**
|
|
16693
|
+
/**
|
|
16694
|
+
* Reassemble one in-sequence datagram body into logical frames.
|
|
16695
|
+
*
|
|
16696
|
+
* Frames are packed back to back, so a frame header can itself be cut by a datagram boundary. The start
|
|
16697
|
+
* of a header left at the end of a datagram is carried into the next one rather than discarded; dropping
|
|
16698
|
+
* it would lose that frame and every frame after it until a datagram happened to begin on a header.
|
|
16699
|
+
*/
|
|
16631
16700
|
reassemble(dataType, datagramBody) {
|
|
16632
16701
|
const pending = this.pendingByDataType.get(dataType);
|
|
16633
16702
|
let body = pending ? Buffer.concat([pending.buf, datagramBody]) : datagramBody;
|
|
@@ -16651,6 +16720,9 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16651
16720
|
this.handleFrame(header, payload.subarray(0, header.bytesToRead), dataType);
|
|
16652
16721
|
body = body.subarray(P2P_DATA_HEADER_BYTES + header.bytesToRead);
|
|
16653
16722
|
}
|
|
16723
|
+
if (body.length > 0 && body.length < P2P_DATA_HEADER_BYTES && MAGIC_WORD.startsWith(body.subarray(0, 4).toString())) {
|
|
16724
|
+
this.pendingByDataType.set(dataType, { buf: body });
|
|
16725
|
+
}
|
|
16654
16726
|
}
|
|
16655
16727
|
/**
|
|
16656
16728
|
* Forget where each data type's sequence numbering had reached, and drop any half-reassembled frame.
|
|
@@ -16735,7 +16807,8 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16735
16807
|
this.emit("error", e instanceof Error ? e : new Error(String(e)));
|
|
16736
16808
|
}
|
|
16737
16809
|
}
|
|
16738
|
-
|
|
16810
|
+
const paddedResult = header.commandId === CMD_SET_PAYLOAD && data.length === PADDED_RESULT_BYTES && data.subarray(4).every((b) => b === 0);
|
|
16811
|
+
if (!isMedia && !frame.json && (data.length === 4 || paddedResult)) {
|
|
16739
16812
|
this.emit("commandResult", { code: data.readInt32LE(0), channel: header.channel });
|
|
16740
16813
|
}
|
|
16741
16814
|
this.emit("data", frame);
|
|
@@ -18164,9 +18237,9 @@ function annexbToJpeg(annexb, spawn2) {
|
|
|
18164
18237
|
|
|
18165
18238
|
// dist/transport/p2p/shared-live-source.js
|
|
18166
18239
|
import { EventEmitter as EventEmitter4 } from "node:events";
|
|
18167
|
-
|
|
18168
|
-
|
|
18169
|
-
return Number.isFinite(
|
|
18240
|
+
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
18241
|
+
function timerMs(value, fallback) {
|
|
18242
|
+
return value !== void 0 && Number.isFinite(value) && value >= 1 && value <= MAX_TIMER_DELAY_MS ? value : fallback;
|
|
18170
18243
|
}
|
|
18171
18244
|
function sameConfig(a, b) {
|
|
18172
18245
|
return b !== void 0 && a.codec === b.codec && a.width === b.width && a.height === b.height;
|
|
@@ -18419,14 +18492,14 @@ var SharedLiveSource = class {
|
|
|
18419
18492
|
tag;
|
|
18420
18493
|
constructor(opts) {
|
|
18421
18494
|
this.opts = opts;
|
|
18422
|
-
this.lingerMs = opts.lingerMs
|
|
18495
|
+
this.lingerMs = opts.lingerMs === 0 ? 1 : timerMs(opts.lingerMs, 8e3);
|
|
18423
18496
|
this.maxQueue = opts.maxQueue ?? 900;
|
|
18424
|
-
this.preBufferMs =
|
|
18425
|
-
this.warmRetryMs = opts.warmRetryMs
|
|
18426
|
-
this.warmTimeoutMs = opts.warmTimeoutMs
|
|
18497
|
+
this.preBufferMs = timerMs((opts.preBufferSeconds ?? 0) * 1e3, 0);
|
|
18498
|
+
this.warmRetryMs = timerMs(opts.warmRetryMs, 2e3);
|
|
18499
|
+
this.warmTimeoutMs = timerMs(opts.warmTimeoutMs, 2e4);
|
|
18427
18500
|
this.powered = opts.powered ?? "wired";
|
|
18428
|
-
this.batteryBudgetMs = opts.batteryBudgetMs
|
|
18429
|
-
this.budgetGraceMs = opts.budgetGraceMs
|
|
18501
|
+
this.batteryBudgetMs = timerMs(opts.batteryBudgetMs, 45e3);
|
|
18502
|
+
this.budgetGraceMs = timerMs(opts.budgetGraceMs, 1e4);
|
|
18430
18503
|
this.logger = opts.logger ?? noopLogger;
|
|
18431
18504
|
this.tag = opts.label ? `[live ${opts.label}]` : "[live]";
|
|
18432
18505
|
this.traceId = `pull-${++pullSequence}`;
|
|
@@ -18674,7 +18747,7 @@ var SharedLiveSource = class {
|
|
|
18674
18747
|
if (this.disposed || !this.stream)
|
|
18675
18748
|
return;
|
|
18676
18749
|
this.clearBudget();
|
|
18677
|
-
this.budgetTimer.arm(ms
|
|
18750
|
+
this.budgetTimer.arm(timerMs(ms, this.batteryBudgetMs), () => this.onBudgetExpire());
|
|
18678
18751
|
}
|
|
18679
18752
|
/** Settle a reuse watch on any frame — the join already holds a decodable picture. */
|
|
18680
18753
|
settleReuseWatch() {
|
|
@@ -18815,7 +18888,7 @@ var SharedLiveSource = class {
|
|
|
18815
18888
|
return this.bufferedMedia(seconds).filter((item) => item.kind === "video").map((item) => item.frame);
|
|
18816
18889
|
}
|
|
18817
18890
|
bufferedMedia(seconds) {
|
|
18818
|
-
const requested = Math.min(
|
|
18891
|
+
const requested = Number.isFinite(seconds) ? Math.min(seconds * 1e3, this.preBufferMs) : 0;
|
|
18819
18892
|
if (requested <= 0 || !this.ring.length)
|
|
18820
18893
|
return [];
|
|
18821
18894
|
const start = this.windowStart(Date.now() - requested);
|
|
@@ -26837,7 +26910,7 @@ var EufyMega = class extends EventEmitter9 {
|
|
|
26837
26910
|
if (ev.eventName === void 0 && ev.eventType != null)
|
|
26838
26911
|
ev.eventName = detectionName(ev.eventType);
|
|
26839
26912
|
if (ev.thumbnailCandidate)
|
|
26840
|
-
void this.observeStoredImage(ev.thumbnailCandidate);
|
|
26913
|
+
void this.observeStoredImage(ev.thumbnailCandidate, ev.deviceSn);
|
|
26841
26914
|
this.emit("push", ev);
|
|
26842
26915
|
const signal2 = {
|
|
26843
26916
|
source: "push",
|
|
@@ -26886,9 +26959,16 @@ var EufyMega = class extends EventEmitter9 {
|
|
|
26886
26959
|
client.on("error", (e) => this.reportError(e));
|
|
26887
26960
|
return client;
|
|
26888
26961
|
}
|
|
26889
|
-
/**
|
|
26890
|
-
|
|
26891
|
-
|
|
26962
|
+
/**
|
|
26963
|
+
* Admit only exact, account-known devices with resolved snapshot evidence into the passive store; a
|
|
26964
|
+
* station-only candidate is admitted under the event's device serial.
|
|
26965
|
+
*/
|
|
26966
|
+
async observeStoredImage(candidate, eventDeviceSn) {
|
|
26967
|
+
if (!this.storedImages)
|
|
26968
|
+
return;
|
|
26969
|
+
const { attribution } = candidate;
|
|
26970
|
+
const deviceSn = attribution.kind === "device" ? attribution.deviceSn : attribution.kind === "station" ? eventDeviceSn : void 0;
|
|
26971
|
+
if (!deviceSn)
|
|
26892
26972
|
return;
|
|
26893
26973
|
const account = this.mega.auth?.userId;
|
|
26894
26974
|
if (!account)
|
|
@@ -26898,9 +26978,9 @@ var EufyMega = class extends EventEmitter9 {
|
|
|
26898
26978
|
await this.registry.getDevices();
|
|
26899
26979
|
if (this.mega.auth?.userId !== account)
|
|
26900
26980
|
return;
|
|
26901
|
-
const caps = this.registry.capabilitiesForDevice(
|
|
26981
|
+
const caps = this.registry.capabilitiesForDevice(deviceSn);
|
|
26902
26982
|
if (caps && hasProvidedAction(caps, "snapshotStored")) {
|
|
26903
|
-
this.storedImages.observe(
|
|
26983
|
+
this.storedImages.observe(deviceSn, candidate.url);
|
|
26904
26984
|
}
|
|
26905
26985
|
} catch (e) {
|
|
26906
26986
|
this.reportError(e);
|
|
@@ -27125,26 +27205,16 @@ var SolixClient = class _SolixClient {
|
|
|
27125
27205
|
}
|
|
27126
27206
|
/** Build the encrypted, signed `/passport/login` request body + headers for the negotiated key. */
|
|
27127
27207
|
async postLogin(kx, verifyCode, limitedToken) {
|
|
27128
|
-
const { clientPublicKeyHex, encryptedPassword } = encryptLoginPassword(this.password);
|
|
27129
27208
|
const bodyObj = {
|
|
27130
|
-
email
|
|
27131
|
-
password: encryptedPassword,
|
|
27132
|
-
ab: this.country,
|
|
27133
|
-
client_secret_info: { public_key: clientPublicKeyHex },
|
|
27209
|
+
...loginCredentials(this.email, this.password, this.country),
|
|
27134
27210
|
answer: "",
|
|
27135
27211
|
captcha_id: "",
|
|
27136
27212
|
verify_code: verifyCode ?? "",
|
|
27137
27213
|
login_id: ""
|
|
27138
27214
|
};
|
|
27139
27215
|
const encBody = encryptBody(JSON.stringify(bodyObj), kx.shareKey);
|
|
27140
|
-
const ts = nowSec();
|
|
27141
|
-
const once = genId();
|
|
27142
27216
|
return this.post(this.apiHost, SOLIX_ENDPOINTS.login, encBody, this.authHeaders({
|
|
27143
|
-
|
|
27144
|
-
"x-key-ident": kx.keyIdent,
|
|
27145
|
-
"x-request-ts": ts,
|
|
27146
|
-
"x-request-once": once,
|
|
27147
|
-
"x-signature": signRequest(kx.shareKey, ts, once, encBody),
|
|
27217
|
+
...signedHeaders(kx, encBody),
|
|
27148
27218
|
...limitedToken ? { "x-auth-token": limitedToken } : {}
|
|
27149
27219
|
}));
|
|
27150
27220
|
}
|
|
@@ -27159,13 +27229,12 @@ var SolixClient = class _SolixClient {
|
|
|
27159
27229
|
* observed refusing the header, which is consistent with the two ids agreeing on the accounts seen.
|
|
27160
27230
|
*/
|
|
27161
27231
|
classifyLogin(data, isVerify) {
|
|
27162
|
-
const
|
|
27163
|
-
|
|
27164
|
-
if (!userId || !authToken)
|
|
27232
|
+
const reply = readLoginReply(data);
|
|
27233
|
+
if (!reply)
|
|
27165
27234
|
throw new Error(`Solix login returned no session: ${JSON.stringify(data).slice(0, 160)}`);
|
|
27166
|
-
const
|
|
27167
|
-
if (!isVerify &&
|
|
27168
|
-
this.pending2fa = { limitedToken: authToken, userId, geoKey:
|
|
27235
|
+
const { userId, authToken } = reply;
|
|
27236
|
+
if (!isVerify && reply.twoFactorPending) {
|
|
27237
|
+
this.pending2fa = { limitedToken: authToken, userId, geoKey: reply.geoKey };
|
|
27169
27238
|
return { status: "2fa", method: "code sent by the passport" };
|
|
27170
27239
|
}
|
|
27171
27240
|
this.pending2fa = void 0;
|
|
@@ -27174,7 +27243,7 @@ var SolixClient = class _SolixClient {
|
|
|
27174
27243
|
userId,
|
|
27175
27244
|
gtoken: gtoken(userId),
|
|
27176
27245
|
apiHost: this.apiHost,
|
|
27177
|
-
tokenExpiresAt:
|
|
27246
|
+
tokenExpiresAt: reply.tokenExpiresAt
|
|
27178
27247
|
};
|
|
27179
27248
|
this.persist();
|
|
27180
27249
|
return { status: "ok", session: this.session_ };
|
|
@@ -27329,14 +27398,8 @@ var SolixClient = class _SolixClient {
|
|
|
27329
27398
|
throw new Error("not authenticated \u2014 call login() first");
|
|
27330
27399
|
const kx = await this.keyExchange();
|
|
27331
27400
|
const encBody = encryptBody(JSON.stringify(payload), kx.shareKey);
|
|
27332
|
-
const ts = nowSec();
|
|
27333
|
-
const once = genId();
|
|
27334
27401
|
const env = await this.post(this.apiHost, path, encBody, this.baseHeaders({
|
|
27335
|
-
|
|
27336
|
-
"x-key-ident": kx.keyIdent,
|
|
27337
|
-
"x-request-ts": ts,
|
|
27338
|
-
"x-request-once": once,
|
|
27339
|
-
"x-signature": signRequest(kx.shareKey, ts, once, encBody),
|
|
27402
|
+
...signedHeaders(kx, encBody),
|
|
27340
27403
|
"x-auth-token": this.session_.authToken,
|
|
27341
27404
|
gtoken: this.session_.gtoken
|
|
27342
27405
|
}));
|