@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/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
- "x-encryption-info": "algo_ecdh",
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 { clientPublicKeyHex, encryptedPassword } = encryptLoginPassword(this.cfg.password);
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 userId = res.ap_cloud_user_id ?? res.user_id ?? res.userId;
1994
- const accountUserId = res.user_id ?? res.userId;
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 faInfo = res.fa_info ?? {};
1999
- const needs2fa = !isVerify && !!faInfo.info;
2000
- if (needs2fa) {
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: res.geo_key };
2010
- this.tokenExpiresAt = Number(res.token_expires_at ?? 0) || 0;
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 answers a policy-denied filter with a
2233
- * SUBACK_FAILURE (`0x80`) grant rather than failing the SUBSCRIBE, so subscribing with a credential
2234
- * whose scope doesn't cover the topic looks identical to success and then delivers nothing. A denied
2235
- * topic is reported via `error` naming the credential scope; only an all-denied device throws, so a
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 } = this.partitionGrants(await this.client.subscribeAsync(topics, { qos: 1 }));
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
- * comes back with SUBACK_FAILURE rather than an error (AWS IoT quirk), so it is dropped from the result
2254
- * instead of throwing — callers that need every leg check the returned list. Used by lines whose topic
2255
- * vocabulary isn't the eufy `subscribeTopics` shape (e.g. Anker Solix `dt/{app}/{pn}/{sn}`).
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
- if (!this.client)
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
- * Split SUBACK grants into granted vs scope-denied topics. AWS IoT marks a policy-denied filter with a
2264
- * SUBACK_FAILURE (`0x80`) grant rather than failing the SUBSCRIBE, so the two subscribe paths share
2265
- * this split and layer their own policy (drop vs report) on top.
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
- partitionGrants(grants) {
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
- for (const g of grants)
2271
- (g.qos === SUBACK_FAILURE ? denied : granted).push(g.topic);
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 u16le(n) {
2396
- const b = Buffer.alloc(2);
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
- var COMMAND_OBSERVATION = /* @__PURE__ */ Symbol("command-observation");
2978
- function observeCommand(command, observation) {
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
- function commandObservation(command) {
2982
- return command[COMMAND_OBSERVATION];
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
- /** In-flight multi-datagram frame per data channel (see onData). */
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
- /** Reassemble one in-sequence datagram body into logical frames. */
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
- if (!isMedia && !frame.json && data.length === 4) {
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
- function durationMs(seconds) {
18168
- const milliseconds = (seconds ?? 0) * 1e3;
18169
- return Number.isFinite(milliseconds) && milliseconds > 0 ? milliseconds : 0;
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 ?? 8e3;
18495
+ this.lingerMs = opts.lingerMs === 0 ? 1 : timerMs(opts.lingerMs, 8e3);
18423
18496
  this.maxQueue = opts.maxQueue ?? 900;
18424
- this.preBufferMs = durationMs(opts.preBufferSeconds);
18425
- this.warmRetryMs = opts.warmRetryMs ?? 2e3;
18426
- this.warmTimeoutMs = opts.warmTimeoutMs ?? 2e4;
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 ?? 45e3;
18429
- this.budgetGraceMs = opts.budgetGraceMs ?? 1e4;
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 ?? this.batteryBudgetMs, () => this.onBudgetExpire());
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(durationMs(seconds), this.preBufferMs);
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
- /** Admit only exact, account-known devices with resolved snapshot evidence into the passive store. */
26890
- async observeStoredImage(candidate) {
26891
- if (!this.storedImages || candidate.attribution.kind !== "device")
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(candidate.attribution.deviceSn);
26981
+ const caps = this.registry.capabilitiesForDevice(deviceSn);
26902
26982
  if (caps && hasProvidedAction(caps, "snapshotStored")) {
26903
- this.storedImages.observe(candidate.attribution.deviceSn, candidate.url);
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: this.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
- "x-encryption-info": "algo_ecdh",
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 userId = data.ap_cloud_user_id ?? data.user_id;
27163
- const authToken = data.auth_token;
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 faInfo = data.fa_info ?? {};
27167
- if (!isVerify && faInfo.info) {
27168
- this.pending2fa = { limitedToken: authToken, userId, geoKey: data.geo_key };
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: Number(data.token_expires_at ?? 0) || 0
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
- "x-encryption-info": "algo_ecdh",
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
  }));