@mega-yfue/eufy-sdk 0.4.0-beta.3 → 0.4.0-beta.30
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 +22 -3
- package/dist/client/types.d.ts +25 -0
- package/dist/core/contracts.d.ts +45 -2
- package/dist/index.js +1087 -577
- package/dist/index.js.map +4 -4
- package/dist/model/capabilities/camera.d.ts +1 -0
- package/dist/model/capabilities/doorbell.d.ts +8 -9
- package/dist/model/capabilities/index.d.ts +1 -1
- package/dist/model/capabilities/manifest.d.ts +18 -2
- package/dist/model/capabilities/motion.d.ts +3 -4
- package/dist/model/capabilities/siren.d.ts +1 -1
- package/dist/model/param-dictionary.d.ts +3 -3
- package/dist/model/types.d.ts +10 -11
- 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/codec.d.ts +13 -0
- package/dist/transport/p2p/command-router.d.ts +36 -6
- package/dist/transport/p2p/live-trace.d.ts +5 -0
- package/dist/transport/p2p/p2p-session.d.ts +50 -12
- package/dist/transport/p2p/recording-download.d.ts +54 -0
- package/dist/transport/p2p/shared-live-source.d.ts +1 -1
- package/dist/transport/p2p/video.d.ts +2 -0
- package/dist/transport/stored-image-cache.d.ts +2 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1070,6 +1070,409 @@ 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 RecordingDownloadError = class extends Error {
|
|
1151
|
+
reason;
|
|
1152
|
+
constructor(reason, message, options) {
|
|
1153
|
+
super(message, options);
|
|
1154
|
+
this.reason = reason;
|
|
1155
|
+
this.name = "RecordingDownloadError";
|
|
1156
|
+
}
|
|
1157
|
+
};
|
|
1158
|
+
var LiveStreamStartError = class extends Error {
|
|
1159
|
+
reason;
|
|
1160
|
+
stage;
|
|
1161
|
+
timeoutMs;
|
|
1162
|
+
attempts;
|
|
1163
|
+
constructor(failure) {
|
|
1164
|
+
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 });
|
|
1165
|
+
this.name = "LiveStreamStartError";
|
|
1166
|
+
this.reason = failure.reason;
|
|
1167
|
+
this.stage = failure.stage;
|
|
1168
|
+
this.timeoutMs = failure.timeoutMs;
|
|
1169
|
+
this.attempts = failure.attempts;
|
|
1170
|
+
}
|
|
1171
|
+
};
|
|
1172
|
+
var StateConvergenceError = class extends Error {
|
|
1173
|
+
sn;
|
|
1174
|
+
property;
|
|
1175
|
+
param;
|
|
1176
|
+
expected;
|
|
1177
|
+
observed;
|
|
1178
|
+
timeoutMs;
|
|
1179
|
+
constructor(failure) {
|
|
1180
|
+
super(`device ${failure.sn} did not report ${failure.property} as ${String(failure.expected)} within ${failure.timeoutMs}ms (param ${failure.param} read ${String(failure.observed)})`);
|
|
1181
|
+
this.name = "StateConvergenceError";
|
|
1182
|
+
this.sn = failure.sn;
|
|
1183
|
+
this.property = failure.property;
|
|
1184
|
+
this.param = failure.param;
|
|
1185
|
+
this.expected = failure.expected;
|
|
1186
|
+
this.observed = failure.observed;
|
|
1187
|
+
this.timeoutMs = failure.timeoutMs;
|
|
1188
|
+
}
|
|
1189
|
+
};
|
|
1190
|
+
var COMMAND_OBSERVATION = /* @__PURE__ */ Symbol("command-observation");
|
|
1191
|
+
function observeCommand(command, observation) {
|
|
1192
|
+
return Object.defineProperty(command, COMMAND_OBSERVATION, { configurable: true, value: observation });
|
|
1193
|
+
}
|
|
1194
|
+
function commandObservation(command) {
|
|
1195
|
+
return command[COMMAND_OBSERVATION];
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// dist/core/raw-dp-writer.js
|
|
1199
|
+
var WIRE = { VARINT: 0, LENGTH_DELIMITED: 2 };
|
|
1200
|
+
function encodeVarint(value) {
|
|
1201
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
1202
|
+
throw new Error(`varint: ${value} is not a non-negative safe integer`);
|
|
1203
|
+
}
|
|
1204
|
+
const out = [];
|
|
1205
|
+
let v = value;
|
|
1206
|
+
while (v > 127) {
|
|
1207
|
+
out.push(v % 128 + 128);
|
|
1208
|
+
v = Math.floor(v / 128);
|
|
1209
|
+
}
|
|
1210
|
+
out.push(v);
|
|
1211
|
+
return out;
|
|
1212
|
+
}
|
|
1213
|
+
function zigzag(value) {
|
|
1214
|
+
if (!Number.isSafeInteger(value))
|
|
1215
|
+
throw new Error(`zigzag: ${value} is not a safe integer`);
|
|
1216
|
+
return value < 0 ? -2 * value - 1 : 2 * value;
|
|
1217
|
+
}
|
|
1218
|
+
var RawDpWriter = class _RawDpWriter {
|
|
1219
|
+
body = [];
|
|
1220
|
+
/** A varint-valued field (`int32`, `uint32`, `bool`, an enum member). */
|
|
1221
|
+
int(field, value) {
|
|
1222
|
+
this.body.push(...encodeVarint(tag(field, WIRE.VARINT)), ...encodeVarint(value));
|
|
1223
|
+
return this;
|
|
1224
|
+
}
|
|
1225
|
+
/** A signed varint field (`sint32`, `sint64`), ZigZag-encoded — see {@link zigzag}. */
|
|
1226
|
+
sint(field, value) {
|
|
1227
|
+
return this.int(field, zigzag(value));
|
|
1228
|
+
}
|
|
1229
|
+
/** A boolean field, on protobuf's own terms: `1` or `0` in a varint. */
|
|
1230
|
+
bool(field, value) {
|
|
1231
|
+
return this.int(field, value ? 1 : 0);
|
|
1232
|
+
}
|
|
1233
|
+
/** A length-delimited field carrying raw bytes — a string, a blob, or a message built elsewhere. */
|
|
1234
|
+
bytes(field, value) {
|
|
1235
|
+
const bytes2 = value instanceof Uint8Array ? Array.from(value) : [...value];
|
|
1236
|
+
this.body.push(...encodeVarint(tag(field, WIRE.LENGTH_DELIMITED)), ...encodeVarint(bytes2.length), ...bytes2);
|
|
1237
|
+
return this;
|
|
1238
|
+
}
|
|
1239
|
+
/**
|
|
1240
|
+
* A nested sub-message, built by `build` into a writer of its own.
|
|
1241
|
+
*
|
|
1242
|
+
* The sub-message is emitted even when `build` writes nothing into it. That is not an oversight: an
|
|
1243
|
+
* EMPTY sub-message is how these protocols say "this subsystem exists and is in its zero state", as
|
|
1244
|
+
* opposed to saying nothing about it, and the readers on the other side of this file depend on the
|
|
1245
|
+
* distinction. Omitting the call is what says nothing about it.
|
|
1246
|
+
*/
|
|
1247
|
+
sub(field, build) {
|
|
1248
|
+
const inner = new _RawDpWriter();
|
|
1249
|
+
build(inner);
|
|
1250
|
+
return this.bytes(field, inner.toBytes());
|
|
1251
|
+
}
|
|
1252
|
+
/** The message body on its own, unframed — for nesting into another writer. */
|
|
1253
|
+
toBytes() {
|
|
1254
|
+
return [...this.body];
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1257
|
+
* The finished DP value: `varint(bodyLength) ++ body`, base64-encoded.
|
|
1258
|
+
*
|
|
1259
|
+
* This is the envelope `RawDpCodec.decode` unwraps, so a value produced here and read back through the
|
|
1260
|
+
* codec is a genuine round-trip rather than two implementations of the same guess.
|
|
1261
|
+
*/
|
|
1262
|
+
finish() {
|
|
1263
|
+
return Buffer.from([...encodeVarint(this.body.length), ...this.body]).toString("base64");
|
|
1264
|
+
}
|
|
1265
|
+
};
|
|
1266
|
+
function tag(field, wire) {
|
|
1267
|
+
if (!Number.isSafeInteger(field) || field < 1)
|
|
1268
|
+
throw new Error(`field number: ${field} is not a positive integer`);
|
|
1269
|
+
return field * 8 + wire;
|
|
1270
|
+
}
|
|
1271
|
+
function rawDp(build) {
|
|
1272
|
+
const w = new RawDpWriter();
|
|
1273
|
+
build(w);
|
|
1274
|
+
return w.finish();
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
// dist/core/raw-dp-hex.js
|
|
1278
|
+
function hexToRawDp(hex) {
|
|
1279
|
+
if (hex.length === 0 || hex.length % 2 !== 0 || !/^[0-9a-fA-F]+$/.test(hex))
|
|
1280
|
+
return void 0;
|
|
1281
|
+
return Buffer.from(hex, "hex").toString("base64");
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
// dist/core/util.js
|
|
1285
|
+
var Timer = class {
|
|
1286
|
+
handle;
|
|
1287
|
+
/** (Re)arm to run `fn` after `ms`, cancelling any already-pending fire. */
|
|
1288
|
+
arm(ms, fn) {
|
|
1289
|
+
this.cancel();
|
|
1290
|
+
this.handle = setTimeout(() => {
|
|
1291
|
+
this.handle = void 0;
|
|
1292
|
+
fn();
|
|
1293
|
+
}, ms);
|
|
1294
|
+
this.handle.unref?.();
|
|
1295
|
+
}
|
|
1296
|
+
/** Cancel a pending fire. No-op if not armed. */
|
|
1297
|
+
cancel() {
|
|
1298
|
+
if (this.handle) {
|
|
1299
|
+
clearTimeout(this.handle);
|
|
1300
|
+
this.handle = void 0;
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
/** Whether a fire is currently scheduled. */
|
|
1304
|
+
get pending() {
|
|
1305
|
+
return this.handle !== void 0;
|
|
1306
|
+
}
|
|
1307
|
+
};
|
|
1308
|
+
function assertNever(value) {
|
|
1309
|
+
throw new Error(`unhandled variant: ${JSON.stringify(value)}`);
|
|
1310
|
+
}
|
|
1311
|
+
function asBool(value) {
|
|
1312
|
+
if (typeof value === "string")
|
|
1313
|
+
return value === "1" || value.toLowerCase() === "true";
|
|
1314
|
+
return value === true || value === 1;
|
|
1315
|
+
}
|
|
1316
|
+
function clamp(value, min, max) {
|
|
1317
|
+
if (Number.isNaN(value))
|
|
1318
|
+
return min;
|
|
1319
|
+
return Math.max(min, Math.min(max, Math.round(value)));
|
|
1320
|
+
}
|
|
1321
|
+
function structuralEqual(a, b) {
|
|
1322
|
+
if (Object.is(a, b))
|
|
1323
|
+
return true;
|
|
1324
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null)
|
|
1325
|
+
return false;
|
|
1326
|
+
const aArr = Array.isArray(a);
|
|
1327
|
+
const bArr = Array.isArray(b);
|
|
1328
|
+
if (aArr !== bArr)
|
|
1329
|
+
return false;
|
|
1330
|
+
if (aArr && bArr) {
|
|
1331
|
+
if (a.length !== b.length)
|
|
1332
|
+
return false;
|
|
1333
|
+
for (let i = 0; i < a.length; i++)
|
|
1334
|
+
if (!structuralEqual(a[i], b[i]))
|
|
1335
|
+
return false;
|
|
1336
|
+
return true;
|
|
1337
|
+
}
|
|
1338
|
+
const aObj = a;
|
|
1339
|
+
const bObj = b;
|
|
1340
|
+
const aKeys = Object.keys(aObj);
|
|
1341
|
+
const bKeys = Object.keys(bObj);
|
|
1342
|
+
if (aKeys.length !== bKeys.length)
|
|
1343
|
+
return false;
|
|
1344
|
+
for (const k of aKeys) {
|
|
1345
|
+
if (!Object.prototype.hasOwnProperty.call(bObj, k))
|
|
1346
|
+
return false;
|
|
1347
|
+
if (!structuralEqual(aObj[k], bObj[k]))
|
|
1348
|
+
return false;
|
|
1349
|
+
}
|
|
1350
|
+
return true;
|
|
1351
|
+
}
|
|
1352
|
+
function coerceEnumValue(enumObj, value) {
|
|
1353
|
+
const n = Number(value);
|
|
1354
|
+
return Number.isInteger(n) && Object.values(enumObj).includes(n) ? n : void 0;
|
|
1355
|
+
}
|
|
1356
|
+
function enumValues(enumObj) {
|
|
1357
|
+
return Object.values(enumObj).join("/");
|
|
1358
|
+
}
|
|
1359
|
+
function enumLabels(enumObj) {
|
|
1360
|
+
return Object.fromEntries(Object.entries(enumObj).map(([name, value]) => [value, name]));
|
|
1361
|
+
}
|
|
1362
|
+
function jsonObject(text2) {
|
|
1363
|
+
if (typeof text2 !== "string")
|
|
1364
|
+
return void 0;
|
|
1365
|
+
try {
|
|
1366
|
+
const v = JSON.parse(text2);
|
|
1367
|
+
return v && typeof v === "object" && !Array.isArray(v) ? v : void 0;
|
|
1368
|
+
} catch {
|
|
1369
|
+
return void 0;
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
function u16le(n) {
|
|
1373
|
+
const b = Buffer.alloc(2);
|
|
1374
|
+
b.writeUInt16LE(n & 65535);
|
|
1375
|
+
return b;
|
|
1376
|
+
}
|
|
1377
|
+
function u16be(n) {
|
|
1378
|
+
const b = Buffer.alloc(2);
|
|
1379
|
+
b.writeUInt16BE(n & 65535);
|
|
1380
|
+
return b;
|
|
1381
|
+
}
|
|
1382
|
+
function u32le(n) {
|
|
1383
|
+
const b = Buffer.alloc(4);
|
|
1384
|
+
b.writeUInt32LE(n >>> 0);
|
|
1385
|
+
return b;
|
|
1386
|
+
}
|
|
1387
|
+
function u32be(n) {
|
|
1388
|
+
const b = Buffer.alloc(4);
|
|
1389
|
+
b.writeUInt32BE(n >>> 0);
|
|
1390
|
+
return b;
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
// dist/core/lz4-block.js
|
|
1394
|
+
var MAX_OUTPUT = 64 * 1024 * 1024;
|
|
1395
|
+
function lz4BlockDecompress(block, size) {
|
|
1396
|
+
if (!Number.isSafeInteger(size) || size < 0 || size > MAX_OUTPUT)
|
|
1397
|
+
return void 0;
|
|
1398
|
+
const out = Buffer.alloc(size);
|
|
1399
|
+
const end = block.length;
|
|
1400
|
+
let pos = 0;
|
|
1401
|
+
let written = 0;
|
|
1402
|
+
const extend2 = (base) => {
|
|
1403
|
+
let len = base;
|
|
1404
|
+
if (base !== 15)
|
|
1405
|
+
return len;
|
|
1406
|
+
for (; ; ) {
|
|
1407
|
+
if (pos >= end)
|
|
1408
|
+
return void 0;
|
|
1409
|
+
const extra = block[pos++];
|
|
1410
|
+
len += extra;
|
|
1411
|
+
if (extra !== 255)
|
|
1412
|
+
return len;
|
|
1413
|
+
}
|
|
1414
|
+
};
|
|
1415
|
+
while (pos < end) {
|
|
1416
|
+
const token = block[pos++];
|
|
1417
|
+
const literals = extend2(token >> 4);
|
|
1418
|
+
if (literals === void 0)
|
|
1419
|
+
return void 0;
|
|
1420
|
+
if (pos + literals > end || written + literals > size)
|
|
1421
|
+
return void 0;
|
|
1422
|
+
for (let i = 0; i < literals; i++)
|
|
1423
|
+
out[written++] = block[pos++];
|
|
1424
|
+
if (pos === end)
|
|
1425
|
+
break;
|
|
1426
|
+
if (pos + 2 > end)
|
|
1427
|
+
return void 0;
|
|
1428
|
+
const offset = block[pos] | block[pos + 1] << 8;
|
|
1429
|
+
pos += 2;
|
|
1430
|
+
if (offset === 0 || offset > written)
|
|
1431
|
+
return void 0;
|
|
1432
|
+
const match = extend2(token & 15);
|
|
1433
|
+
if (match === void 0)
|
|
1434
|
+
return void 0;
|
|
1435
|
+
const length = match + 4;
|
|
1436
|
+
if (written + length > size)
|
|
1437
|
+
return void 0;
|
|
1438
|
+
let from = written - offset;
|
|
1439
|
+
for (let i = 0; i < length; i++)
|
|
1440
|
+
out[written++] = out[from++];
|
|
1441
|
+
}
|
|
1442
|
+
return written === size ? out : void 0;
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
// dist/transport/http/passport.js
|
|
1446
|
+
function loginCredentials(email, password, country) {
|
|
1447
|
+
const { clientPublicKeyHex, encryptedPassword } = encryptLoginPassword(password);
|
|
1448
|
+
return { email, password: encryptedPassword, ab: country, client_secret_info: { public_key: clientPublicKeyHex } };
|
|
1449
|
+
}
|
|
1450
|
+
function readLoginReply(data) {
|
|
1451
|
+
const userId = data.ap_cloud_user_id ?? data.user_id ?? data.userId;
|
|
1452
|
+
const authToken = data.auth_token ?? data.token;
|
|
1453
|
+
if (!userId || !authToken)
|
|
1454
|
+
return void 0;
|
|
1455
|
+
return {
|
|
1456
|
+
userId,
|
|
1457
|
+
accountUserId: data.user_id ?? data.userId,
|
|
1458
|
+
authToken,
|
|
1459
|
+
geoKey: data.geo_key,
|
|
1460
|
+
tokenExpiresAt: Number(data.token_expires_at ?? 0) || 0,
|
|
1461
|
+
twoFactorPending: !!(data.fa_info ?? {}).info
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
function signedHeaders(entry, encBody) {
|
|
1465
|
+
const ts = nowSec();
|
|
1466
|
+
const once = genId();
|
|
1467
|
+
return {
|
|
1468
|
+
"x-encryption-info": "algo_ecdh",
|
|
1469
|
+
"x-key-ident": entry.keyIdent,
|
|
1470
|
+
"x-request-ts": ts,
|
|
1471
|
+
"x-request-once": once,
|
|
1472
|
+
"x-signature": signRequest(entry.shareKey, ts, once, encBody)
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1073
1476
|
// dist/transport/http/mega-client.js
|
|
1074
1477
|
function parseMaybeJson(text2) {
|
|
1075
1478
|
if (text2.length === 0)
|
|
@@ -1437,17 +1840,11 @@ ${this.auth_?.authToken ?? ""}`;
|
|
|
1437
1840
|
const order = preferred === "text/plain" ? ["text/plain", "application/json"] : ["application/json", "text/plain"];
|
|
1438
1841
|
let last;
|
|
1439
1842
|
for (const contentType of order) {
|
|
1440
|
-
const ts = nowSec();
|
|
1441
|
-
const once = genId();
|
|
1442
1843
|
const headers = {
|
|
1443
1844
|
...this.baseHeaders(),
|
|
1444
1845
|
"content-type": contentType,
|
|
1445
|
-
|
|
1846
|
+
...signedHeaders(entry, encBody),
|
|
1446
1847
|
"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
1848
|
// Per-call header overrides (e.g. app-name=eufy_security to request a security-scoped
|
|
1452
1849
|
// MQTT cert on an existing eufy_mega session — see EufyMega.getUserMqttInfo).
|
|
1453
1850
|
...headerOverrides
|
|
@@ -1746,13 +2143,7 @@ ${this.auth_?.authToken ?? ""}`;
|
|
|
1746
2143
|
}
|
|
1747
2144
|
/** Build the /passport/login body (verify_code empty unless 2FA). */
|
|
1748
2145
|
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
|
-
};
|
|
2146
|
+
const base = loginCredentials(this.cfg.email, this.cfg.password, this.cfg.countryCode);
|
|
1756
2147
|
if (opts.verifyCode)
|
|
1757
2148
|
return { ...base, verify_code: opts.verifyCode, login_id: "" };
|
|
1758
2149
|
if (opts.captchaId)
|
|
@@ -1990,15 +2381,12 @@ ${this.auth_?.authToken ?? ""}`;
|
|
|
1990
2381
|
if (cap.length)
|
|
1991
2382
|
this.logger.debug("[mega] captcha/fa:", JSON.stringify(Object.fromEntries(cap.map((k) => [k, res[k]]))));
|
|
1992
2383
|
}
|
|
1993
|
-
const
|
|
1994
|
-
|
|
1995
|
-
const authToken = res.auth_token ?? res.token;
|
|
1996
|
-
if (!userId || !authToken)
|
|
2384
|
+
const reply = readLoginReply(res);
|
|
2385
|
+
if (!reply)
|
|
1997
2386
|
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 };
|
|
2387
|
+
const { userId, accountUserId, authToken, geoKey } = reply;
|
|
2388
|
+
if (!isVerify && reply.twoFactorPending) {
|
|
2389
|
+
this.auth_ = { userId, accountUserId, authToken, geoKey };
|
|
2002
2390
|
this.pendingCaptchaId = void 0;
|
|
2003
2391
|
this.pending2fa = true;
|
|
2004
2392
|
await this.sendVerifyCode(messageType);
|
|
@@ -2006,8 +2394,8 @@ ${this.auth_?.authToken ?? ""}`;
|
|
|
2006
2394
|
}
|
|
2007
2395
|
this.pendingCaptchaId = void 0;
|
|
2008
2396
|
this.pending2fa = false;
|
|
2009
|
-
this.auth_ = { userId, accountUserId, authToken, geoKey
|
|
2010
|
-
this.tokenExpiresAt =
|
|
2397
|
+
this.auth_ = { userId, accountUserId, authToken, geoKey };
|
|
2398
|
+
this.tokenExpiresAt = reply.tokenExpiresAt;
|
|
2011
2399
|
this.sessionKey = void 0;
|
|
2012
2400
|
await this.ensureSessionKey();
|
|
2013
2401
|
this.persist();
|
|
@@ -2125,6 +2513,10 @@ function solixUserTopics(appName, userId) {
|
|
|
2125
2513
|
|
|
2126
2514
|
// dist/transport/mqtt/secure-mqtt.js
|
|
2127
2515
|
var SUBACK_FAILURE = 128;
|
|
2516
|
+
function isRefusal(err) {
|
|
2517
|
+
const granted = err?.packet?.granted;
|
|
2518
|
+
return Array.isArray(granted) && granted.some((g) => typeof g === "number" && (g & SUBACK_FAILURE) !== 0);
|
|
2519
|
+
}
|
|
2128
2520
|
function isNotAuthorized(err) {
|
|
2129
2521
|
return err instanceof Error && /connection refused/i.test(err.message) && /not authori[sz]ed/i.test(err.message);
|
|
2130
2522
|
}
|
|
@@ -2227,19 +2619,16 @@ var SecureMqtt = class extends EventEmitter {
|
|
|
2227
2619
|
}
|
|
2228
2620
|
/**
|
|
2229
2621
|
* Subscribe every inbound leg this device's line uses (see `topics.ts` — one `/res` for most lines,
|
|
2230
|
-
* four topics for `eufy_life`).
|
|
2622
|
+
* four topics for `eufy_life` and the clean line).
|
|
2231
2623
|
*
|
|
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.
|
|
2624
|
+
* The grants are INSPECTED, not assumed: AWS IoT refuses a policy-denied filter with a
|
|
2625
|
+
* {@link SUBACK_FAILURE} grant rather than failing the connection. A denied topic is reported via
|
|
2626
|
+
* `error` naming the credential scope; only an all-denied device throws, so a line that grants its
|
|
2627
|
+
* state channel but refuses (say) a business leg still works.
|
|
2237
2628
|
*/
|
|
2238
2629
|
async subscribeDevice(device) {
|
|
2239
|
-
if (!this.client)
|
|
2240
|
-
throw new Error("SecureMqtt not connected");
|
|
2241
2630
|
const topics = [...subscribeTopics(device)];
|
|
2242
|
-
const { denied } =
|
|
2631
|
+
const { denied } = await this.subscribeEach(topics);
|
|
2243
2632
|
const scope = this.o.credentials.app_name ?? "default";
|
|
2244
2633
|
if (denied.length === topics.length) {
|
|
2245
2634
|
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 +2639,43 @@ var SecureMqtt = class extends EventEmitter {
|
|
|
2250
2639
|
}
|
|
2251
2640
|
/**
|
|
2252
2641
|
* Subscribe to explicit topic filters, returning the topics that were granted. A scope-denied filter
|
|
2253
|
-
*
|
|
2254
|
-
*
|
|
2255
|
-
*
|
|
2642
|
+
* is dropped from the result instead of throwing — callers that need every leg check the returned
|
|
2643
|
+
* list. Used by lines whose topic vocabulary isn't the eufy `subscribeTopics` shape (e.g. Anker Solix
|
|
2644
|
+
* `dt/{app}/{pn}/{sn}`).
|
|
2256
2645
|
*/
|
|
2257
2646
|
async subscribe(topics) {
|
|
2258
|
-
|
|
2259
|
-
throw new Error("SecureMqtt not connected");
|
|
2260
|
-
return this.partitionGrants(await this.client.subscribeAsync(topics, { qos: 1 })).granted;
|
|
2647
|
+
return (await this.subscribeEach(topics)).granted;
|
|
2261
2648
|
}
|
|
2262
2649
|
/**
|
|
2263
|
-
*
|
|
2264
|
-
*
|
|
2265
|
-
*
|
|
2650
|
+
* Subscribe each filter in a SUBSCRIBE of its own and split the outcome into granted vs refused.
|
|
2651
|
+
*
|
|
2652
|
+
* One request per filter because the MQTT engine treats a SUBACK as all-or-nothing: one refused grant
|
|
2653
|
+
* rejects the whole request, and it forgets EVERY filter of that request for resubscription after a
|
|
2654
|
+
* reconnect — so a batch holding one denied leg would lose the granted legs on the next drop. Alone,
|
|
2655
|
+
* a refused filter costs only itself. A rejection that carries no SUBACK is a transport failure and
|
|
2656
|
+
* is thrown.
|
|
2657
|
+
*
|
|
2658
|
+
* The cost is one SUBSCRIBE and one SUBACK per filter instead of one per device — four for a
|
|
2659
|
+
* four-leg line. They are sent concurrently, so the wall-clock cost is one round trip. Folding them
|
|
2660
|
+
* back into one request brings back the lost resubscription.
|
|
2266
2661
|
*/
|
|
2267
|
-
|
|
2662
|
+
async subscribeEach(topics) {
|
|
2663
|
+
const client = this.client;
|
|
2664
|
+
if (!client)
|
|
2665
|
+
throw new Error("SecureMqtt not connected");
|
|
2666
|
+
const outcomes = await Promise.allSettled(topics.map((topic) => client.subscribeAsync(topic, { qos: 1 })));
|
|
2268
2667
|
const granted = [];
|
|
2269
2668
|
const denied = [];
|
|
2270
|
-
|
|
2271
|
-
|
|
2669
|
+
outcomes.forEach((outcome, i) => {
|
|
2670
|
+
const topic = topics[i];
|
|
2671
|
+
if (outcome.status === "fulfilled") {
|
|
2672
|
+
granted.push(topic);
|
|
2673
|
+
return;
|
|
2674
|
+
}
|
|
2675
|
+
if (!isRefusal(outcome.reason))
|
|
2676
|
+
throw outcome.reason;
|
|
2677
|
+
denied.push(topic);
|
|
2678
|
+
});
|
|
2272
2679
|
return { granted, denied };
|
|
2273
2680
|
}
|
|
2274
2681
|
/**
|
|
@@ -2283,134 +2690,25 @@ var SecureMqtt = class extends EventEmitter {
|
|
|
2283
2690
|
* don't share subscribe/publish routing).
|
|
2284
2691
|
*/
|
|
2285
2692
|
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;
|
|
2393
|
-
}
|
|
2394
|
-
}
|
|
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;
|
|
2693
|
+
if (!this.client)
|
|
2694
|
+
throw new Error("SecureMqtt not connected");
|
|
2695
|
+
await this.client.publishAsync(topic, body, { qos: opts.qos ?? 1 });
|
|
2696
|
+
}
|
|
2697
|
+
async disconnect() {
|
|
2698
|
+
this.connecting = void 0;
|
|
2699
|
+
await this.client?.endAsync(true);
|
|
2700
|
+
this.client = void 0;
|
|
2701
|
+
}
|
|
2702
|
+
};
|
|
2703
|
+
|
|
2704
|
+
// dist/transport/mqtt/app-client-id.js
|
|
2705
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
2706
|
+
function buildAppShapedClientId(input) {
|
|
2707
|
+
const ts = input.timestamp ?? Math.floor(Date.now() / 1e3);
|
|
2708
|
+
return `android-${input.appName}-${input.uid}-${input.mqttUuid}-${ts}`;
|
|
2409
2709
|
}
|
|
2410
|
-
function
|
|
2411
|
-
|
|
2412
|
-
b.writeUInt32BE(n >>> 0);
|
|
2413
|
-
return b;
|
|
2710
|
+
function mqttUuidFrom(installId) {
|
|
2711
|
+
return createHash4("sha256").update(installId).digest("hex").slice(0, 16);
|
|
2414
2712
|
}
|
|
2415
2713
|
|
|
2416
2714
|
// dist/transport/mqtt/availability.js
|
|
@@ -2537,13 +2835,6 @@ function parseAiotDpReport(raw) {
|
|
|
2537
2835
|
return Object.keys(out).length ? out : void 0;
|
|
2538
2836
|
}
|
|
2539
2837
|
|
|
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
2838
|
// dist/transport/mqtt/biz-stream.js
|
|
2548
2839
|
var BIZ_PROTOCOL = {
|
|
2549
2840
|
/** Map-stream data: the frames this module parses. */
|
|
@@ -2727,18 +3018,6 @@ var DeviceType = {
|
|
|
2727
3018
|
CAMERA_C35: 10035
|
|
2728
3019
|
};
|
|
2729
3020
|
|
|
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
3021
|
// dist/model/classify.js
|
|
2743
3022
|
var STATION_TYPES = /* @__PURE__ */ new Set([
|
|
2744
3023
|
DeviceType.STATION,
|
|
@@ -2877,111 +3156,6 @@ function inferName(rec) {
|
|
|
2877
3156
|
return void 0;
|
|
2878
3157
|
}
|
|
2879
3158
|
|
|
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;
|
|
2975
|
-
}
|
|
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 });
|
|
2980
|
-
}
|
|
2981
|
-
function commandObservation(command) {
|
|
2982
|
-
return command[COMMAND_OBSERVATION];
|
|
2983
|
-
}
|
|
2984
|
-
|
|
2985
3159
|
// dist/model/capabilities/access.js
|
|
2986
3160
|
function setScalar(param, value, ctx, form = "auto") {
|
|
2987
3161
|
return { kind: "set-param", param, value, form, channel: ctx.channel };
|
|
@@ -3254,11 +3428,17 @@ var unreflectedMembers = (surface) => statement(surface, UNREFLECTED);
|
|
|
3254
3428
|
// dist/model/capabilities/manifest.js
|
|
3255
3429
|
function describeBound(modules, bound, ctx) {
|
|
3256
3430
|
const out = [];
|
|
3431
|
+
const anyBound = Object.values(bound).some((obj) => obj && typeof obj === "object");
|
|
3257
3432
|
for (const m of modules) {
|
|
3258
3433
|
const accessor = camelCase(m.capability);
|
|
3259
3434
|
const obj = bound[accessor];
|
|
3260
|
-
if (!obj || typeof obj !== "object")
|
|
3435
|
+
if (!obj || typeof obj !== "object") {
|
|
3436
|
+
const events = anyBound ? emitsOf(m, [], ctx) : [];
|
|
3437
|
+
if (events.length && ctx?.capabilities?.has(m.capability)) {
|
|
3438
|
+
out.push({ capability: m.capability, reads: [], actions: [], undescribedActions: [], events });
|
|
3439
|
+
}
|
|
3261
3440
|
continue;
|
|
3441
|
+
}
|
|
3262
3442
|
const descriptors = Object.getOwnPropertyDescriptors(obj);
|
|
3263
3443
|
const reads2 = [];
|
|
3264
3444
|
const actions = [];
|
|
@@ -3781,7 +3961,6 @@ var MOTION_CMD = {
|
|
|
3781
3961
|
* payload key AND pass mChannel 0 explicitly; the two are separate choices, not linked.)
|
|
3782
3962
|
*
|
|
3783
3963
|
* ⚠️ Replay + readback confirmed on a HomeBase-attached T8425 (1719 `0`→`1`→`0`), NOT byte-captured.
|
|
3784
|
-
* Provenance is `apk`, not `verified`: a divergent-but-also-accepted frame can't be ruled out.
|
|
3785
3964
|
*/
|
|
3786
3965
|
HUMAN_ONLY_AT_NIGHT: 1719,
|
|
3787
3966
|
/**
|
|
@@ -3795,7 +3974,7 @@ var MOTION_CMD = {
|
|
|
3795
3974
|
* form, so a feature that is on reads as off.
|
|
3796
3975
|
* Decoded by {@link decodeRadarWdSwitch} to match the app.
|
|
3797
3976
|
*
|
|
3798
|
-
* ⚠️ Replay + readback confirmed on a T8214 (2706 `0`→`1`→`0`), NOT byte-captured.
|
|
3977
|
+
* ⚠️ Replay + readback confirmed on a T8214 (2706 `0`→`1`→`0`), NOT byte-captured.
|
|
3799
3978
|
*/
|
|
3800
3979
|
LOITERING_DETECTION: 2706,
|
|
3801
3980
|
/**
|
|
@@ -4119,7 +4298,7 @@ var MOTION_MEMBERS = {
|
|
|
4119
4298
|
param: MOTION_CMD.HUMAN_ONLY_AT_NIGHT,
|
|
4120
4299
|
type: "bool",
|
|
4121
4300
|
kind: "boolean",
|
|
4122
|
-
provenance: "
|
|
4301
|
+
provenance: "verified",
|
|
4123
4302
|
description: "Restrict AI classification to night-time only (1719). \u26A0\uFE0F Replay + readback confirmed on a HomeBase-attached T8425, not byte-captured.",
|
|
4124
4303
|
requires: [MOTION_CMD.HUMAN_ONLY_AT_NIGHT],
|
|
4125
4304
|
write: (v, ctx) => {
|
|
@@ -4135,7 +4314,7 @@ var MOTION_MEMBERS = {
|
|
|
4135
4314
|
param: MOTION_CMD.LOITERING_DETECTION,
|
|
4136
4315
|
type: "bool",
|
|
4137
4316
|
kind: "boolean",
|
|
4138
|
-
provenance: "
|
|
4317
|
+
provenance: "verified",
|
|
4139
4318
|
coerce: (raw) => decodeRadarWdSwitch(raw) ?? false,
|
|
4140
4319
|
description: "Loitering detection \u2014 alert on lingering rather than passing (2706). Observed on the T8214 doorbell only; the read is object-OR-scalar, matching the app's own decode.",
|
|
4141
4320
|
requires: [MOTION_CMD.LOITERING_DETECTION],
|
|
@@ -4241,6 +4420,7 @@ var MOTION = {
|
|
|
4241
4420
|
};
|
|
4242
4421
|
|
|
4243
4422
|
// dist/model/capabilities/person-detection.js
|
|
4423
|
+
var IDENTIFIED_PERSON_CLAIM = { homeBaseAttached: true };
|
|
4244
4424
|
var PERSON_DETECTION = {
|
|
4245
4425
|
capability: "person_detection",
|
|
4246
4426
|
description: "AI human detection enable switch and live detected state.",
|
|
@@ -4257,8 +4437,18 @@ var PERSON_DETECTION = {
|
|
|
4257
4437
|
*/
|
|
4258
4438
|
events: [
|
|
4259
4439
|
{ source: "push", match: DoorbellPushEvent.FACE_DETECTION, emit: "personDetected" },
|
|
4260
|
-
{
|
|
4261
|
-
|
|
4440
|
+
{
|
|
4441
|
+
source: "push",
|
|
4442
|
+
match: HB3PairedDevicePushEvent.IDENTITY_PERSON_DETECTION,
|
|
4443
|
+
emit: "personDetected",
|
|
4444
|
+
claim: IDENTIFIED_PERSON_CLAIM
|
|
4445
|
+
},
|
|
4446
|
+
{
|
|
4447
|
+
source: "push",
|
|
4448
|
+
match: HB3PairedDevicePushEvent.STRANGER_PERSON_DETECTION,
|
|
4449
|
+
emit: "strangerDetected",
|
|
4450
|
+
claim: IDENTIFIED_PERSON_CLAIM
|
|
4451
|
+
}
|
|
4262
4452
|
]
|
|
4263
4453
|
};
|
|
4264
4454
|
|
|
@@ -4326,7 +4516,7 @@ function decodePowerSource(raw) {
|
|
|
4326
4516
|
function recordSetting(param, value, ctx) {
|
|
4327
4517
|
return setStationScalar(param, value, ctx.channel);
|
|
4328
4518
|
}
|
|
4329
|
-
var MAINS_CAMERA_MODELS = ["T8425", "T8419", "T8410", "T84A1"];
|
|
4519
|
+
var MAINS_CAMERA_MODELS = ["T8425", "T8419", "T8410", "T84A1", "T8423"];
|
|
4330
4520
|
function cameraPowerTier(model, capabilities) {
|
|
4331
4521
|
return capabilities.has("battery") && notMainsCamera({ model }) ? "battery" : "wired";
|
|
4332
4522
|
}
|
|
@@ -5536,6 +5726,7 @@ var CAMERA_MEMBERS = {
|
|
|
5536
5726
|
refuseWhenDisabled(ctx, read);
|
|
5537
5727
|
return m.recordFragments({ powered: poweredOf(ctx), ...opts });
|
|
5538
5728
|
}), "Continuous fragmented-MP4 (CMAF) recording."),
|
|
5729
|
+
downloadRecording: provided("media", (m) => m.downloadRecording && ((opts) => m.downloadRecording(opts)), "Download a recording the station holds \u2192 H.264 and AAC elementary streams."),
|
|
5539
5730
|
/**
|
|
5540
5731
|
* Push audio from the host to this camera's speaker. Gated on the **speaker** param specifically —
|
|
5541
5732
|
* not the `audio` capability, which resolves on a microphone alone and would advertise a speaker the
|
|
@@ -5577,7 +5768,7 @@ var SIREN_CMD = {
|
|
|
5577
5768
|
ALARM_TEST: 1826,
|
|
5578
5769
|
/** Manually stop a sounding alarm (app `APP_CMD_SIREN_SENSOR_MANUAL_STOP_ALARM`). */
|
|
5579
5770
|
MANUAL_STOP: 1871,
|
|
5580
|
-
/** Station-family HomeBase duration alarm, verified live on T8010 (app `SET_TONE_FILE`). */
|
|
5771
|
+
/** Station-family HomeBase duration alarm, verified live on T8010 and T8030 (app `SET_TONE_FILE`). */
|
|
5581
5772
|
HOMEBASE_TONE: 1201,
|
|
5582
5773
|
/** HomeBase alarm volume percentage (app `CMD_SET_HUB_SPK_VOLUME`). */
|
|
5583
5774
|
HUB_SPK_VOLUME: 1235,
|
|
@@ -5592,7 +5783,7 @@ var SirenAlarmDuration = { Min1: 60, Min5: 300, Min10: 600, Min15: 900 };
|
|
|
5592
5783
|
var HubAlarmTone = { Tone1: 1, Tone2: 2 };
|
|
5593
5784
|
var HUB_ALARM_EVIDENCE = [SIREN_CMD.HUB_ALARM_TONE, 1282];
|
|
5594
5785
|
function isStationAlarmOutput(ctx) {
|
|
5595
|
-
return ctx.deviceType === DeviceType.STATION;
|
|
5786
|
+
return ctx.deviceType === DeviceType.STATION || ctx.deviceType === DeviceType.HB3;
|
|
5596
5787
|
}
|
|
5597
5788
|
function isEvidencedStationAlarm(ctx) {
|
|
5598
5789
|
return isStationAlarmOutput(ctx) && HUB_ALARM_EVIDENCE.some((param) => ctx.paramIds.has(param));
|
|
@@ -6714,9 +6905,9 @@ function parseQuickResponses(voiceList) {
|
|
|
6714
6905
|
var DOORBELL_MEMBERS = {
|
|
6715
6906
|
/**
|
|
6716
6907
|
* The HOMEBASE as the doorbell's chime — the hub plays the ring, not the wired chime box
|
|
6717
|
-
* `mechanicalChimeSwitch` drives. `provenance` is "verified" on our own decrypt of the app's frame
|
|
6718
|
-
*
|
|
6719
|
-
*
|
|
6908
|
+
* `mechanicalChimeSwitch` drives. `provenance` is "verified" on our own decrypt of the app's frame:
|
|
6909
|
+
* direct-binary `[ch][value][acct]`, 1=on/0=off, the same shape as its 1703 sibling — captured, not
|
|
6910
|
+
* inferred from the shared param range.
|
|
6720
6911
|
*/
|
|
6721
6912
|
chimeSwitch: {
|
|
6722
6913
|
param: DOORBELL_CMD.CHIME_SWITCH,
|
|
@@ -6727,9 +6918,8 @@ var DOORBELL_MEMBERS = {
|
|
|
6727
6918
|
write: (v, ctx) => setScalar(DOORBELL_CMD.CHIME_SWITCH, asBool(v) ? 1 : 0, ctx, "direct-binary")
|
|
6728
6919
|
},
|
|
6729
6920
|
/**
|
|
6730
|
-
* `provenance` is "verified"
|
|
6731
|
-
*
|
|
6732
|
-
* `[ch][value][acct]`, 1=on/0=off — verified live on a T8214 (ON then OFF).
|
|
6921
|
+
* `provenance` is "verified" on our own P2P decrypt of the write wire: direct-binary
|
|
6922
|
+
* `[ch][value][acct]`, 1=on/0=off, verified live on a T8214 (ON then OFF).
|
|
6733
6923
|
*/
|
|
6734
6924
|
mechanicalChimeSwitch: {
|
|
6735
6925
|
param: DOORBELL_CMD.MECHANICAL_CHIME_SWITCH,
|
|
@@ -6760,7 +6950,7 @@ var DOORBELL_MEMBERS = {
|
|
|
6760
6950
|
type: "number",
|
|
6761
6951
|
unit: "%",
|
|
6762
6952
|
kind: "percent",
|
|
6763
|
-
provenance: "
|
|
6953
|
+
provenance: "verified",
|
|
6764
6954
|
writtenElsewhere: true,
|
|
6765
6955
|
description: "Ringtone volume (1708; confirmed on T8214, observed value 80). The WRITE is `audio`'s setRingtoneVolume \u2014 that capability owns every volume wire."
|
|
6766
6956
|
},
|
|
@@ -6865,7 +7055,7 @@ var DOORBELL_MEMBERS = {
|
|
|
6865
7055
|
param: 1710,
|
|
6866
7056
|
type: "string",
|
|
6867
7057
|
kind: "text",
|
|
6868
|
-
provenance: "
|
|
7058
|
+
provenance: "verified",
|
|
6869
7059
|
description: "Notification config JSON {notification_motion_onoff,notification_ring_onoff,notification_style} (1710; confirmed on T8214)."
|
|
6870
7060
|
},
|
|
6871
7061
|
/**
|
|
@@ -7526,120 +7716,41 @@ var ARMING_MEMBERS = {
|
|
|
7526
7716
|
var ARMING = {
|
|
7527
7717
|
capability: "arming",
|
|
7528
7718
|
description: "Station/device guard (arming) mode.",
|
|
7529
|
-
members: ARMING_MEMBERS,
|
|
7530
|
-
properties: propertiesOf(ARMING_MEMBERS),
|
|
7531
|
-
// A reported guard-mode param is the verified proof; every station-codec device also owns an
|
|
7532
|
-
// arming surface as part of its baseline.
|
|
7533
|
-
detection: { evidenceParams: [ARMING_CMD.SET_ARMING], codecs: ["station"] },
|
|
7534
|
-
ownedByStation: true,
|
|
7535
|
-
/**
|
|
7536
|
-
* Station-scoped push events. The hub reports a guard-mode switch and the alarm lifecycle on the
|
|
7537
|
-
* generic `CusPushEvent` channel, so these ids are shared with other families and resolve by
|
|
7538
|
-
* capability (see the barrel's event index).
|
|
7539
|
-
*
|
|
7540
|
-
* The alarm ids carry a static `phase` so one event name covers the whole lifecycle, the same shape
|
|
7541
|
-
* `battery` uses for its threshold pushes. The mode-switch push is known to identify WHICH mode and
|
|
7542
|
-
* what triggered the change, but neither has been observed on the wire here, so this emits the bare
|
|
7543
|
-
* transition and refreshes the `mode` read rather than trusting a decoded field.
|
|
7544
|
-
*/
|
|
7545
|
-
events: [
|
|
7546
|
-
{
|
|
7547
|
-
source: "push",
|
|
7548
|
-
match: CusPushEvent.MODE_SWITCH,
|
|
7549
|
-
emit: "armingModeChanged",
|
|
7550
|
-
refresh: { member: "mode" }
|
|
7551
|
-
},
|
|
7552
|
-
{ source: "push", match: CusPushEvent.ALARM, emit: "alarm", payload: { phase: "triggered" } },
|
|
7553
|
-
{ source: "push", match: CusPushEvent.ALARM_DELAY, emit: "alarm", payload: { phase: "delayed" } }
|
|
7554
|
-
]
|
|
7555
|
-
};
|
|
7556
|
-
|
|
7557
|
-
// dist/model/capabilities/storage.js
|
|
7558
|
-
var STORAGE = {
|
|
7559
|
-
capability: "storage",
|
|
7560
|
-
description: "Local storage (SD card / eMMC / HDD). Behaviour-only until a capacity param is captured.",
|
|
7561
|
-
properties: [],
|
|
7562
|
-
detection: { codecs: ["station"] }
|
|
7563
|
-
};
|
|
7564
|
-
|
|
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
|
-
}
|
|
7719
|
+
members: ARMING_MEMBERS,
|
|
7720
|
+
properties: propertiesOf(ARMING_MEMBERS),
|
|
7721
|
+
// A reported guard-mode param is the verified proof; every station-codec device also owns an
|
|
7722
|
+
// arming surface as part of its baseline.
|
|
7723
|
+
detection: { evidenceParams: [ARMING_CMD.SET_ARMING], codecs: ["station"] },
|
|
7724
|
+
ownedByStation: true,
|
|
7623
7725
|
/**
|
|
7624
|
-
* The
|
|
7726
|
+
* Station-scoped push events. The hub reports a guard-mode switch and the alarm lifecycle on the
|
|
7727
|
+
* generic `CusPushEvent` channel, so these ids are shared with other families and resolve by
|
|
7728
|
+
* capability (see the barrel's event index).
|
|
7625
7729
|
*
|
|
7626
|
-
*
|
|
7627
|
-
*
|
|
7730
|
+
* The alarm ids carry a static `phase` so one event name covers the whole lifecycle, the same shape
|
|
7731
|
+
* `battery` uses for its threshold pushes. The mode-switch push is known to identify WHICH mode and
|
|
7732
|
+
* what triggered the change, but neither has been observed on the wire here, so this emits the bare
|
|
7733
|
+
* transition and refreshes the `mode` read rather than trusting a decoded field.
|
|
7628
7734
|
*/
|
|
7629
|
-
|
|
7630
|
-
|
|
7631
|
-
|
|
7735
|
+
events: [
|
|
7736
|
+
{
|
|
7737
|
+
source: "push",
|
|
7738
|
+
match: CusPushEvent.MODE_SWITCH,
|
|
7739
|
+
emit: "armingModeChanged",
|
|
7740
|
+
refresh: { member: "mode" }
|
|
7741
|
+
},
|
|
7742
|
+
{ source: "push", match: CusPushEvent.ALARM, emit: "alarm", payload: { phase: "triggered" } },
|
|
7743
|
+
{ source: "push", match: CusPushEvent.ALARM_DELAY, emit: "alarm", payload: { phase: "delayed" } }
|
|
7744
|
+
]
|
|
7745
|
+
};
|
|
7746
|
+
|
|
7747
|
+
// dist/model/capabilities/storage.js
|
|
7748
|
+
var STORAGE = {
|
|
7749
|
+
capability: "storage",
|
|
7750
|
+
description: "Local storage (SD card / eMMC / HDD). Behaviour-only until a capacity param is captured.",
|
|
7751
|
+
properties: [],
|
|
7752
|
+
detection: { codecs: ["station"] }
|
|
7632
7753
|
};
|
|
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
7754
|
|
|
7644
7755
|
// dist/model/capabilities/vacuum-dock.js
|
|
7645
7756
|
var VACUUM_DOCK_DP = 173;
|
|
@@ -13345,58 +13456,6 @@ function parseCleanRecordDetail(blob, codec) {
|
|
|
13345
13456
|
};
|
|
13346
13457
|
}
|
|
13347
13458
|
|
|
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
13459
|
// dist/model/vacuum-map.js
|
|
13401
13460
|
var DOCK_KINDS = ["charger", "station"];
|
|
13402
13461
|
var MAP_QUALITIES = ["incomplete", "rough", "effective", "listFull"];
|
|
@@ -14307,6 +14366,7 @@ __export(codec_exports, {
|
|
|
14307
14366
|
buildLookupWithKeyPayload2: () => buildLookupWithKeyPayload2,
|
|
14308
14367
|
buildRawCommandPayload: () => buildRawCommandPayload,
|
|
14309
14368
|
buildStringCommandPayload: () => buildStringCommandPayload,
|
|
14369
|
+
buildStringPairCommandPayload: () => buildStringPairCommandPayload,
|
|
14310
14370
|
buildVoidCommandPayload: () => buildVoidCommandPayload,
|
|
14311
14371
|
decodeP2PCloudIPs: () => decodeP2PCloudIPs,
|
|
14312
14372
|
decryptP2PData: () => decryptP2PData,
|
|
@@ -14343,6 +14403,11 @@ var ResponseMessageType = {
|
|
|
14343
14403
|
LOOKUP_ADDR: Buffer.from([241, 64]),
|
|
14344
14404
|
LOOKUP_ADDR2: Buffer.from([241, 130]),
|
|
14345
14405
|
CAM_ID: Buffer.from([241, 66]),
|
|
14406
|
+
/**
|
|
14407
|
+
* The device's own address record, sent in answer to CHECK_CAM ahead of CAM_ID: its 20-byte device id, one
|
|
14408
|
+
* record in `encodeSelfAddress`'s shape naming the address it answers from, then 8 zero bytes (44-byte payload).
|
|
14409
|
+
*/
|
|
14410
|
+
CAM_ADDR: Buffer.from([241, 67]),
|
|
14346
14411
|
TURN_SERVER_CAM_ID: Buffer.from([241, 132]),
|
|
14347
14412
|
PING: Buffer.from([241, 224]),
|
|
14348
14413
|
PONG: Buffer.from([241, 225]),
|
|
@@ -14554,6 +14619,12 @@ function buildIntStringCommandPayload(value, valueSub, strValue, channel = 0, ke
|
|
|
14554
14619
|
data
|
|
14555
14620
|
]);
|
|
14556
14621
|
}
|
|
14622
|
+
function buildStringPairCommandPayload(strValue, strValueSub, channel = 0, key, encType = 1) {
|
|
14623
|
+
const encrypted = !!key && key.length === 16;
|
|
14624
|
+
const body = Buffer.concat([Buffer.alloc(5), stringWithLength(strValue), stringWithLength(strValueSub)]);
|
|
14625
|
+
const data = encrypted ? encryptP2PData(paddingP2PData(body), key) : body;
|
|
14626
|
+
return buildRawCommandPayload(data, channel, encrypted ? encType : 0);
|
|
14627
|
+
}
|
|
14557
14628
|
function buildRawCommandPayload(data, channel = 0, signCode = 0, magic = [1, 0], streamId = 0) {
|
|
14558
14629
|
const header = Buffer.allocUnsafe(2);
|
|
14559
14630
|
header.writeUInt16LE(data.length, 0);
|
|
@@ -15159,6 +15230,7 @@ var LOCAL_LOOKUP_PORT = 32108;
|
|
|
15159
15230
|
var HEARTBEAT_MS = 5e3;
|
|
15160
15231
|
var PATH_SILENCE_MS = HEARTBEAT_MS * 3;
|
|
15161
15232
|
var LOOKUP_RETRY_MS = 1e3;
|
|
15233
|
+
var RECEIVE_BUFFER_BYTES = 4 * 1024 * 1024;
|
|
15162
15234
|
var CONNECT_TIMEOUT_MS = 15e3;
|
|
15163
15235
|
var STATION_CHANNEL3 = 255;
|
|
15164
15236
|
var CMD_GATEWAYINFO = 1100;
|
|
@@ -15184,6 +15256,7 @@ var SEQUENCE_LOOKBACK = 32768;
|
|
|
15184
15256
|
var STALE_RETRANSMIT_DEPTH = 1024;
|
|
15185
15257
|
var PUNCH_PROBE_SOCKETS = 7;
|
|
15186
15258
|
var REORDER_WAIT_MS = 250;
|
|
15259
|
+
var VIDEO_REORDER_WAIT_MS = 700;
|
|
15187
15260
|
var REORDER_MAX_DATAGRAMS = 128;
|
|
15188
15261
|
var MAX_TRACED_DATAGRAM_GAPS = 8;
|
|
15189
15262
|
var CMD_SET_PAYLOAD = 1350;
|
|
@@ -15194,6 +15267,7 @@ var CMD_DATABASE_IMAGE = 1308;
|
|
|
15194
15267
|
var CMD_DATABASE = 1306;
|
|
15195
15268
|
var DB_QUERY = { FULL_TABLE: 1e4, COMBINATION_WITH_AI: 10011 };
|
|
15196
15269
|
var GCM_AAD = Buffer.from("eufy security");
|
|
15270
|
+
var LEVEL2_SEQ_BASE = 16909056;
|
|
15197
15271
|
function paramReport(payload) {
|
|
15198
15272
|
const list = payload?.params;
|
|
15199
15273
|
if (!Array.isArray(list))
|
|
@@ -15208,12 +15282,57 @@ function paramReport(payload) {
|
|
|
15208
15282
|
return Object.keys(out).length ? out : void 0;
|
|
15209
15283
|
}
|
|
15210
15284
|
var traceSequence = 0;
|
|
15285
|
+
var DB_TABLE_TIMEOUT_MS = 15e3;
|
|
15286
|
+
function firstTableRows(text2) {
|
|
15287
|
+
const decoded = Buffer.from(text2, "latin1").toString("utf8");
|
|
15288
|
+
for (let start = decoded.indexOf("{"); start >= 0; start = decoded.indexOf("{", start + 1)) {
|
|
15289
|
+
const end = closingBrace(decoded, start);
|
|
15290
|
+
if (end < 0)
|
|
15291
|
+
return void 0;
|
|
15292
|
+
let parsed;
|
|
15293
|
+
try {
|
|
15294
|
+
parsed = JSON.parse(decoded.slice(start, end + 1));
|
|
15295
|
+
} catch {
|
|
15296
|
+
continue;
|
|
15297
|
+
}
|
|
15298
|
+
const data = parsed?.data;
|
|
15299
|
+
if (Array.isArray(data))
|
|
15300
|
+
return data;
|
|
15301
|
+
}
|
|
15302
|
+
return void 0;
|
|
15303
|
+
}
|
|
15304
|
+
function closingBrace(text2, start) {
|
|
15305
|
+
let depth = 0;
|
|
15306
|
+
let inString = false;
|
|
15307
|
+
let escaped = false;
|
|
15308
|
+
for (let i = start; i < text2.length; i++) {
|
|
15309
|
+
const c = text2[i];
|
|
15310
|
+
if (inString) {
|
|
15311
|
+
if (escaped)
|
|
15312
|
+
escaped = false;
|
|
15313
|
+
else if (c === "\\")
|
|
15314
|
+
escaped = true;
|
|
15315
|
+
else if (c === '"')
|
|
15316
|
+
inString = false;
|
|
15317
|
+
continue;
|
|
15318
|
+
}
|
|
15319
|
+
if (c === '"')
|
|
15320
|
+
inString = true;
|
|
15321
|
+
else if (c === "{")
|
|
15322
|
+
depth++;
|
|
15323
|
+
else if (c === "}" && --depth === 0)
|
|
15324
|
+
return i;
|
|
15325
|
+
}
|
|
15326
|
+
return -1;
|
|
15327
|
+
}
|
|
15211
15328
|
var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
15212
15329
|
cfg;
|
|
15213
15330
|
socket;
|
|
15214
15331
|
connected = false;
|
|
15215
15332
|
connecting = false;
|
|
15216
15333
|
closed = false;
|
|
15334
|
+
/** Whether a short receive buffer has been reported, so a reconnect does not repeat the same warning. */
|
|
15335
|
+
receiveBufferReported = false;
|
|
15217
15336
|
connectAddress;
|
|
15218
15337
|
seqNumber = 0;
|
|
15219
15338
|
/**
|
|
@@ -15240,8 +15359,8 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
15240
15359
|
audioStalled = false;
|
|
15241
15360
|
audioRetransmitTimer;
|
|
15242
15361
|
lastPongData;
|
|
15243
|
-
/** When
|
|
15244
|
-
|
|
15362
|
+
/** When the selected peer last answered outbound P2P traffic on this connection. */
|
|
15363
|
+
lastPeerAt;
|
|
15245
15364
|
/** Whether the silence has already been stated, so it is traced once per connection rather than per read. */
|
|
15246
15365
|
pathStaleTraced = false;
|
|
15247
15366
|
lookupTimer;
|
|
@@ -15310,17 +15429,17 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
15310
15429
|
/**
|
|
15311
15430
|
* How long this connection's path has been silent, or nothing where it has never answered.
|
|
15312
15431
|
*
|
|
15313
|
-
*
|
|
15314
|
-
*
|
|
15432
|
+
* PONG and ACK from the selected peer prove the path answers outbound traffic. `undefined` means no such reply
|
|
15433
|
+
* has arrived since connection, so silence alone does not establish a stale path.
|
|
15315
15434
|
*/
|
|
15316
15435
|
get pathSilentMs() {
|
|
15317
|
-
return this.
|
|
15436
|
+
return this.lastPeerAt === void 0 ? void 0 : Date.now() - this.lastPeerAt;
|
|
15318
15437
|
}
|
|
15319
15438
|
/**
|
|
15320
15439
|
* Whether this path can still be committed to, on the evidence the heartbeat gives.
|
|
15321
15440
|
*
|
|
15322
|
-
* False where
|
|
15323
|
-
* not known to be dead, so it answers true.
|
|
15441
|
+
* False where selected-peer replies arrived and then stopped for {@link PATH_SILENCE_MS}. A connection with
|
|
15442
|
+
* no post-connect reply is not known to be dead, so it answers true.
|
|
15324
15443
|
*
|
|
15325
15444
|
* Traces the silence once per connection, on the read that first observes it.
|
|
15326
15445
|
*/
|
|
@@ -15536,6 +15655,26 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
15536
15655
|
get isConnected() {
|
|
15537
15656
|
return this.connected;
|
|
15538
15657
|
}
|
|
15658
|
+
/**
|
|
15659
|
+
* Ask the OS for {@link RECEIVE_BUFFER_BYTES} on a bound socket, and warn once per session when it grants
|
|
15660
|
+
* less or refuses.
|
|
15661
|
+
*
|
|
15662
|
+
* The request is made here rather than through `createSocket`'s `recvBufferSize`: Node applies that option
|
|
15663
|
+
* inside the bind callback, where a refusal is thrown out of reach of this session and ends the process.
|
|
15664
|
+
*/
|
|
15665
|
+
requestReceiveBuffer(socket) {
|
|
15666
|
+
let granted;
|
|
15667
|
+
try {
|
|
15668
|
+
socket.setRecvBufferSize(RECEIVE_BUFFER_BYTES);
|
|
15669
|
+
granted = socket.getRecvBufferSize();
|
|
15670
|
+
} catch {
|
|
15671
|
+
granted = 0;
|
|
15672
|
+
}
|
|
15673
|
+
if (granted >= RECEIVE_BUFFER_BYTES || this.receiveBufferReported)
|
|
15674
|
+
return;
|
|
15675
|
+
this.receiveBufferReported = true;
|
|
15676
|
+
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}.`);
|
|
15677
|
+
}
|
|
15539
15678
|
/** Open the socket and start the lookup → hole-punch handshake. */
|
|
15540
15679
|
async connect() {
|
|
15541
15680
|
if (this.connecting || this.connected)
|
|
@@ -15559,6 +15698,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
15559
15698
|
socket.setBroadcast(true);
|
|
15560
15699
|
} catch {
|
|
15561
15700
|
}
|
|
15701
|
+
this.requestReceiveBuffer(socket);
|
|
15562
15702
|
resolve();
|
|
15563
15703
|
});
|
|
15564
15704
|
});
|
|
@@ -15575,7 +15715,8 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
15575
15715
|
this.trace({
|
|
15576
15716
|
phase: "lookup-channels",
|
|
15577
15717
|
local: !this.cfg.noBroadcast || this.cfg.localAddress !== void 0,
|
|
15578
|
-
cloud: this.cloudLookup !== void 0
|
|
15718
|
+
cloud: this.cloudLookup !== void 0,
|
|
15719
|
+
...this.cloudLookup ? {} : { cloudMissing: dskKey ? "cloud-addresses" : "dsk-key" }
|
|
15579
15720
|
});
|
|
15580
15721
|
this.sendLookups();
|
|
15581
15722
|
this.lookupTimer = setInterval(() => this.sendLookups(), LOOKUP_RETRY_MS);
|
|
@@ -15677,10 +15818,18 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
15677
15818
|
* straddling a level-2 wait on a camera whose failure to deliver video had a separate cause, and did not
|
|
15678
15819
|
* recur across later probes of it. Correlate an unmodelled type against a WORKING session before reading it
|
|
15679
15820
|
* as a cause.
|
|
15821
|
+
*
|
|
15822
|
+
* CAM_ADDR is recognised and dropped: it names the address the device is already answering from, and the
|
|
15823
|
+
* CAM_ID that follows it is what completes the connect.
|
|
15680
15824
|
*/
|
|
15681
15825
|
onMessage(msg, rinfo, socket = this.socket) {
|
|
15682
15826
|
if (!socket)
|
|
15683
15827
|
return;
|
|
15828
|
+
const fromConnectedPeer = this.connected && this.connectAddress?.host === rinfo.address && this.connectAddress.port === rinfo.port;
|
|
15829
|
+
if (fromConnectedPeer && (hasHeader(msg, ResponseMessageType.PONG) || hasHeader(msg, ResponseMessageType.ACK))) {
|
|
15830
|
+
this.lastPeerAt = Date.now();
|
|
15831
|
+
this.pathStaleTraced = false;
|
|
15832
|
+
}
|
|
15684
15833
|
if (!hasHeader(msg, ResponseMessageType.DATA)) {
|
|
15685
15834
|
this.logger.debug(`[p2p] ${this.cfg.stationSn} <<< ${rinfo.address}:${rinfo.port} header=${msg.subarray(0, 2).toString("hex")} len=${msg.length}`);
|
|
15686
15835
|
}
|
|
@@ -15698,8 +15847,6 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
15698
15847
|
this.onConnected({ host: rinfo.address, port: rinfo.port }, socket);
|
|
15699
15848
|
} else if (hasHeader(msg, ResponseMessageType.PONG)) {
|
|
15700
15849
|
this.lastPongData = msg.length > 4 ? msg.subarray(4) : void 0;
|
|
15701
|
-
this.lastPongAt = Date.now();
|
|
15702
|
-
this.pathStaleTraced = false;
|
|
15703
15850
|
} else if (hasHeader(msg, ResponseMessageType.PING)) {
|
|
15704
15851
|
this.send({ host: rinfo.address, port: rinfo.port }, RequestMessageType.PONG, void 0, socket);
|
|
15705
15852
|
} else if (hasHeader(msg, ResponseMessageType.ACK)) {
|
|
@@ -15707,7 +15854,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
15707
15854
|
} else if (hasHeader(msg, ResponseMessageType.DATA)) {
|
|
15708
15855
|
if (this.connected)
|
|
15709
15856
|
this.onData(msg, { host: rinfo.address, port: rinfo.port });
|
|
15710
|
-
} else {
|
|
15857
|
+
} else if (!hasHeader(msg, ResponseMessageType.CAM_ADDR)) {
|
|
15711
15858
|
this.logger.debug(`[p2p] ${this.cfg.stationSn} UNHANDLED payload hex: ${msg.toString("hex")}`);
|
|
15712
15859
|
}
|
|
15713
15860
|
}
|
|
@@ -15998,6 +16145,17 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
15998
16145
|
this.seqNumber = this.seqNumber + 1 & 65535;
|
|
15999
16146
|
this.send(this.connectAddress, RequestMessageType.DATA, data);
|
|
16000
16147
|
}
|
|
16148
|
+
/** Send a level-1 {@link buildStringPairCommandPayload} command on `channel`. */
|
|
16149
|
+
sendStringPairCommand(commandType, strValue, strValueSub, channel) {
|
|
16150
|
+
if (!this.connectAddress)
|
|
16151
|
+
throw new Error(`P2P session ${this.cfg.stationSn} is not connected`);
|
|
16152
|
+
const data = Buffer.concat([
|
|
16153
|
+
buildCommandHeader(this.seqNumber, commandType),
|
|
16154
|
+
buildStringPairCommandPayload(strValue, strValueSub, channel, this.level1Key, 1)
|
|
16155
|
+
]);
|
|
16156
|
+
this.seqNumber = this.seqNumber + 1 & 65535;
|
|
16157
|
+
this.send(this.connectAddress, RequestMessageType.DATA, data);
|
|
16158
|
+
}
|
|
16001
16159
|
/**
|
|
16002
16160
|
* Send a **level-2 (AES-256-GCM, signCode 8) control payload** to a HomeBase-attached device. The
|
|
16003
16161
|
* target camera is selected by `channel` (= device_channel) + the `mChannel` envelope — the same
|
|
@@ -16275,11 +16433,6 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16275
16433
|
this.seqNumber = this.seqNumber + 1 & 65535;
|
|
16276
16434
|
this.send(this.connectAddress, RequestMessageType.DATA, data);
|
|
16277
16435
|
}
|
|
16278
|
-
/**
|
|
16279
|
-
* Encrypt a level-2 command body (signCode 8): `tag(16) ‖ nonce(12) ‖ [seq,03,02,01](4) ‖
|
|
16280
|
-
* ciphertext`, AES-256-GCM under the negotiated session key, AAD "eufy security". Inverse of
|
|
16281
|
-
* `decryptLevel2`. The 4-byte sub-header is cleartext (skipped on decrypt); `seq` is a counter.
|
|
16282
|
-
*/
|
|
16283
16436
|
/**
|
|
16284
16437
|
* Decode a `CMD_VIDEO_FRAME` (1300) payload into clean Annex-B H.264 (the 22-byte frame header
|
|
16285
16438
|
* stripped). Reversed from the V6 app + live H.264 captures: the 22-byte header is
|
|
@@ -16346,6 +16499,11 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16346
16499
|
}
|
|
16347
16500
|
return this.rsaModulusHex;
|
|
16348
16501
|
}
|
|
16502
|
+
/**
|
|
16503
|
+
* Encrypt a level-2 command body (signCode 8): `tag(16) ‖ nonce(12) ‖ seq(4) ‖ ciphertext`,
|
|
16504
|
+
* AES-256-GCM under the negotiated session key, AAD "eufy security". Inverse of `decryptLevel2`.
|
|
16505
|
+
* The 4-byte sub-header is cleartext (skipped on decrypt); `seq` counts up from {@link LEVEL2_SEQ_BASE}.
|
|
16506
|
+
*/
|
|
16349
16507
|
encryptLevel2(plaintext) {
|
|
16350
16508
|
if (!this.level2Key)
|
|
16351
16509
|
return void 0;
|
|
@@ -16353,8 +16511,8 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16353
16511
|
const c = createCipheriv3("aes-256-gcm", this.level2Key, nonce);
|
|
16354
16512
|
c.setAAD(GCM_AAD);
|
|
16355
16513
|
const ct = Buffer.concat([c.update(plaintext), c.final()]);
|
|
16356
|
-
const sub2 = Buffer.
|
|
16357
|
-
|
|
16514
|
+
const sub2 = Buffer.alloc(4);
|
|
16515
|
+
sub2.writeUInt32LE(LEVEL2_SEQ_BASE + this.level2Seq++ >>> 0);
|
|
16358
16516
|
return Buffer.concat([c.getAuthTag(), nonce, sub2, ct]);
|
|
16359
16517
|
}
|
|
16360
16518
|
/** Send a no-arg command frame (e.g. CMD_GATEWAYINFO) on a channel (default: the station channel). */
|
|
@@ -16400,8 +16558,14 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16400
16558
|
* The HomeBase streams back `CMD_DATABASE` (1306) frames `{cmd:10000,count,data:[…]}`,
|
|
16401
16559
|
* level-1-encrypted — decoded and emitted as `dbChunk` (decrypted text) per frame.
|
|
16402
16560
|
* Tables: `familiar_faces`, `person_basic_info`, `event_person_list`, `history_record_info`.
|
|
16561
|
+
*
|
|
16562
|
+
* Throws while a {@link readDatabase} is accumulating: every table answers `{data:[…]}` and the
|
|
16563
|
+
* frames tie no chunk to its request, so a second query's reply would be assembled into the first
|
|
16564
|
+
* one's buffer and answered as its rows.
|
|
16403
16565
|
*/
|
|
16404
16566
|
queryDatabase(table, opts = {}) {
|
|
16567
|
+
if (this.dbReadInFlight)
|
|
16568
|
+
throw new Error(`queryDatabase: ${this.cfg.stationSn} is already reading a table`);
|
|
16405
16569
|
if (!this.connectAddress)
|
|
16406
16570
|
throw new Error("not connected");
|
|
16407
16571
|
const channel = opts.channel ?? STATION_CHANNEL3;
|
|
@@ -16464,6 +16628,49 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16464
16628
|
query: this.fullTableQuery()
|
|
16465
16629
|
});
|
|
16466
16630
|
}
|
|
16631
|
+
/**
|
|
16632
|
+
* Query one on-station table and answer its rows, once the reply is whole.
|
|
16633
|
+
*
|
|
16634
|
+
* The request half of {@link queryDatabase} with its reply assembled: `CMD_DATABASE` arrives as
|
|
16635
|
+
* several frames whose decrypted text is a fragment of one document, so the fragments are
|
|
16636
|
+
* accumulated here and scanned after each one. Answers that document's `data` rows. Rejects when
|
|
16637
|
+
* `signal` aborts, and when `timeoutMs` (default {@link DB_TABLE_TIMEOUT_MS}) elapses with no
|
|
16638
|
+
* complete reply.
|
|
16639
|
+
*
|
|
16640
|
+
* One read at a time per session: while one is accumulating, every {@link queryDatabase} on the
|
|
16641
|
+
* session throws, so a second reply cannot land in this buffer.
|
|
16642
|
+
*/
|
|
16643
|
+
async readDatabase(table, opts = {}) {
|
|
16644
|
+
if (opts.signal?.aborted)
|
|
16645
|
+
throw new Error("readDatabase: aborted");
|
|
16646
|
+
this.queryDatabase(table, { accountId: opts.accountId, query: this.fullTableQuery() });
|
|
16647
|
+
this.dbReadInFlight = true;
|
|
16648
|
+
try {
|
|
16649
|
+
return await new Promise((resolve, reject) => {
|
|
16650
|
+
let text2 = "";
|
|
16651
|
+
const onChunk = (chunk) => {
|
|
16652
|
+
text2 += chunk.text;
|
|
16653
|
+
const rows = firstTableRows(text2);
|
|
16654
|
+
if (rows)
|
|
16655
|
+
settle(() => resolve(rows));
|
|
16656
|
+
};
|
|
16657
|
+
const timer = setTimeout(() => settle(() => reject(new Error(`readDatabase: ${this.cfg.stationSn} sent no complete ${table}`))), opts.timeoutMs ?? DB_TABLE_TIMEOUT_MS);
|
|
16658
|
+
const onAbort = () => settle(() => reject(new Error("readDatabase: aborted")));
|
|
16659
|
+
const settle = (finish) => {
|
|
16660
|
+
clearTimeout(timer);
|
|
16661
|
+
this.off("dbChunk", onChunk);
|
|
16662
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
16663
|
+
finish();
|
|
16664
|
+
};
|
|
16665
|
+
this.on("dbChunk", onChunk);
|
|
16666
|
+
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
16667
|
+
});
|
|
16668
|
+
} finally {
|
|
16669
|
+
this.dbReadInFlight = false;
|
|
16670
|
+
}
|
|
16671
|
+
}
|
|
16672
|
+
/** Whether a {@link readDatabase} is accumulating; see {@link queryDatabase} for what it bars. */
|
|
16673
|
+
dbReadInFlight = false;
|
|
16467
16674
|
/**
|
|
16468
16675
|
* Request the **face feature rows** over P2P (`face_feature_info`, inner `cmd 10000`). Each row
|
|
16469
16676
|
* carries `{person_id, face_name, face_id, face_picture_content, face_feature_file_path}` — where
|
|
@@ -16531,7 +16738,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16531
16738
|
* if the missing one arrives. A logical frame's payload spans datagrams that carry no header of their
|
|
16532
16739
|
* own, so reassembling around a hole is impossible; waiting for it is what keeps the frame whole.
|
|
16533
16740
|
*
|
|
16534
|
-
* Only once
|
|
16741
|
+
* Only once the data type's reorder wait passes, or {@link REORDER_MAX_DATAGRAMS} pile up, is the datagram
|
|
16535
16742
|
* treated as lost: a pending frame is discarded and delivery resumes from the earliest held datagram.
|
|
16536
16743
|
*/
|
|
16537
16744
|
onData(msg, addr) {
|
|
@@ -16586,7 +16793,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16586
16793
|
const timer = setTimeout(() => {
|
|
16587
16794
|
reorder.timer = void 0;
|
|
16588
16795
|
this.abandonHole(dataType);
|
|
16589
|
-
}, REORDER_WAIT_MS);
|
|
16796
|
+
}, dataType === P2PDataType.VIDEO ? VIDEO_REORDER_WAIT_MS : REORDER_WAIT_MS);
|
|
16590
16797
|
timer.unref?.();
|
|
16591
16798
|
reorder.timer = timer;
|
|
16592
16799
|
}
|
|
@@ -18176,11 +18383,195 @@ function annexbToJpeg(annexb, spawn2) {
|
|
|
18176
18383
|
});
|
|
18177
18384
|
}
|
|
18178
18385
|
|
|
18386
|
+
// dist/transport/p2p/recording-download.js
|
|
18387
|
+
import { createDecipheriv as createDecipheriv7 } from "node:crypto";
|
|
18388
|
+
var RECORDING_DATA_TYPE = 3;
|
|
18389
|
+
var VIDEO_HEADER_LEN2 = 22;
|
|
18390
|
+
var AUDIO_HEADER_LEN2 = 16;
|
|
18391
|
+
var AUDIO_NONCE_START = 32;
|
|
18392
|
+
var AUDIO_BODY_START = 44;
|
|
18393
|
+
var AUDIO_CODEC_AAC_LC = 0;
|
|
18394
|
+
var FINISH_CHANNEL = 0;
|
|
18395
|
+
var FIRST_FRAME_WAIT_MS = 2e4;
|
|
18396
|
+
var IDLE_END_MS = 15e3;
|
|
18397
|
+
var DEFAULT_TRANSFER_MS = 12e4;
|
|
18398
|
+
var MAX_TRANSFER_BYTES = 32 * 1024 * 1024;
|
|
18399
|
+
function homeBase2RecordingPath(channel, recording) {
|
|
18400
|
+
if (!/^\d{14}$/.test(recording) || !Number.isInteger(channel) || channel < 0 || channel > 99)
|
|
18401
|
+
return void 0;
|
|
18402
|
+
return `/media/mmcblk0p1/Camera${String(channel).padStart(2, "0")}/${recording}.dat`;
|
|
18403
|
+
}
|
|
18404
|
+
function receiveRecording(session, request) {
|
|
18405
|
+
let resolveFrames;
|
|
18406
|
+
let rejectFrames;
|
|
18407
|
+
const frames = new Promise((resolve, reject) => {
|
|
18408
|
+
resolveFrames = resolve;
|
|
18409
|
+
rejectFrames = reject;
|
|
18410
|
+
});
|
|
18411
|
+
let resolveDrained;
|
|
18412
|
+
const drained = new Promise((resolve) => resolveDrained = resolve);
|
|
18413
|
+
const limitMs = request.timeoutMs ?? DEFAULT_TRANSFER_MS;
|
|
18414
|
+
let collected = [];
|
|
18415
|
+
const started = Date.now();
|
|
18416
|
+
let lastFrameAt = started;
|
|
18417
|
+
let received = false;
|
|
18418
|
+
let bytes2 = 0;
|
|
18419
|
+
let draining;
|
|
18420
|
+
let delivered = false;
|
|
18421
|
+
let listening = true;
|
|
18422
|
+
const deliver = (error) => {
|
|
18423
|
+
if (delivered)
|
|
18424
|
+
return;
|
|
18425
|
+
delivered = true;
|
|
18426
|
+
request.signal?.removeEventListener("abort", onAbort);
|
|
18427
|
+
if (error)
|
|
18428
|
+
rejectFrames(error);
|
|
18429
|
+
else
|
|
18430
|
+
resolveFrames(collected);
|
|
18431
|
+
};
|
|
18432
|
+
const stop = () => {
|
|
18433
|
+
if (!listening)
|
|
18434
|
+
return;
|
|
18435
|
+
listening = false;
|
|
18436
|
+
clearInterval(tick);
|
|
18437
|
+
session.off("data", onData);
|
|
18438
|
+
resolveDrained();
|
|
18439
|
+
};
|
|
18440
|
+
const incomplete = (why) => deliver(new RecordingDownloadError("incomplete", `the recording transfer ${why} before its finish frame`));
|
|
18441
|
+
const drain = (why) => {
|
|
18442
|
+
draining ??= why;
|
|
18443
|
+
collected = [];
|
|
18444
|
+
};
|
|
18445
|
+
const onData = (frame) => {
|
|
18446
|
+
if (frame.commandId === CommandType.CMD_DOWNLOAD_FINISH) {
|
|
18447
|
+
if (frame.channel !== request.channel && frame.channel !== FINISH_CHANNEL)
|
|
18448
|
+
return;
|
|
18449
|
+
if (draining)
|
|
18450
|
+
incomplete(draining);
|
|
18451
|
+
else
|
|
18452
|
+
deliver();
|
|
18453
|
+
return stop();
|
|
18454
|
+
}
|
|
18455
|
+
if (frame.channel !== request.channel)
|
|
18456
|
+
return;
|
|
18457
|
+
if (frame.commandId !== CommandType.CMD_VIDEO_FRAME && frame.commandId !== CommandType.CMD_AUDIO_FRAME)
|
|
18458
|
+
return;
|
|
18459
|
+
if (frame.dataType !== RECORDING_DATA_TYPE)
|
|
18460
|
+
return;
|
|
18461
|
+
received = true;
|
|
18462
|
+
lastFrameAt = Date.now();
|
|
18463
|
+
if (draining)
|
|
18464
|
+
return;
|
|
18465
|
+
collected.push({ commandId: frame.commandId, signCode: frame.signCode, raw: Buffer.from(frame.raw) });
|
|
18466
|
+
bytes2 += frame.raw.length;
|
|
18467
|
+
if (bytes2 >= MAX_TRANSFER_BYTES)
|
|
18468
|
+
drain(`reached ${MAX_TRANSFER_BYTES} bytes`);
|
|
18469
|
+
};
|
|
18470
|
+
const onAbort = () => {
|
|
18471
|
+
deliver(request.signal?.reason instanceof Error ? request.signal.reason : new Error("aborted"));
|
|
18472
|
+
drain("was aborted");
|
|
18473
|
+
};
|
|
18474
|
+
const tick = setInterval(() => {
|
|
18475
|
+
const now2 = Date.now();
|
|
18476
|
+
if (!received && now2 - started > FIRST_FRAME_WAIT_MS) {
|
|
18477
|
+
deliver(new RecordingDownloadError("no-data", `the station sent nothing within ${FIRST_FRAME_WAIT_MS} ms`));
|
|
18478
|
+
stop();
|
|
18479
|
+
} else if (received && now2 - lastFrameAt > IDLE_END_MS) {
|
|
18480
|
+
incomplete(draining ?? `went quiet for ${IDLE_END_MS} ms`);
|
|
18481
|
+
stop();
|
|
18482
|
+
} else if (now2 - started > 2 * limitMs && draining) {
|
|
18483
|
+
incomplete(draining);
|
|
18484
|
+
stop();
|
|
18485
|
+
} else if (now2 - started > limitMs) {
|
|
18486
|
+
drain(`ran past ${limitMs} ms`);
|
|
18487
|
+
}
|
|
18488
|
+
}, 250);
|
|
18489
|
+
if (request.signal?.aborted) {
|
|
18490
|
+
onAbort();
|
|
18491
|
+
stop();
|
|
18492
|
+
return { frames, drained };
|
|
18493
|
+
}
|
|
18494
|
+
request.signal?.addEventListener("abort", onAbort, { once: true });
|
|
18495
|
+
session.on("data", onData);
|
|
18496
|
+
try {
|
|
18497
|
+
session.sendStringPairCommand(CommandType.CMD_DOWNLOAD_VIDEO, request.path, request.accountId, request.channel);
|
|
18498
|
+
} catch (error) {
|
|
18499
|
+
deliver(error instanceof Error ? error : new Error(String(error)));
|
|
18500
|
+
stop();
|
|
18501
|
+
}
|
|
18502
|
+
return { frames, drained };
|
|
18503
|
+
}
|
|
18504
|
+
function openAudio(key, nonce, tag2, body) {
|
|
18505
|
+
try {
|
|
18506
|
+
const decipher = createDecipheriv7("aes-256-gcm", key, nonce);
|
|
18507
|
+
decipher.setAAD(VIDEO_GCM_AAD);
|
|
18508
|
+
decipher.setAuthTag(tag2);
|
|
18509
|
+
return Buffer.concat([decipher.update(body), decipher.final()]);
|
|
18510
|
+
} catch {
|
|
18511
|
+
return void 0;
|
|
18512
|
+
}
|
|
18513
|
+
}
|
|
18514
|
+
function decodeRecording(frames, eccPrivateKeyHex) {
|
|
18515
|
+
const eccPrivateKey = Buffer.from(eccPrivateKeyHex, "hex");
|
|
18516
|
+
if (eccPrivateKey.length !== 32)
|
|
18517
|
+
throw new RecordingDownloadError("undecodable", "the cipher key is not a P-256 key");
|
|
18518
|
+
const decoder = new VideoFrameDecoder(eccPrivateKey);
|
|
18519
|
+
const video = [];
|
|
18520
|
+
const audio = [];
|
|
18521
|
+
const numbers = /* @__PURE__ */ new Set();
|
|
18522
|
+
let firstStamp;
|
|
18523
|
+
let lastStamp;
|
|
18524
|
+
for (const { commandId, raw } of frames) {
|
|
18525
|
+
if (commandId === CommandType.CMD_VIDEO_FRAME) {
|
|
18526
|
+
const header = parseVideoFrameHeader(raw);
|
|
18527
|
+
if (!header)
|
|
18528
|
+
continue;
|
|
18529
|
+
const h264 = header.keyframe ? decoder.decodeFrame(raw)?.h264 : raw.length >= VIDEO_HEADER_LEN2 + header.payloadLength ? raw.subarray(VIDEO_HEADER_LEN2, VIDEO_HEADER_LEN2 + header.payloadLength) : void 0;
|
|
18530
|
+
if (!h264?.length)
|
|
18531
|
+
continue;
|
|
18532
|
+
video.push(h264);
|
|
18533
|
+
numbers.add(header.sequence);
|
|
18534
|
+
const stamp = raw.readUIntLE(14, 6);
|
|
18535
|
+
firstStamp ??= stamp;
|
|
18536
|
+
lastStamp = stamp;
|
|
18537
|
+
} else if (commandId === CommandType.CMD_AUDIO_FRAME) {
|
|
18538
|
+
const mediaKey = decoder.currentMediaKey;
|
|
18539
|
+
if (!mediaKey || raw.length < AUDIO_BODY_START || raw[5] !== AUDIO_CODEC_AAC_LC)
|
|
18540
|
+
continue;
|
|
18541
|
+
const len = raw.readUInt32LE(0);
|
|
18542
|
+
if (raw.length < AUDIO_BODY_START + len)
|
|
18543
|
+
continue;
|
|
18544
|
+
const aac = openAudio(mediaKey, raw.subarray(AUDIO_NONCE_START, AUDIO_BODY_START), raw.subarray(AUDIO_HEADER_LEN2, AUDIO_NONCE_START), raw.subarray(AUDIO_BODY_START, AUDIO_BODY_START + len));
|
|
18545
|
+
if (aac?.length)
|
|
18546
|
+
audio.push(aac);
|
|
18547
|
+
}
|
|
18548
|
+
}
|
|
18549
|
+
if (!video.length)
|
|
18550
|
+
throw new RecordingDownloadError("undecodable", "no video frame of the recording decoded");
|
|
18551
|
+
const ordered = [...numbers];
|
|
18552
|
+
let missingFrames = 0;
|
|
18553
|
+
for (let i = 1; i < ordered.length; i++) {
|
|
18554
|
+
const step = ordered[i] - ordered[i - 1] & 65535;
|
|
18555
|
+
if (step > 1 && step < 32768)
|
|
18556
|
+
missingFrames += step - 1;
|
|
18557
|
+
}
|
|
18558
|
+
const span = ordered.length > 1 ? ordered[ordered.length - 1] - ordered[0] & 65535 : 0;
|
|
18559
|
+
const durationMs = (lastStamp ?? 0) - (firstStamp ?? 0);
|
|
18560
|
+
return {
|
|
18561
|
+
video: Buffer.concat(video),
|
|
18562
|
+
...audio.length ? { audio: Buffer.concat(audio) } : {},
|
|
18563
|
+
frames: ordered.length,
|
|
18564
|
+
missingFrames,
|
|
18565
|
+
durationMs,
|
|
18566
|
+
fps: durationMs > 0 && span > 0 ? Math.round(span * 1e5 / durationMs) / 100 : 0
|
|
18567
|
+
};
|
|
18568
|
+
}
|
|
18569
|
+
|
|
18179
18570
|
// dist/transport/p2p/shared-live-source.js
|
|
18180
18571
|
import { EventEmitter as EventEmitter4 } from "node:events";
|
|
18181
|
-
|
|
18182
|
-
|
|
18183
|
-
return Number.isFinite(
|
|
18572
|
+
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
18573
|
+
function timerMs(value, fallback) {
|
|
18574
|
+
return value !== void 0 && Number.isFinite(value) && value >= 1 && value <= MAX_TIMER_DELAY_MS ? value : fallback;
|
|
18184
18575
|
}
|
|
18185
18576
|
function sameConfig(a, b) {
|
|
18186
18577
|
return b !== void 0 && a.codec === b.codec && a.width === b.width && a.height === b.height;
|
|
@@ -18433,14 +18824,14 @@ var SharedLiveSource = class {
|
|
|
18433
18824
|
tag;
|
|
18434
18825
|
constructor(opts) {
|
|
18435
18826
|
this.opts = opts;
|
|
18436
|
-
this.lingerMs = opts.lingerMs
|
|
18827
|
+
this.lingerMs = opts.lingerMs === 0 ? 1 : timerMs(opts.lingerMs, 8e3);
|
|
18437
18828
|
this.maxQueue = opts.maxQueue ?? 900;
|
|
18438
|
-
this.preBufferMs =
|
|
18439
|
-
this.warmRetryMs = opts.warmRetryMs
|
|
18440
|
-
this.warmTimeoutMs = opts.warmTimeoutMs
|
|
18829
|
+
this.preBufferMs = timerMs((opts.preBufferSeconds ?? 0) * 1e3, 0);
|
|
18830
|
+
this.warmRetryMs = timerMs(opts.warmRetryMs, 2e3);
|
|
18831
|
+
this.warmTimeoutMs = timerMs(opts.warmTimeoutMs, 2e4);
|
|
18441
18832
|
this.powered = opts.powered ?? "wired";
|
|
18442
|
-
this.batteryBudgetMs = opts.batteryBudgetMs
|
|
18443
|
-
this.budgetGraceMs = opts.budgetGraceMs
|
|
18833
|
+
this.batteryBudgetMs = timerMs(opts.batteryBudgetMs, 45e3);
|
|
18834
|
+
this.budgetGraceMs = timerMs(opts.budgetGraceMs, 1e4);
|
|
18444
18835
|
this.logger = opts.logger ?? noopLogger;
|
|
18445
18836
|
this.tag = opts.label ? `[live ${opts.label}]` : "[live]";
|
|
18446
18837
|
this.traceId = `pull-${++pullSequence}`;
|
|
@@ -18688,7 +19079,7 @@ var SharedLiveSource = class {
|
|
|
18688
19079
|
if (this.disposed || !this.stream)
|
|
18689
19080
|
return;
|
|
18690
19081
|
this.clearBudget();
|
|
18691
|
-
this.budgetTimer.arm(ms
|
|
19082
|
+
this.budgetTimer.arm(timerMs(ms, this.batteryBudgetMs), () => this.onBudgetExpire());
|
|
18692
19083
|
}
|
|
18693
19084
|
/** Settle a reuse watch on any frame — the join already holds a decodable picture. */
|
|
18694
19085
|
settleReuseWatch() {
|
|
@@ -18829,7 +19220,7 @@ var SharedLiveSource = class {
|
|
|
18829
19220
|
return this.bufferedMedia(seconds).filter((item) => item.kind === "video").map((item) => item.frame);
|
|
18830
19221
|
}
|
|
18831
19222
|
bufferedMedia(seconds) {
|
|
18832
|
-
const requested = Math.min(
|
|
19223
|
+
const requested = Number.isFinite(seconds) ? Math.min(seconds * 1e3, this.preBufferMs) : 0;
|
|
18833
19224
|
if (requested <= 0 || !this.ring.length)
|
|
18834
19225
|
return [];
|
|
18835
19226
|
const start = this.windowStart(Date.now() - requested);
|
|
@@ -20356,6 +20747,8 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
20356
20747
|
talkbacks = /* @__PURE__ */ new Map();
|
|
20357
20748
|
/** cipher_id → ECC private key (one eufylife get_ciphers call per cipher), shared across (re)opens. */
|
|
20358
20749
|
cipherKeyCache = /* @__PURE__ */ new Map();
|
|
20750
|
+
/** The recording download in flight per station; a station serves one at a time. */
|
|
20751
|
+
recordingDownloads = /* @__PURE__ */ new Map();
|
|
20359
20752
|
constructor(deps) {
|
|
20360
20753
|
this.deps = deps;
|
|
20361
20754
|
this.manager = new SessionManager({
|
|
@@ -20568,28 +20961,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
20568
20961
|
localAddress,
|
|
20569
20962
|
dskKey,
|
|
20570
20963
|
noBroadcast: this.deps.noBroadcast,
|
|
20571
|
-
resolveCipherKey:
|
|
20572
|
-
if (this.cipherKeyCache.has(cipherId))
|
|
20573
|
-
return this.cipherKeyCache.get(cipherId);
|
|
20574
|
-
let ecc;
|
|
20575
|
-
try {
|
|
20576
|
-
const ciphers = await this.deps.mega.getCiphers([cipherId], adminUserId, stationSn);
|
|
20577
|
-
ecc = ciphers.find((c) => Number(c.cipher_id) === cipherId)?.ecc_private_key;
|
|
20578
|
-
if (ecc === void 0 && ciphers[0]?.ecc_private_key !== void 0) {
|
|
20579
|
-
ecc = ciphers[0].ecc_private_key;
|
|
20580
|
-
this.traceOnStation(session, {
|
|
20581
|
-
phase: "cipher-fallback",
|
|
20582
|
-
cipherId,
|
|
20583
|
-
answeredCipherId: Number(ciphers[0].cipher_id)
|
|
20584
|
-
});
|
|
20585
|
-
}
|
|
20586
|
-
} catch (e) {
|
|
20587
|
-
this.deps.onError(e instanceof Error ? e : new Error(String(e)));
|
|
20588
|
-
}
|
|
20589
|
-
if (ecc !== void 0)
|
|
20590
|
-
this.cipherKeyCache.set(cipherId, ecc);
|
|
20591
|
-
return ecc;
|
|
20592
|
-
},
|
|
20964
|
+
resolveCipherKey: (cipherId) => this.cipherKeyFor(session, cipherId, adminUserId, stationSn),
|
|
20593
20965
|
logger: this.deps.logger ?? noopLogger
|
|
20594
20966
|
});
|
|
20595
20967
|
session.on("error", (e) => this.reportError(e));
|
|
@@ -20613,6 +20985,33 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
20613
20985
|
session.on("data", (f) => this.deps.onFrame(stationSn, f));
|
|
20614
20986
|
return session;
|
|
20615
20987
|
}
|
|
20988
|
+
/**
|
|
20989
|
+
* The ECC private key of `cipherId`, from the router's cache or one `get_ciphers` call. When the cloud
|
|
20990
|
+
* answers with another cipher, that one is used and traced on `session`. Only a successful lookup is
|
|
20991
|
+
* cached.
|
|
20992
|
+
*/
|
|
20993
|
+
async cipherKeyFor(session, cipherId, userId, stationSn) {
|
|
20994
|
+
if (this.cipherKeyCache.has(cipherId))
|
|
20995
|
+
return this.cipherKeyCache.get(cipherId);
|
|
20996
|
+
let ecc;
|
|
20997
|
+
try {
|
|
20998
|
+
const ciphers = await this.deps.mega.getCiphers([cipherId], userId, stationSn);
|
|
20999
|
+
ecc = ciphers.find((c) => Number(c.cipher_id) === cipherId)?.ecc_private_key;
|
|
21000
|
+
if (ecc === void 0 && ciphers[0]?.ecc_private_key !== void 0) {
|
|
21001
|
+
ecc = ciphers[0].ecc_private_key;
|
|
21002
|
+
this.traceOnStation(session, {
|
|
21003
|
+
phase: "cipher-fallback",
|
|
21004
|
+
cipherId,
|
|
21005
|
+
answeredCipherId: Number(ciphers[0].cipher_id)
|
|
21006
|
+
});
|
|
21007
|
+
}
|
|
21008
|
+
} catch (e) {
|
|
21009
|
+
this.deps.onError(e instanceof Error ? e : new Error(String(e)));
|
|
21010
|
+
}
|
|
21011
|
+
if (ecc !== void 0)
|
|
21012
|
+
this.cipherKeyCache.set(cipherId, ecc);
|
|
21013
|
+
return ecc;
|
|
21014
|
+
}
|
|
20616
21015
|
/**
|
|
20617
21016
|
* Open (or reuse) a station's P2P session and await its completed handshake. An optional abort only
|
|
20618
21017
|
* stops this wait; session ownership remains with {@link SessionManager} and its normal teardown.
|
|
@@ -20740,7 +21139,9 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
20740
21139
|
* start has no level-1 form for.
|
|
20741
21140
|
*/
|
|
20742
21141
|
mediaProviderFor(sn) {
|
|
21142
|
+
const recordings = this.downloadsRecordings(sn) ? { downloadRecording: (opts) => this.downloadRecording(sn, opts) } : {};
|
|
20743
21143
|
return {
|
|
21144
|
+
...recordings,
|
|
20744
21145
|
snapshotLive: async (opts) => {
|
|
20745
21146
|
const source = await this.sharedLiveSourceFor(sn, opts ?? {});
|
|
20746
21147
|
return captureSnapshotFromShared(source, {
|
|
@@ -20779,6 +21180,63 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
20779
21180
|
}
|
|
20780
21181
|
};
|
|
20781
21182
|
}
|
|
21183
|
+
/**
|
|
21184
|
+
* Whether `sn` is a camera attached to a HomeBase 2 (T8010), the one station whose recording path layout
|
|
21185
|
+
* and frame formats are confirmed.
|
|
21186
|
+
*/
|
|
21187
|
+
downloadsRecordings(sn) {
|
|
21188
|
+
const dev = this.recordFor(sn);
|
|
21189
|
+
if (!dev)
|
|
21190
|
+
return false;
|
|
21191
|
+
const parentSn = stationOf(dev);
|
|
21192
|
+
return parentSn !== sn && this.recordFor(parentSn)?.model === "T8010";
|
|
21193
|
+
}
|
|
21194
|
+
/**
|
|
21195
|
+
* Download one recording a HomeBase 2 holds for an attached camera, then decode it
|
|
21196
|
+
* ({@link decodeRecording}). Downloads queue per station: the station streams one recording at a time
|
|
21197
|
+
* on the camera's channel, so a download starts only once the previous one has drained
|
|
21198
|
+
* ({@link RecordingTransfer.drained}), even when that one's caller already has its answer. An abort
|
|
21199
|
+
* while waiting rejects without sending anything; an abort mid-transfer rejects at once and the station's
|
|
21200
|
+
* turn is held until the transfer drains.
|
|
21201
|
+
*/
|
|
21202
|
+
async downloadRecording(sn, opts) {
|
|
21203
|
+
opts.signal?.throwIfAborted();
|
|
21204
|
+
const parentSn = this.stationKeyOf(sn);
|
|
21205
|
+
const previous = this.recordingDownloads.get(parentSn) ?? Promise.resolve();
|
|
21206
|
+
const started = this.waitTurn(previous, opts.signal).then(() => this.startRecordingDownload(sn, opts));
|
|
21207
|
+
const turn = Promise.allSettled([previous, started.then((t) => t.drained)]).then(() => {
|
|
21208
|
+
if (this.recordingDownloads.get(parentSn) === turn)
|
|
21209
|
+
this.recordingDownloads.delete(parentSn);
|
|
21210
|
+
});
|
|
21211
|
+
this.recordingDownloads.set(parentSn, turn);
|
|
21212
|
+
return started.then((t) => t.recording);
|
|
21213
|
+
}
|
|
21214
|
+
/** Resolve once `previous` settles, or reject as soon as `signal` aborts. */
|
|
21215
|
+
waitTurn(previous, signal) {
|
|
21216
|
+
return abortable(previous.then(() => void 0, () => void 0), signal);
|
|
21217
|
+
}
|
|
21218
|
+
/**
|
|
21219
|
+
* Start one recording download, once the station's previous download has drained: the decoded recording,
|
|
21220
|
+
* and when the station is done sending it.
|
|
21221
|
+
*/
|
|
21222
|
+
async startRecordingDownload(sn, opts) {
|
|
21223
|
+
const { session, parentSn, channel, accountId } = await this.resolveSession(sn, { signal: opts.signal });
|
|
21224
|
+
const path = homeBase2RecordingPath(channel, opts.recording);
|
|
21225
|
+
if (!path)
|
|
21226
|
+
throw new RecordingDownloadError("invalid-recording", `no recording named ${opts.recording}`);
|
|
21227
|
+
const eccKey = await this.cipherKeyFor(session, opts.cipherId, accountId, parentSn);
|
|
21228
|
+
if (!eccKey) {
|
|
21229
|
+
throw new RecordingDownloadError("key-unavailable", `cipher ${opts.cipherId} could not be obtained`);
|
|
21230
|
+
}
|
|
21231
|
+
const transfer = receiveRecording(session, {
|
|
21232
|
+
path,
|
|
21233
|
+
accountId,
|
|
21234
|
+
channel,
|
|
21235
|
+
timeoutMs: opts.timeoutMs,
|
|
21236
|
+
signal: opts.signal
|
|
21237
|
+
});
|
|
21238
|
+
return { recording: transfer.frames.then((frames) => decodeRecording(frames, eccKey)), drained: transfer.drained };
|
|
21239
|
+
}
|
|
20782
21240
|
/**
|
|
20783
21241
|
* Open a {@link Talkback} on a device's camera channel.
|
|
20784
21242
|
*
|
|
@@ -21127,9 +21585,9 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
21127
21585
|
*
|
|
21128
21586
|
* Reached two ways, both idempotent: the session's own `close` event, when it died while still the
|
|
21129
21587
|
* station's registered session, and {@link SessionManagerOpts.onAutoClose}, when the manager closed it
|
|
21130
|
-
* unasked. A close a CALLER made is deliberately not routed here — {@link closeAll}
|
|
21131
|
-
* sources first,
|
|
21132
|
-
* on the replacement session.
|
|
21588
|
+
* unasked. A close a CALLER made is deliberately not routed here — {@link closeAll} and
|
|
21589
|
+
* {@link resolveSession} dispose their own sources first, while {@link replaceUnreachableSession} keeps
|
|
21590
|
+
* its source alive on purpose to rewarm it on the replacement session.
|
|
21133
21591
|
*/
|
|
21134
21592
|
tearDownStation(stationSn) {
|
|
21135
21593
|
this.manager.remove(stationSn);
|
|
@@ -21448,9 +21906,10 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
21448
21906
|
*
|
|
21449
21907
|
* A session whose {@link P2PSession.pathAnswering} is false is closed and re-resolved before it is handed
|
|
21450
21908
|
* over: the station answers every heartbeat, so a path silent past several of them is gone. A session
|
|
21451
|
-
* reporting nothing about its path is not reporting that evidence and is handed over as it is.
|
|
21452
|
-
*
|
|
21453
|
-
*
|
|
21909
|
+
* reporting nothing about its path is not reporting that evidence and is handed over as it is. Cached live
|
|
21910
|
+
* sources on the replaced session are discarded before closing it (see {@link tearDownStation}).
|
|
21911
|
+
* Replaced at most once per resolution, so a station whose replacement is silent too is returned rather
|
|
21912
|
+
* than closed again.
|
|
21454
21913
|
*/
|
|
21455
21914
|
async resolveSession(sn, opts = {}, rebuilt = false) {
|
|
21456
21915
|
const dev = await this.deviceFor(sn);
|
|
@@ -21464,6 +21923,10 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
21464
21923
|
}
|
|
21465
21924
|
if (session.pathAnswering === false && !rebuilt) {
|
|
21466
21925
|
(this.deps.logger ?? noopLogger).debug(`[p2p] ${parentSn} path stopped answering \u2014 rebuilding before use`);
|
|
21926
|
+
for (const [key, sessionKey] of [...this.liveSessionKeys]) {
|
|
21927
|
+
if (sessionKey === parentSn)
|
|
21928
|
+
this.dropLiveSource(key);
|
|
21929
|
+
}
|
|
21467
21930
|
await this.manager.close(parentSn).catch((error) => this.reportError(error instanceof Error ? error : new Error(String(error))));
|
|
21468
21931
|
return await this.resolveSession(sn, opts, true);
|
|
21469
21932
|
}
|
|
@@ -24304,6 +24767,7 @@ var MEDIA_FAILURE_REASONS = [
|
|
|
24304
24767
|
// dist/transport/stored-image-cache.js
|
|
24305
24768
|
var MAX_JPEG_BYTES = 10 * 1024 * 1024;
|
|
24306
24769
|
var MAX_REMEMBERED_URLS = 64;
|
|
24770
|
+
var NOT_YET_PUBLISHED_RETRY_MS = [2e3, 4e3];
|
|
24307
24771
|
var DIAGNOSTIC_INTERVAL_MS = 6e4;
|
|
24308
24772
|
var StoredImageCache = class {
|
|
24309
24773
|
downloader;
|
|
@@ -24341,6 +24805,8 @@ var StoredImageCache = class {
|
|
|
24341
24805
|
if (state.seenUrls.has(url))
|
|
24342
24806
|
return;
|
|
24343
24807
|
state.seenUrls.add(url);
|
|
24808
|
+
clearTimeout(state.retrying);
|
|
24809
|
+
state.retrying = void 0;
|
|
24344
24810
|
while (state.seenUrls.size > MAX_REMEMBERED_URLS) {
|
|
24345
24811
|
const oldest = state.seenUrls.values().next();
|
|
24346
24812
|
if (oldest.done)
|
|
@@ -24352,14 +24818,15 @@ var StoredImageCache = class {
|
|
|
24352
24818
|
url,
|
|
24353
24819
|
sequence: ++state.nextSequence,
|
|
24354
24820
|
observedAt: this.clock(),
|
|
24355
|
-
generation: this.generation
|
|
24821
|
+
generation: this.generation,
|
|
24822
|
+
retries: 0
|
|
24356
24823
|
};
|
|
24357
24824
|
this.pump();
|
|
24358
24825
|
}
|
|
24359
24826
|
/** Return retained bytes without starting or awaiting network work. */
|
|
24360
24827
|
snapshotStored(deviceKey) {
|
|
24361
24828
|
const state = this.devices.get(deviceKey);
|
|
24362
|
-
const reason = state?.queued || state?.inFlight ? "pending" : state?.reason ?? "not-observed";
|
|
24829
|
+
const reason = state?.queued || state?.inFlight || state?.retrying ? "pending" : state?.reason ?? "not-observed";
|
|
24363
24830
|
if (reason !== "pending" && state?.lifecycleError)
|
|
24364
24831
|
return Promise.reject(state.lifecycleError);
|
|
24365
24832
|
if (state?.retained)
|
|
@@ -24393,6 +24860,8 @@ var StoredImageCache = class {
|
|
|
24393
24860
|
if (error !== void 0 && this.isLifecycleError(error)) {
|
|
24394
24861
|
state.lifecycleError = error;
|
|
24395
24862
|
state.reason = void 0;
|
|
24863
|
+
} else if (image === void 0 && this.retryLater(state, candidate, error)) {
|
|
24864
|
+
state.lifecycleError = void 0;
|
|
24396
24865
|
} else if (image === void 0) {
|
|
24397
24866
|
state.lifecycleError = void 0;
|
|
24398
24867
|
state.reason = "download-failed";
|
|
@@ -24409,6 +24878,21 @@ var StoredImageCache = class {
|
|
|
24409
24878
|
}
|
|
24410
24879
|
this.pump();
|
|
24411
24880
|
}
|
|
24881
|
+
/** Schedule the next attempt after a 404; answers whether one was scheduled. */
|
|
24882
|
+
retryLater(state, candidate, error) {
|
|
24883
|
+
const delay = NOT_YET_PUBLISHED_RETRY_MS[candidate.retries];
|
|
24884
|
+
const tag2 = mediaFailureTag(error);
|
|
24885
|
+
if (delay === void 0 || tag2.cause !== "http-status" || tag2.status !== 404 || state.queued)
|
|
24886
|
+
return false;
|
|
24887
|
+
const timer = setTimeout(() => {
|
|
24888
|
+
state.retrying = void 0;
|
|
24889
|
+
state.queued = { ...candidate, retries: candidate.retries + 1 };
|
|
24890
|
+
this.pump();
|
|
24891
|
+
}, delay);
|
|
24892
|
+
timer.unref?.();
|
|
24893
|
+
state.retrying = timer;
|
|
24894
|
+
return true;
|
|
24895
|
+
}
|
|
24412
24896
|
isValidJpeg(image) {
|
|
24413
24897
|
return Buffer.isBuffer(image) && image.length > 0 && image.length <= MAX_JPEG_BYTES && image.length >= 5 && image[0] === 255 && image[1] === 216 && image[2] === 255 && image[image.length - 2] === 255 && image[image.length - 1] === 217;
|
|
24414
24898
|
}
|
|
@@ -25004,6 +25488,11 @@ function recordString(raw, key) {
|
|
|
25004
25488
|
const v = typeof nested === "string" && nested ? nested : raw[key];
|
|
25005
25489
|
return typeof v === "string" && v ? v : void 0;
|
|
25006
25490
|
}
|
|
25491
|
+
function adminUserIdFrom(raw) {
|
|
25492
|
+
const member = (raw ?? {}).member;
|
|
25493
|
+
const id = member?.admin_user_id;
|
|
25494
|
+
return typeof id === "string" && id ? id : void 0;
|
|
25495
|
+
}
|
|
25007
25496
|
function tuyaDevIdFrom(raw) {
|
|
25008
25497
|
for (const field of ["tuya_uuid", "tuya_virtual_id", "tuya_device_id", "virtualId"]) {
|
|
25009
25498
|
const v = recordString(raw, field);
|
|
@@ -25161,13 +25650,7 @@ var EufyMega = class extends EventEmitter9 {
|
|
|
25161
25650
|
onError: (e) => this.reportError(e),
|
|
25162
25651
|
// The `a2` account id an `eufy_life` DP frame embeds — the owning member's `admin_user_id`,
|
|
25163
25652
|
// falling back to the logged-in account's user id (the confirmed script path does the same).
|
|
25164
|
-
resolveAccountId: (dev) =>
|
|
25165
|
-
const member = (dev.raw ?? {}).member;
|
|
25166
|
-
const adminId = member?.admin_user_id;
|
|
25167
|
-
if (typeof adminId === "string" && adminId)
|
|
25168
|
-
return adminId;
|
|
25169
|
-
return this.mega.auth?.userId ?? "";
|
|
25170
|
-
},
|
|
25653
|
+
resolveAccountId: (dev) => adminUserIdFrom(dev.raw) ?? this.mega.auth?.userId ?? "",
|
|
25171
25654
|
// Fetch + parse a gallery effect from the HTTP catalogue into the serializable spec. The catalogue
|
|
25172
25655
|
// lives in the http layer (transport/http/light-catalog); the mqtt router owns the frame bytes.
|
|
25173
25656
|
resolvePreset: (presetId) => resolveLightEffect(this.mega, presetId),
|
|
@@ -26614,6 +27097,37 @@ var EufyMega = class extends EventEmitter9 {
|
|
|
26614
27097
|
getP2pSessions() {
|
|
26615
27098
|
return this.p2p.getSessions();
|
|
26616
27099
|
}
|
|
27100
|
+
/**
|
|
27101
|
+
* The people enrolled on a station, read from its own `person_basic_info` table over P2P.
|
|
27102
|
+
*
|
|
27103
|
+
* The cloud roster (`api.getFaces()`) answers empty for an account that keeps its faces on the
|
|
27104
|
+
* station, because they were never uploaded — the station holds them.
|
|
27105
|
+
*
|
|
27106
|
+
* Resolves with the rows the station returned. Rejects on a station that cannot be reached, on a
|
|
27107
|
+
* record stating no `admin_user_id`, and on the reply not arriving whole in time. Assembling that
|
|
27108
|
+
* reply is `P2PSession.readDatabase`'s; this resolves which station and which account id.
|
|
27109
|
+
*
|
|
27110
|
+
* `stranger<n>` names are the station's own placeholders for a face nobody has named, and are
|
|
27111
|
+
* returned as they arrive; a caller that wants only named people filters them.
|
|
27112
|
+
*/
|
|
27113
|
+
async getStationFaces(stationSn, opts = {}) {
|
|
27114
|
+
if (!this.registry.list().length)
|
|
27115
|
+
await this.getDevices();
|
|
27116
|
+
const station = this.p2p.stationKeyOf(stationSn);
|
|
27117
|
+
const accountId = this.adminUserIdOf(station);
|
|
27118
|
+
if (!accountId)
|
|
27119
|
+
throw new Error(`getStationFaces: no admin_user_id on record for ${station}`);
|
|
27120
|
+
await this.p2p.ensureStation(station, opts.signal);
|
|
27121
|
+
const session = this.p2p.getSessions().get(station);
|
|
27122
|
+
if (!session)
|
|
27123
|
+
throw new Error(`getStationFaces: no P2P session for ${station}`);
|
|
27124
|
+
const rows = await session.readDatabase("person_basic_info", { accountId, ...opts });
|
|
27125
|
+
return rows.filter((row) => typeof row === "object" && row !== null);
|
|
27126
|
+
}
|
|
27127
|
+
/** The station's own `admin_user_id`, which its database refuses the query without. */
|
|
27128
|
+
adminUserIdOf(stationSn) {
|
|
27129
|
+
return adminUserIdFrom(this.registry.list().find((d) => d.sn === stationSn)?.raw);
|
|
27130
|
+
}
|
|
26617
27131
|
/**
|
|
26618
27132
|
* The liveness facts for one device — see {@link DeviceState}. Facts, not an `online` verdict: "how
|
|
26619
27133
|
* long is too long" is a threshold that belongs to the caller, and it differs per device class.
|
|
@@ -26851,7 +27365,7 @@ var EufyMega = class extends EventEmitter9 {
|
|
|
26851
27365
|
if (ev.eventName === void 0 && ev.eventType != null)
|
|
26852
27366
|
ev.eventName = detectionName(ev.eventType);
|
|
26853
27367
|
if (ev.thumbnailCandidate)
|
|
26854
|
-
void this.observeStoredImage(ev.thumbnailCandidate);
|
|
27368
|
+
void this.observeStoredImage(ev.thumbnailCandidate, ev.deviceSn);
|
|
26855
27369
|
this.emit("push", ev);
|
|
26856
27370
|
const signal2 = {
|
|
26857
27371
|
source: "push",
|
|
@@ -26862,7 +27376,10 @@ var EufyMega = class extends EventEmitter9 {
|
|
|
26862
27376
|
thumbnailUrl: ev.thumbnailUrl,
|
|
26863
27377
|
payload: ev.payload
|
|
26864
27378
|
};
|
|
26865
|
-
|
|
27379
|
+
const caps = this.capsForEvent(ev.deviceSn);
|
|
27380
|
+
const decoded = decodeEvent(signal2, caps);
|
|
27381
|
+
this.opts.logger?.debug(`[push] in: eventType=${ev.eventType} device=${caps ? "known" : ev.deviceSn ? "unknown" : "none"} events=${decoded.length}`);
|
|
27382
|
+
for (const out of decoded) {
|
|
26866
27383
|
this.emitSemantic(out.event, out.payload, { edge: true, refresh: out.refresh });
|
|
26867
27384
|
const dsn = out.payload.deviceSn ?? ev.deviceSn;
|
|
26868
27385
|
if (dsn)
|
|
@@ -26900,9 +27417,16 @@ var EufyMega = class extends EventEmitter9 {
|
|
|
26900
27417
|
client.on("error", (e) => this.reportError(e));
|
|
26901
27418
|
return client;
|
|
26902
27419
|
}
|
|
26903
|
-
/**
|
|
26904
|
-
|
|
26905
|
-
|
|
27420
|
+
/**
|
|
27421
|
+
* Admit only exact, account-known devices with resolved snapshot evidence into the passive store; a
|
|
27422
|
+
* station-only candidate is admitted under the event's device serial.
|
|
27423
|
+
*/
|
|
27424
|
+
async observeStoredImage(candidate, eventDeviceSn) {
|
|
27425
|
+
if (!this.storedImages)
|
|
27426
|
+
return;
|
|
27427
|
+
const { attribution } = candidate;
|
|
27428
|
+
const deviceSn = attribution.kind === "device" ? attribution.deviceSn : attribution.kind === "station" ? eventDeviceSn : void 0;
|
|
27429
|
+
if (!deviceSn)
|
|
26906
27430
|
return;
|
|
26907
27431
|
const account = this.mega.auth?.userId;
|
|
26908
27432
|
if (!account)
|
|
@@ -26912,9 +27436,9 @@ var EufyMega = class extends EventEmitter9 {
|
|
|
26912
27436
|
await this.registry.getDevices();
|
|
26913
27437
|
if (this.mega.auth?.userId !== account)
|
|
26914
27438
|
return;
|
|
26915
|
-
const caps = this.registry.capabilitiesForDevice(
|
|
27439
|
+
const caps = this.registry.capabilitiesForDevice(deviceSn);
|
|
26916
27440
|
if (caps && hasProvidedAction(caps, "snapshotStored")) {
|
|
26917
|
-
this.storedImages.observe(
|
|
27441
|
+
this.storedImages.observe(deviceSn, candidate.url);
|
|
26918
27442
|
}
|
|
26919
27443
|
} catch (e) {
|
|
26920
27444
|
this.reportError(e);
|
|
@@ -27139,26 +27663,16 @@ var SolixClient = class _SolixClient {
|
|
|
27139
27663
|
}
|
|
27140
27664
|
/** Build the encrypted, signed `/passport/login` request body + headers for the negotiated key. */
|
|
27141
27665
|
async postLogin(kx, verifyCode, limitedToken) {
|
|
27142
|
-
const { clientPublicKeyHex, encryptedPassword } = encryptLoginPassword(this.password);
|
|
27143
27666
|
const bodyObj = {
|
|
27144
|
-
email
|
|
27145
|
-
password: encryptedPassword,
|
|
27146
|
-
ab: this.country,
|
|
27147
|
-
client_secret_info: { public_key: clientPublicKeyHex },
|
|
27667
|
+
...loginCredentials(this.email, this.password, this.country),
|
|
27148
27668
|
answer: "",
|
|
27149
27669
|
captcha_id: "",
|
|
27150
27670
|
verify_code: verifyCode ?? "",
|
|
27151
27671
|
login_id: ""
|
|
27152
27672
|
};
|
|
27153
27673
|
const encBody = encryptBody(JSON.stringify(bodyObj), kx.shareKey);
|
|
27154
|
-
const ts = nowSec();
|
|
27155
|
-
const once = genId();
|
|
27156
27674
|
return this.post(this.apiHost, SOLIX_ENDPOINTS.login, encBody, this.authHeaders({
|
|
27157
|
-
|
|
27158
|
-
"x-key-ident": kx.keyIdent,
|
|
27159
|
-
"x-request-ts": ts,
|
|
27160
|
-
"x-request-once": once,
|
|
27161
|
-
"x-signature": signRequest(kx.shareKey, ts, once, encBody),
|
|
27675
|
+
...signedHeaders(kx, encBody),
|
|
27162
27676
|
...limitedToken ? { "x-auth-token": limitedToken } : {}
|
|
27163
27677
|
}));
|
|
27164
27678
|
}
|
|
@@ -27173,13 +27687,12 @@ var SolixClient = class _SolixClient {
|
|
|
27173
27687
|
* observed refusing the header, which is consistent with the two ids agreeing on the accounts seen.
|
|
27174
27688
|
*/
|
|
27175
27689
|
classifyLogin(data, isVerify) {
|
|
27176
|
-
const
|
|
27177
|
-
|
|
27178
|
-
if (!userId || !authToken)
|
|
27690
|
+
const reply = readLoginReply(data);
|
|
27691
|
+
if (!reply)
|
|
27179
27692
|
throw new Error(`Solix login returned no session: ${JSON.stringify(data).slice(0, 160)}`);
|
|
27180
|
-
const
|
|
27181
|
-
if (!isVerify &&
|
|
27182
|
-
this.pending2fa = { limitedToken: authToken, userId, geoKey:
|
|
27693
|
+
const { userId, authToken } = reply;
|
|
27694
|
+
if (!isVerify && reply.twoFactorPending) {
|
|
27695
|
+
this.pending2fa = { limitedToken: authToken, userId, geoKey: reply.geoKey };
|
|
27183
27696
|
return { status: "2fa", method: "code sent by the passport" };
|
|
27184
27697
|
}
|
|
27185
27698
|
this.pending2fa = void 0;
|
|
@@ -27188,7 +27701,7 @@ var SolixClient = class _SolixClient {
|
|
|
27188
27701
|
userId,
|
|
27189
27702
|
gtoken: gtoken(userId),
|
|
27190
27703
|
apiHost: this.apiHost,
|
|
27191
|
-
tokenExpiresAt:
|
|
27704
|
+
tokenExpiresAt: reply.tokenExpiresAt
|
|
27192
27705
|
};
|
|
27193
27706
|
this.persist();
|
|
27194
27707
|
return { status: "ok", session: this.session_ };
|
|
@@ -27343,14 +27856,8 @@ var SolixClient = class _SolixClient {
|
|
|
27343
27856
|
throw new Error("not authenticated \u2014 call login() first");
|
|
27344
27857
|
const kx = await this.keyExchange();
|
|
27345
27858
|
const encBody = encryptBody(JSON.stringify(payload), kx.shareKey);
|
|
27346
|
-
const ts = nowSec();
|
|
27347
|
-
const once = genId();
|
|
27348
27859
|
const env = await this.post(this.apiHost, path, encBody, this.baseHeaders({
|
|
27349
|
-
|
|
27350
|
-
"x-key-ident": kx.keyIdent,
|
|
27351
|
-
"x-request-ts": ts,
|
|
27352
|
-
"x-request-once": once,
|
|
27353
|
-
"x-signature": signRequest(kx.shareKey, ts, once, encBody),
|
|
27860
|
+
...signedHeaders(kx, encBody),
|
|
27354
27861
|
"x-auth-token": this.session_.authToken,
|
|
27355
27862
|
gtoken: this.session_.gtoken
|
|
27356
27863
|
}));
|
|
@@ -28118,6 +28625,7 @@ export {
|
|
|
28118
28625
|
ROOM_SUCTIONS,
|
|
28119
28626
|
RTSP_MEMBERS,
|
|
28120
28627
|
RawDpWriter,
|
|
28628
|
+
RecordingDownloadError,
|
|
28121
28629
|
RecordingQuality,
|
|
28122
28630
|
RtspRecordingMode,
|
|
28123
28631
|
SCENE_INVALID_REASONS,
|
|
@@ -28161,6 +28669,7 @@ export {
|
|
|
28161
28669
|
VACUUM_DOCK_MEMBERS,
|
|
28162
28670
|
VACUUM_SCHEDULE_ACTIONS,
|
|
28163
28671
|
VACUUM_SCHEDULE_WEEKDAYS,
|
|
28672
|
+
VIDEO_GCM_AAD,
|
|
28164
28673
|
VacuumMapStore,
|
|
28165
28674
|
VideoFrameDecoder,
|
|
28166
28675
|
WORKING_MODE_MAPS,
|
|
@@ -28180,6 +28689,7 @@ export {
|
|
|
28180
28689
|
buildEventIndex,
|
|
28181
28690
|
buildModelIndex,
|
|
28182
28691
|
buildRealtimeInit,
|
|
28692
|
+
cameraPowerTier,
|
|
28183
28693
|
captureSnapshotFromShared,
|
|
28184
28694
|
cellAtPoint,
|
|
28185
28695
|
claimedParams,
|