@mega-yfue/eufy-sdk 0.4.0-beta.9 → 0.4.0
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 +18 -2
- package/dist/client/types.d.ts +25 -0
- package/dist/core/contracts.d.ts +43 -0
- package/dist/index.js +1064 -547
- package/dist/index.js.map +4 -4
- package/dist/model/capabilities/camera.d.ts +14 -9
- 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/members.d.ts +2 -0
- package/dist/model/capabilities/motion.d.ts +3 -4
- package/dist/model/capabilities/siren.d.ts +1 -1
- package/dist/model/capabilities/types.d.ts +2 -0
- 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/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 +51 -14
- package/dist/transport/p2p/recording-download.d.ts +54 -0
- 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
|
|
@@ -1650,6 +2047,15 @@ ${this.auth_?.authToken ?? ""}`;
|
|
|
1650
2047
|
token,
|
|
1651
2048
|
voip_token: ""
|
|
1652
2049
|
});
|
|
2050
|
+
await this.securityAppPost("/v1/apppush/register_push_token", {
|
|
2051
|
+
is_notification_enable: true,
|
|
2052
|
+
token,
|
|
2053
|
+
transaction: String(Date.now())
|
|
2054
|
+
});
|
|
2055
|
+
await this.securityAppPost("/v1/app/review/app_push_check", {
|
|
2056
|
+
app_type: "eufySecurity",
|
|
2057
|
+
transaction: String(Date.now())
|
|
2058
|
+
});
|
|
1653
2059
|
}
|
|
1654
2060
|
/** Download raw bytes from a push-media URL using the active account session. */
|
|
1655
2061
|
async downloadMedia(url) {
|
|
@@ -1746,13 +2152,7 @@ ${this.auth_?.authToken ?? ""}`;
|
|
|
1746
2152
|
}
|
|
1747
2153
|
/** Build the /passport/login body (verify_code empty unless 2FA). */
|
|
1748
2154
|
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
|
-
};
|
|
2155
|
+
const base = loginCredentials(this.cfg.email, this.cfg.password, this.cfg.countryCode);
|
|
1756
2156
|
if (opts.verifyCode)
|
|
1757
2157
|
return { ...base, verify_code: opts.verifyCode, login_id: "" };
|
|
1758
2158
|
if (opts.captchaId)
|
|
@@ -1990,15 +2390,12 @@ ${this.auth_?.authToken ?? ""}`;
|
|
|
1990
2390
|
if (cap.length)
|
|
1991
2391
|
this.logger.debug("[mega] captcha/fa:", JSON.stringify(Object.fromEntries(cap.map((k) => [k, res[k]]))));
|
|
1992
2392
|
}
|
|
1993
|
-
const
|
|
1994
|
-
|
|
1995
|
-
const authToken = res.auth_token ?? res.token;
|
|
1996
|
-
if (!userId || !authToken)
|
|
2393
|
+
const reply = readLoginReply(res);
|
|
2394
|
+
if (!reply)
|
|
1997
2395
|
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 };
|
|
2396
|
+
const { userId, accountUserId, authToken, geoKey } = reply;
|
|
2397
|
+
if (!isVerify && reply.twoFactorPending) {
|
|
2398
|
+
this.auth_ = { userId, accountUserId, authToken, geoKey };
|
|
2002
2399
|
this.pendingCaptchaId = void 0;
|
|
2003
2400
|
this.pending2fa = true;
|
|
2004
2401
|
await this.sendVerifyCode(messageType);
|
|
@@ -2006,8 +2403,8 @@ ${this.auth_?.authToken ?? ""}`;
|
|
|
2006
2403
|
}
|
|
2007
2404
|
this.pendingCaptchaId = void 0;
|
|
2008
2405
|
this.pending2fa = false;
|
|
2009
|
-
this.auth_ = { userId, accountUserId, authToken, geoKey
|
|
2010
|
-
this.tokenExpiresAt =
|
|
2406
|
+
this.auth_ = { userId, accountUserId, authToken, geoKey };
|
|
2407
|
+
this.tokenExpiresAt = reply.tokenExpiresAt;
|
|
2011
2408
|
this.sessionKey = void 0;
|
|
2012
2409
|
await this.ensureSessionKey();
|
|
2013
2410
|
this.persist();
|
|
@@ -2317,119 +2714,10 @@ var SecureMqtt = class extends EventEmitter {
|
|
|
2317
2714
|
import { createHash as createHash4 } from "node:crypto";
|
|
2318
2715
|
function buildAppShapedClientId(input) {
|
|
2319
2716
|
const ts = input.timestamp ?? Math.floor(Date.now() / 1e3);
|
|
2320
|
-
return `android-${input.appName}-${input.uid}-${input.mqttUuid}-${ts}`;
|
|
2321
|
-
}
|
|
2322
|
-
function mqttUuidFrom(installId) {
|
|
2323
|
-
return createHash4("sha256").update(installId).digest("hex").slice(0, 16);
|
|
2324
|
-
}
|
|
2325
|
-
|
|
2326
|
-
// dist/core/util.js
|
|
2327
|
-
var Timer = class {
|
|
2328
|
-
handle;
|
|
2329
|
-
/** (Re)arm to run `fn` after `ms`, cancelling any already-pending fire. */
|
|
2330
|
-
arm(ms, fn) {
|
|
2331
|
-
this.cancel();
|
|
2332
|
-
this.handle = setTimeout(() => {
|
|
2333
|
-
this.handle = void 0;
|
|
2334
|
-
fn();
|
|
2335
|
-
}, ms);
|
|
2336
|
-
this.handle.unref?.();
|
|
2337
|
-
}
|
|
2338
|
-
/** Cancel a pending fire. No-op if not armed. */
|
|
2339
|
-
cancel() {
|
|
2340
|
-
if (this.handle) {
|
|
2341
|
-
clearTimeout(this.handle);
|
|
2342
|
-
this.handle = void 0;
|
|
2343
|
-
}
|
|
2344
|
-
}
|
|
2345
|
-
/** Whether a fire is currently scheduled. */
|
|
2346
|
-
get pending() {
|
|
2347
|
-
return this.handle !== void 0;
|
|
2348
|
-
}
|
|
2349
|
-
};
|
|
2350
|
-
function assertNever(value) {
|
|
2351
|
-
throw new Error(`unhandled variant: ${JSON.stringify(value)}`);
|
|
2352
|
-
}
|
|
2353
|
-
function asBool(value) {
|
|
2354
|
-
if (typeof value === "string")
|
|
2355
|
-
return value === "1" || value.toLowerCase() === "true";
|
|
2356
|
-
return value === true || value === 1;
|
|
2357
|
-
}
|
|
2358
|
-
function clamp(value, min, max) {
|
|
2359
|
-
if (Number.isNaN(value))
|
|
2360
|
-
return min;
|
|
2361
|
-
return Math.max(min, Math.min(max, Math.round(value)));
|
|
2362
|
-
}
|
|
2363
|
-
function structuralEqual(a, b) {
|
|
2364
|
-
if (Object.is(a, b))
|
|
2365
|
-
return true;
|
|
2366
|
-
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null)
|
|
2367
|
-
return false;
|
|
2368
|
-
const aArr = Array.isArray(a);
|
|
2369
|
-
const bArr = Array.isArray(b);
|
|
2370
|
-
if (aArr !== bArr)
|
|
2371
|
-
return false;
|
|
2372
|
-
if (aArr && bArr) {
|
|
2373
|
-
if (a.length !== b.length)
|
|
2374
|
-
return false;
|
|
2375
|
-
for (let i = 0; i < a.length; i++)
|
|
2376
|
-
if (!structuralEqual(a[i], b[i]))
|
|
2377
|
-
return false;
|
|
2378
|
-
return true;
|
|
2379
|
-
}
|
|
2380
|
-
const aObj = a;
|
|
2381
|
-
const bObj = b;
|
|
2382
|
-
const aKeys = Object.keys(aObj);
|
|
2383
|
-
const bKeys = Object.keys(bObj);
|
|
2384
|
-
if (aKeys.length !== bKeys.length)
|
|
2385
|
-
return false;
|
|
2386
|
-
for (const k of aKeys) {
|
|
2387
|
-
if (!Object.prototype.hasOwnProperty.call(bObj, k))
|
|
2388
|
-
return false;
|
|
2389
|
-
if (!structuralEqual(aObj[k], bObj[k]))
|
|
2390
|
-
return false;
|
|
2391
|
-
}
|
|
2392
|
-
return true;
|
|
2393
|
-
}
|
|
2394
|
-
function coerceEnumValue(enumObj, value) {
|
|
2395
|
-
const n = Number(value);
|
|
2396
|
-
return Number.isInteger(n) && Object.values(enumObj).includes(n) ? n : void 0;
|
|
2397
|
-
}
|
|
2398
|
-
function enumValues(enumObj) {
|
|
2399
|
-
return Object.values(enumObj).join("/");
|
|
2400
|
-
}
|
|
2401
|
-
function enumLabels(enumObj) {
|
|
2402
|
-
return Object.fromEntries(Object.entries(enumObj).map(([name, value]) => [value, name]));
|
|
2403
|
-
}
|
|
2404
|
-
function jsonObject(text2) {
|
|
2405
|
-
if (typeof text2 !== "string")
|
|
2406
|
-
return void 0;
|
|
2407
|
-
try {
|
|
2408
|
-
const v = JSON.parse(text2);
|
|
2409
|
-
return v && typeof v === "object" && !Array.isArray(v) ? v : void 0;
|
|
2410
|
-
} catch {
|
|
2411
|
-
return void 0;
|
|
2412
|
-
}
|
|
2413
|
-
}
|
|
2414
|
-
function u16le(n) {
|
|
2415
|
-
const b = Buffer.alloc(2);
|
|
2416
|
-
b.writeUInt16LE(n & 65535);
|
|
2417
|
-
return b;
|
|
2418
|
-
}
|
|
2419
|
-
function u16be(n) {
|
|
2420
|
-
const b = Buffer.alloc(2);
|
|
2421
|
-
b.writeUInt16BE(n & 65535);
|
|
2422
|
-
return b;
|
|
2423
|
-
}
|
|
2424
|
-
function u32le(n) {
|
|
2425
|
-
const b = Buffer.alloc(4);
|
|
2426
|
-
b.writeUInt32LE(n >>> 0);
|
|
2427
|
-
return b;
|
|
2428
|
-
}
|
|
2429
|
-
function u32be(n) {
|
|
2430
|
-
const b = Buffer.alloc(4);
|
|
2431
|
-
b.writeUInt32BE(n >>> 0);
|
|
2432
|
-
return b;
|
|
2717
|
+
return `android-${input.appName}-${input.uid}-${input.mqttUuid}-${ts}`;
|
|
2718
|
+
}
|
|
2719
|
+
function mqttUuidFrom(installId) {
|
|
2720
|
+
return createHash4("sha256").update(installId).digest("hex").slice(0, 16);
|
|
2433
2721
|
}
|
|
2434
2722
|
|
|
2435
2723
|
// dist/transport/mqtt/availability.js
|
|
@@ -2556,13 +2844,6 @@ function parseAiotDpReport(raw) {
|
|
|
2556
2844
|
return Object.keys(out).length ? out : void 0;
|
|
2557
2845
|
}
|
|
2558
2846
|
|
|
2559
|
-
// dist/core/raw-dp-hex.js
|
|
2560
|
-
function hexToRawDp(hex) {
|
|
2561
|
-
if (hex.length === 0 || hex.length % 2 !== 0 || !/^[0-9a-fA-F]+$/.test(hex))
|
|
2562
|
-
return void 0;
|
|
2563
|
-
return Buffer.from(hex, "hex").toString("base64");
|
|
2564
|
-
}
|
|
2565
|
-
|
|
2566
2847
|
// dist/transport/mqtt/biz-stream.js
|
|
2567
2848
|
var BIZ_PROTOCOL = {
|
|
2568
2849
|
/** Map-stream data: the frames this module parses. */
|
|
@@ -2746,18 +3027,6 @@ var DeviceType = {
|
|
|
2746
3027
|
CAMERA_C35: 10035
|
|
2747
3028
|
};
|
|
2748
3029
|
|
|
2749
|
-
// dist/core/types.js
|
|
2750
|
-
var PRINTER_CATEGORY_RE = /ankermake|eufymake|fdm|3d_?print/i;
|
|
2751
|
-
function classifyDevice(raw) {
|
|
2752
|
-
const category = raw.category ?? "";
|
|
2753
|
-
const hasP2p = typeof raw.p2p_did === "string" && raw.p2p_did.length > 0;
|
|
2754
|
-
if (category === "eufy_security" || hasP2p)
|
|
2755
|
-
return { category, api: "mega", realtime: "p2p" };
|
|
2756
|
-
if (PRINTER_CATEGORY_RE.test(category))
|
|
2757
|
-
return { category, api: "ankermake", realtime: "ankermake-mqtt" };
|
|
2758
|
-
return { category, api: "mega", realtime: "smqtt" };
|
|
2759
|
-
}
|
|
2760
|
-
|
|
2761
3030
|
// dist/model/classify.js
|
|
2762
3031
|
var STATION_TYPES = /* @__PURE__ */ new Set([
|
|
2763
3032
|
DeviceType.STATION,
|
|
@@ -2896,111 +3165,6 @@ function inferName(rec) {
|
|
|
2896
3165
|
return void 0;
|
|
2897
3166
|
}
|
|
2898
3167
|
|
|
2899
|
-
// dist/core/contracts.js
|
|
2900
|
-
var StoredSnapshotUnavailableError = class extends Error {
|
|
2901
|
-
reason;
|
|
2902
|
-
constructor(reason, message, options) {
|
|
2903
|
-
super(message, options);
|
|
2904
|
-
this.reason = reason;
|
|
2905
|
-
this.name = "StoredSnapshotUnavailableError";
|
|
2906
|
-
}
|
|
2907
|
-
};
|
|
2908
|
-
var RETRYABLE_LIVE_SNAPSHOT_REASONS = [
|
|
2909
|
-
"no-keyframe",
|
|
2910
|
-
"source-failed",
|
|
2911
|
-
"undecodable-burst"
|
|
2912
|
-
];
|
|
2913
|
-
var LiveSnapshotUnavailableError = class extends Error {
|
|
2914
|
-
reason;
|
|
2915
|
-
/** Whether another attempt could plausibly succeed without the host changing anything. */
|
|
2916
|
-
retryable;
|
|
2917
|
-
constructor(reason, message, options) {
|
|
2918
|
-
super(message, options);
|
|
2919
|
-
this.reason = reason;
|
|
2920
|
-
this.name = "LiveSnapshotUnavailableError";
|
|
2921
|
-
this.retryable = RETRYABLE_LIVE_SNAPSHOT_REASONS.includes(reason);
|
|
2922
|
-
}
|
|
2923
|
-
};
|
|
2924
|
-
var CameraDisabledError = class extends Error {
|
|
2925
|
-
camera;
|
|
2926
|
-
constructor(camera, options) {
|
|
2927
|
-
super(`camera${camera ? ` ${camera}` : ""} is disabled \u2014 no live stream possible`, options);
|
|
2928
|
-
this.camera = camera;
|
|
2929
|
-
this.name = "CameraDisabledError";
|
|
2930
|
-
}
|
|
2931
|
-
};
|
|
2932
|
-
var StationKeyUnavailableError = class extends Error {
|
|
2933
|
-
stationSn;
|
|
2934
|
-
/** Always true: the negotiation is per connection, so a later one may still produce a key. */
|
|
2935
|
-
retryable = true;
|
|
2936
|
-
constructor(stationSn, options) {
|
|
2937
|
-
super(`station ${stationSn} did not provide its session key, so nothing that requires one could be sent`, options);
|
|
2938
|
-
this.stationSn = stationSn;
|
|
2939
|
-
this.name = "StationKeyUnavailableError";
|
|
2940
|
-
}
|
|
2941
|
-
};
|
|
2942
|
-
var StationUnreachableError = class extends Error {
|
|
2943
|
-
stationSn;
|
|
2944
|
-
waitedMs;
|
|
2945
|
-
/** Always true: a station unreachable now may answer on a later attempt. */
|
|
2946
|
-
retryable = true;
|
|
2947
|
-
constructor(stationSn, waitedMs, options) {
|
|
2948
|
-
super(`station ${stationSn}'s P2P session did not connect within ${waitedMs}ms, so nothing could be sent to it`, options);
|
|
2949
|
-
this.stationSn = stationSn;
|
|
2950
|
-
this.waitedMs = waitedMs;
|
|
2951
|
-
this.name = "StationUnreachableError";
|
|
2952
|
-
}
|
|
2953
|
-
};
|
|
2954
|
-
var DeviceChannelUnresolvedError = class extends Error {
|
|
2955
|
-
sn;
|
|
2956
|
-
stationSn;
|
|
2957
|
-
constructor(sn, stationSn, options) {
|
|
2958
|
-
super(`${sn} has no usable channel on station ${stationSn}, so nothing was sent to it`, options);
|
|
2959
|
-
this.sn = sn;
|
|
2960
|
-
this.stationSn = stationSn;
|
|
2961
|
-
this.name = "DeviceChannelUnresolvedError";
|
|
2962
|
-
}
|
|
2963
|
-
};
|
|
2964
|
-
var LiveStreamStartError = class extends Error {
|
|
2965
|
-
reason;
|
|
2966
|
-
stage;
|
|
2967
|
-
timeoutMs;
|
|
2968
|
-
attempts;
|
|
2969
|
-
constructor(failure) {
|
|
2970
|
-
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 });
|
|
2971
|
-
this.name = "LiveStreamStartError";
|
|
2972
|
-
this.reason = failure.reason;
|
|
2973
|
-
this.stage = failure.stage;
|
|
2974
|
-
this.timeoutMs = failure.timeoutMs;
|
|
2975
|
-
this.attempts = failure.attempts;
|
|
2976
|
-
}
|
|
2977
|
-
};
|
|
2978
|
-
var StateConvergenceError = class extends Error {
|
|
2979
|
-
sn;
|
|
2980
|
-
property;
|
|
2981
|
-
param;
|
|
2982
|
-
expected;
|
|
2983
|
-
observed;
|
|
2984
|
-
timeoutMs;
|
|
2985
|
-
constructor(failure) {
|
|
2986
|
-
super(`device ${failure.sn} did not report ${failure.property} as ${String(failure.expected)} within ${failure.timeoutMs}ms (param ${failure.param} read ${String(failure.observed)})`);
|
|
2987
|
-
this.name = "StateConvergenceError";
|
|
2988
|
-
this.sn = failure.sn;
|
|
2989
|
-
this.property = failure.property;
|
|
2990
|
-
this.param = failure.param;
|
|
2991
|
-
this.expected = failure.expected;
|
|
2992
|
-
this.observed = failure.observed;
|
|
2993
|
-
this.timeoutMs = failure.timeoutMs;
|
|
2994
|
-
}
|
|
2995
|
-
};
|
|
2996
|
-
var COMMAND_OBSERVATION = /* @__PURE__ */ Symbol("command-observation");
|
|
2997
|
-
function observeCommand(command, observation) {
|
|
2998
|
-
return Object.defineProperty(command, COMMAND_OBSERVATION, { configurable: true, value: observation });
|
|
2999
|
-
}
|
|
3000
|
-
function commandObservation(command) {
|
|
3001
|
-
return command[COMMAND_OBSERVATION];
|
|
3002
|
-
}
|
|
3003
|
-
|
|
3004
3168
|
// dist/model/capabilities/access.js
|
|
3005
3169
|
function setScalar(param, value, ctx, form = "auto") {
|
|
3006
3170
|
return { kind: "set-param", param, value, form, channel: ctx.channel };
|
|
@@ -3109,7 +3273,7 @@ function propertiesOf(members, ctx) {
|
|
|
3109
3273
|
unit: m.unit,
|
|
3110
3274
|
enumValues: enumValues2,
|
|
3111
3275
|
provenance: m.provenance,
|
|
3112
|
-
invert: promoted?.invert ?? m.invert,
|
|
3276
|
+
invert: promoted?.invert ?? (ctx ? m.invertFor?.(ctx) : void 0) ?? m.invert,
|
|
3113
3277
|
decode: m.coerce,
|
|
3114
3278
|
// A member with a getter-side `decode` reads a field out of a structured payload, so the
|
|
3115
3279
|
// stored value is that payload — see `PropertySpec.raw`.
|
|
@@ -3273,11 +3437,17 @@ var unreflectedMembers = (surface) => statement(surface, UNREFLECTED);
|
|
|
3273
3437
|
// dist/model/capabilities/manifest.js
|
|
3274
3438
|
function describeBound(modules, bound, ctx) {
|
|
3275
3439
|
const out = [];
|
|
3440
|
+
const anyBound = Object.values(bound).some((obj) => obj && typeof obj === "object");
|
|
3276
3441
|
for (const m of modules) {
|
|
3277
3442
|
const accessor = camelCase(m.capability);
|
|
3278
3443
|
const obj = bound[accessor];
|
|
3279
|
-
if (!obj || typeof obj !== "object")
|
|
3444
|
+
if (!obj || typeof obj !== "object") {
|
|
3445
|
+
const events = anyBound ? emitsOf(m, [], ctx) : [];
|
|
3446
|
+
if (events.length && ctx?.capabilities?.has(m.capability)) {
|
|
3447
|
+
out.push({ capability: m.capability, reads: [], actions: [], undescribedActions: [], events });
|
|
3448
|
+
}
|
|
3280
3449
|
continue;
|
|
3450
|
+
}
|
|
3281
3451
|
const descriptors = Object.getOwnPropertyDescriptors(obj);
|
|
3282
3452
|
const reads2 = [];
|
|
3283
3453
|
const actions = [];
|
|
@@ -3800,7 +3970,6 @@ var MOTION_CMD = {
|
|
|
3800
3970
|
* payload key AND pass mChannel 0 explicitly; the two are separate choices, not linked.)
|
|
3801
3971
|
*
|
|
3802
3972
|
* ⚠️ Replay + readback confirmed on a HomeBase-attached T8425 (1719 `0`→`1`→`0`), NOT byte-captured.
|
|
3803
|
-
* Provenance is `apk`, not `verified`: a divergent-but-also-accepted frame can't be ruled out.
|
|
3804
3973
|
*/
|
|
3805
3974
|
HUMAN_ONLY_AT_NIGHT: 1719,
|
|
3806
3975
|
/**
|
|
@@ -3814,7 +3983,7 @@ var MOTION_CMD = {
|
|
|
3814
3983
|
* form, so a feature that is on reads as off.
|
|
3815
3984
|
* Decoded by {@link decodeRadarWdSwitch} to match the app.
|
|
3816
3985
|
*
|
|
3817
|
-
* ⚠️ Replay + readback confirmed on a T8214 (2706 `0`→`1`→`0`), NOT byte-captured.
|
|
3986
|
+
* ⚠️ Replay + readback confirmed on a T8214 (2706 `0`→`1`→`0`), NOT byte-captured.
|
|
3818
3987
|
*/
|
|
3819
3988
|
LOITERING_DETECTION: 2706,
|
|
3820
3989
|
/**
|
|
@@ -4138,7 +4307,7 @@ var MOTION_MEMBERS = {
|
|
|
4138
4307
|
param: MOTION_CMD.HUMAN_ONLY_AT_NIGHT,
|
|
4139
4308
|
type: "bool",
|
|
4140
4309
|
kind: "boolean",
|
|
4141
|
-
provenance: "
|
|
4310
|
+
provenance: "verified",
|
|
4142
4311
|
description: "Restrict AI classification to night-time only (1719). \u26A0\uFE0F Replay + readback confirmed on a HomeBase-attached T8425, not byte-captured.",
|
|
4143
4312
|
requires: [MOTION_CMD.HUMAN_ONLY_AT_NIGHT],
|
|
4144
4313
|
write: (v, ctx) => {
|
|
@@ -4154,7 +4323,7 @@ var MOTION_MEMBERS = {
|
|
|
4154
4323
|
param: MOTION_CMD.LOITERING_DETECTION,
|
|
4155
4324
|
type: "bool",
|
|
4156
4325
|
kind: "boolean",
|
|
4157
|
-
provenance: "
|
|
4326
|
+
provenance: "verified",
|
|
4158
4327
|
coerce: (raw) => decodeRadarWdSwitch(raw) ?? false,
|
|
4159
4328
|
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.",
|
|
4160
4329
|
requires: [MOTION_CMD.LOITERING_DETECTION],
|
|
@@ -4260,6 +4429,7 @@ var MOTION = {
|
|
|
4260
4429
|
};
|
|
4261
4430
|
|
|
4262
4431
|
// dist/model/capabilities/person-detection.js
|
|
4432
|
+
var IDENTIFIED_PERSON_CLAIM = { homeBaseAttached: true };
|
|
4263
4433
|
var PERSON_DETECTION = {
|
|
4264
4434
|
capability: "person_detection",
|
|
4265
4435
|
description: "AI human detection enable switch and live detected state.",
|
|
@@ -4276,8 +4446,18 @@ var PERSON_DETECTION = {
|
|
|
4276
4446
|
*/
|
|
4277
4447
|
events: [
|
|
4278
4448
|
{ source: "push", match: DoorbellPushEvent.FACE_DETECTION, emit: "personDetected" },
|
|
4279
|
-
{
|
|
4280
|
-
|
|
4449
|
+
{
|
|
4450
|
+
source: "push",
|
|
4451
|
+
match: HB3PairedDevicePushEvent.IDENTITY_PERSON_DETECTION,
|
|
4452
|
+
emit: "personDetected",
|
|
4453
|
+
claim: IDENTIFIED_PERSON_CLAIM
|
|
4454
|
+
},
|
|
4455
|
+
{
|
|
4456
|
+
source: "push",
|
|
4457
|
+
match: HB3PairedDevicePushEvent.STRANGER_PERSON_DETECTION,
|
|
4458
|
+
emit: "strangerDetected",
|
|
4459
|
+
claim: IDENTIFIED_PERSON_CLAIM
|
|
4460
|
+
}
|
|
4281
4461
|
]
|
|
4282
4462
|
};
|
|
4283
4463
|
|
|
@@ -5025,8 +5205,10 @@ var AUDIO = {
|
|
|
5025
5205
|
|
|
5026
5206
|
// dist/model/capabilities/camera.js
|
|
5027
5207
|
var CAMERA_CMD = {
|
|
5028
|
-
/** Camera on/off.
|
|
5208
|
+
/** Camera on/off. Read and write polarity depend on the family and topology. */
|
|
5029
5209
|
CAMERA_ENABLE: 1035,
|
|
5210
|
+
/** Wrapped T8410 power switch on HomeBase 3. */
|
|
5211
|
+
PRIVACY_MODE: 6250,
|
|
5030
5212
|
/**
|
|
5031
5213
|
* Camera status LED on/off — the "power/recording" indicator. The
|
|
5032
5214
|
* app JS has a sibling param `1056` (`APP_CMD_LIVEVIEW_LED_SWITCH`) for the SAME UI "Status Light"
|
|
@@ -5235,6 +5417,8 @@ function isEnableBitPolarity(ctx) {
|
|
|
5235
5417
|
const t = ctx.deviceType;
|
|
5236
5418
|
if (t === void 0)
|
|
5237
5419
|
return false;
|
|
5420
|
+
if (t === DeviceType.INDOOR_CAMERA && ctx.paramIds.has(CAMERA_CMD.CAMERA_ENABLE))
|
|
5421
|
+
return false;
|
|
5238
5422
|
if (isIndoorCamera(ctx) && !isIndoorCamMini(ctx) && !isIndoorPanTiltS350(ctx))
|
|
5239
5423
|
return true;
|
|
5240
5424
|
return ENABLE_BIT_FLOODLIGHT_TYPES.has(t);
|
|
@@ -5246,7 +5430,27 @@ var ENABLE_BIT_FLOODLIGHT_TYPES = /* @__PURE__ */ new Set([
|
|
|
5246
5430
|
function powerValue(on, ctx) {
|
|
5247
5431
|
return isEnableBitPolarity(ctx) ? on ? 1 : 0 : on ? 0 : 1;
|
|
5248
5432
|
}
|
|
5433
|
+
function isT8410HomeBase3(ctx) {
|
|
5434
|
+
return ctx.model === "T8410" && ctx.stationSerial?.startsWith("T8030") === true;
|
|
5435
|
+
}
|
|
5436
|
+
function usesHomeBasePowerPayload(ctx) {
|
|
5437
|
+
if (!isT8410HomeBase3(ctx) || !ctx.firmwareVersion || !/^\d+(?:\.\d+){2,3}$/.test(ctx.firmwareVersion))
|
|
5438
|
+
return false;
|
|
5439
|
+
const version = ctx.firmwareVersion.split(".").map(Number);
|
|
5440
|
+
const minimum = [2, 3, 1, 0];
|
|
5441
|
+
for (let i = 0; i < minimum.length; i++) {
|
|
5442
|
+
const difference = (version[i] ?? 0) - minimum[i];
|
|
5443
|
+
if (difference !== 0)
|
|
5444
|
+
return difference > 0;
|
|
5445
|
+
}
|
|
5446
|
+
return true;
|
|
5447
|
+
}
|
|
5448
|
+
function hasUnreflectedHomeBasePower(ctx) {
|
|
5449
|
+
return usesHomeBasePowerPayload(ctx) && !ctx.paramIds.has(2001);
|
|
5450
|
+
}
|
|
5249
5451
|
function powerCommand(on, ctx) {
|
|
5452
|
+
if (usesHomeBasePowerPayload(ctx))
|
|
5453
|
+
return setPayload(CAMERA_CMD.PRIVACY_MODE, { switch: on ? 0 : 1 }, ctx, 0);
|
|
5250
5454
|
return setScalar(CAMERA_CMD.CAMERA_ENABLE, powerValue(on, ctx), ctx, "auto");
|
|
5251
5455
|
}
|
|
5252
5456
|
function privacyCommand(enabled, channel) {
|
|
@@ -5264,28 +5468,34 @@ function enablementReflection(on, ctx) {
|
|
|
5264
5468
|
const alias = CAMERA_MEMBERS.enabled.readAliases[0].paramType;
|
|
5265
5469
|
if (ctx.paramIds.has(alias))
|
|
5266
5470
|
return { param: alias, expected: on, observed: on };
|
|
5471
|
+
if (hasUnreflectedHomeBasePower(ctx))
|
|
5472
|
+
return void 0;
|
|
5267
5473
|
if (ctx.paramIds.has(CAMERA_CMD.CAMERA_ENABLE)) {
|
|
5268
|
-
return {
|
|
5474
|
+
return {
|
|
5475
|
+
param: CAMERA_CMD.CAMERA_ENABLE,
|
|
5476
|
+
expected: powerValue(on, ctx),
|
|
5477
|
+
observed: on
|
|
5478
|
+
};
|
|
5269
5479
|
}
|
|
5270
5480
|
return void 0;
|
|
5271
5481
|
}
|
|
5272
5482
|
function refuseWhenDisabled(ctx, read) {
|
|
5273
|
-
if (read("enabled")?.value === false)
|
|
5483
|
+
if (!hasUnreflectedHomeBasePower(ctx) && read("enabled")?.value === false)
|
|
5274
5484
|
throw new CameraDisabledError(ctx.name ?? ctx.serial);
|
|
5275
5485
|
}
|
|
5276
5486
|
var CAMERA_MEMBERS = {
|
|
5277
5487
|
/**
|
|
5278
|
-
* The READ is the *disable*-bit convention (1035 "0" ⇒ ON, 2001 direct); the WRITE polarity is
|
|
5488
|
+
* The default READ is the *disable*-bit convention (1035 "0" ⇒ ON, 2001 direct); the WRITE polarity is
|
|
5279
5489
|
* family-dependent — see `powerValue` / `isEnableBitPolarity`. Battery/solo cams report the state under
|
|
5280
5490
|
* 1035, standalone indoor/outdoor cams under 2001 OPEN_DEVICE with direct polarity, so 2001 is a
|
|
5281
5491
|
* read-alias. Both verified live, and the write polarity is confirmed against the app's own frames.
|
|
5282
5492
|
*
|
|
5283
|
-
*
|
|
5284
|
-
*
|
|
5493
|
+
* T8410 on HomeBase 3 reports 1035 as an enable bit ("1" ⇒ ON). That reported state does not reliably
|
|
5494
|
+
* reflect the privacy command. `readReflectsWrite` states this distinction.
|
|
5285
5495
|
*
|
|
5286
5496
|
* The privacy param (6250) is reported by the outdoor-PT family and by no other camera measured, and both
|
|
5287
5497
|
* of its polarities are observed. It is deliberately NOT aliased here: it moved in the same step as 1035, so
|
|
5288
|
-
* the reading cannot say whether power and privacy are one state or two
|
|
5498
|
+
* the reading cannot say whether power and privacy are one state or two — so
|
|
5289
5499
|
* aliasing a second param could only fold two possible states into one getter for no gain.
|
|
5290
5500
|
*/
|
|
5291
5501
|
enabled: {
|
|
@@ -5294,8 +5504,10 @@ var CAMERA_MEMBERS = {
|
|
|
5294
5504
|
kind: "boolean",
|
|
5295
5505
|
provenance: "verified",
|
|
5296
5506
|
invert: true,
|
|
5507
|
+
invertFor: (ctx) => isT8410HomeBase3(ctx) ? false : void 0,
|
|
5297
5508
|
readAliases: [{ paramType: 2001, invert: false }],
|
|
5298
|
-
|
|
5509
|
+
readReflectsWrite: (ctx) => !hasUnreflectedHomeBasePower(ctx),
|
|
5510
|
+
description: "Camera enabled. Family-dependent wire param: 1035 CMD_DEVS_SWITCH (enable bit on T8410/HomeBase 3, disable bit on battery/solo cams) or 2001 OPEN_DEVICE (standalone indoor/outdoor). Reported on/off state; unreflectedMembers identifies commands whose effect this report does not reliably reflect.",
|
|
5299
5511
|
observation: {
|
|
5300
5512
|
event: "cameraEnabledChanged",
|
|
5301
5513
|
reflects: (value, ctx) => enablementReflection(asBool(value), ctx),
|
|
@@ -5522,7 +5734,7 @@ var CAMERA_MEMBERS = {
|
|
|
5522
5734
|
* value, and it exists only on a device bound to a provider. Each is declared once, with its
|
|
5523
5735
|
* signature taken FROM {@link MediaProvider} — so a change there is a compile error here, not a drift.
|
|
5524
5736
|
*
|
|
5525
|
-
* Every pull is refused where {@link CAMERA_MEMBERS.enabled} reads false, with {@link CameraDisabledError} —
|
|
5737
|
+
* Every pull is refused where a reliable {@link CAMERA_MEMBERS.enabled} reads false, with {@link CameraDisabledError} —
|
|
5526
5738
|
* `live`, `snapshotLive`, `record`, `openReadable` and `recordFragments` alike, since each opens media on a
|
|
5527
5739
|
* camera that serves none. That reading is the on/off source; a live probe is not one, since a disabled
|
|
5528
5740
|
* camera answers a start with audio and never a video frame.
|
|
@@ -5555,6 +5767,7 @@ var CAMERA_MEMBERS = {
|
|
|
5555
5767
|
refuseWhenDisabled(ctx, read);
|
|
5556
5768
|
return m.recordFragments({ powered: poweredOf(ctx), ...opts });
|
|
5557
5769
|
}), "Continuous fragmented-MP4 (CMAF) recording."),
|
|
5770
|
+
downloadRecording: provided("media", (m) => m.downloadRecording && ((opts) => m.downloadRecording(opts)), "Download a recording the station holds \u2192 H.264 and AAC elementary streams."),
|
|
5558
5771
|
/**
|
|
5559
5772
|
* Push audio from the host to this camera's speaker. Gated on the **speaker** param specifically —
|
|
5560
5773
|
* not the `audio` capability, which resolves on a microphone alone and would advertise a speaker the
|
|
@@ -5596,7 +5809,7 @@ var SIREN_CMD = {
|
|
|
5596
5809
|
ALARM_TEST: 1826,
|
|
5597
5810
|
/** Manually stop a sounding alarm (app `APP_CMD_SIREN_SENSOR_MANUAL_STOP_ALARM`). */
|
|
5598
5811
|
MANUAL_STOP: 1871,
|
|
5599
|
-
/** Station-family HomeBase duration alarm, verified live on T8010 (app `SET_TONE_FILE`). */
|
|
5812
|
+
/** Station-family HomeBase duration alarm, verified live on T8010 and T8030 (app `SET_TONE_FILE`). */
|
|
5600
5813
|
HOMEBASE_TONE: 1201,
|
|
5601
5814
|
/** HomeBase alarm volume percentage (app `CMD_SET_HUB_SPK_VOLUME`). */
|
|
5602
5815
|
HUB_SPK_VOLUME: 1235,
|
|
@@ -5611,7 +5824,7 @@ var SirenAlarmDuration = { Min1: 60, Min5: 300, Min10: 600, Min15: 900 };
|
|
|
5611
5824
|
var HubAlarmTone = { Tone1: 1, Tone2: 2 };
|
|
5612
5825
|
var HUB_ALARM_EVIDENCE = [SIREN_CMD.HUB_ALARM_TONE, 1282];
|
|
5613
5826
|
function isStationAlarmOutput(ctx) {
|
|
5614
|
-
return ctx.deviceType === DeviceType.STATION;
|
|
5827
|
+
return ctx.deviceType === DeviceType.STATION || ctx.deviceType === DeviceType.HB3;
|
|
5615
5828
|
}
|
|
5616
5829
|
function isEvidencedStationAlarm(ctx) {
|
|
5617
5830
|
return isStationAlarmOutput(ctx) && HUB_ALARM_EVIDENCE.some((param) => ctx.paramIds.has(param));
|
|
@@ -6733,9 +6946,9 @@ function parseQuickResponses(voiceList) {
|
|
|
6733
6946
|
var DOORBELL_MEMBERS = {
|
|
6734
6947
|
/**
|
|
6735
6948
|
* The HOMEBASE as the doorbell's chime — the hub plays the ring, not the wired chime box
|
|
6736
|
-
* `mechanicalChimeSwitch` drives. `provenance` is "verified" on our own decrypt of the app's frame
|
|
6737
|
-
*
|
|
6738
|
-
*
|
|
6949
|
+
* `mechanicalChimeSwitch` drives. `provenance` is "verified" on our own decrypt of the app's frame:
|
|
6950
|
+
* direct-binary `[ch][value][acct]`, 1=on/0=off, the same shape as its 1703 sibling — captured, not
|
|
6951
|
+
* inferred from the shared param range.
|
|
6739
6952
|
*/
|
|
6740
6953
|
chimeSwitch: {
|
|
6741
6954
|
param: DOORBELL_CMD.CHIME_SWITCH,
|
|
@@ -6746,9 +6959,8 @@ var DOORBELL_MEMBERS = {
|
|
|
6746
6959
|
write: (v, ctx) => setScalar(DOORBELL_CMD.CHIME_SWITCH, asBool(v) ? 1 : 0, ctx, "direct-binary")
|
|
6747
6960
|
},
|
|
6748
6961
|
/**
|
|
6749
|
-
* `provenance` is "verified"
|
|
6750
|
-
*
|
|
6751
|
-
* `[ch][value][acct]`, 1=on/0=off — verified live on a T8214 (ON then OFF).
|
|
6962
|
+
* `provenance` is "verified" on our own P2P decrypt of the write wire: direct-binary
|
|
6963
|
+
* `[ch][value][acct]`, 1=on/0=off, verified live on a T8214 (ON then OFF).
|
|
6752
6964
|
*/
|
|
6753
6965
|
mechanicalChimeSwitch: {
|
|
6754
6966
|
param: DOORBELL_CMD.MECHANICAL_CHIME_SWITCH,
|
|
@@ -6779,7 +6991,7 @@ var DOORBELL_MEMBERS = {
|
|
|
6779
6991
|
type: "number",
|
|
6780
6992
|
unit: "%",
|
|
6781
6993
|
kind: "percent",
|
|
6782
|
-
provenance: "
|
|
6994
|
+
provenance: "verified",
|
|
6783
6995
|
writtenElsewhere: true,
|
|
6784
6996
|
description: "Ringtone volume (1708; confirmed on T8214, observed value 80). The WRITE is `audio`'s setRingtoneVolume \u2014 that capability owns every volume wire."
|
|
6785
6997
|
},
|
|
@@ -6884,7 +7096,7 @@ var DOORBELL_MEMBERS = {
|
|
|
6884
7096
|
param: 1710,
|
|
6885
7097
|
type: "string",
|
|
6886
7098
|
kind: "text",
|
|
6887
|
-
provenance: "
|
|
7099
|
+
provenance: "verified",
|
|
6888
7100
|
description: "Notification config JSON {notification_motion_onoff,notification_ring_onoff,notification_style} (1710; confirmed on T8214)."
|
|
6889
7101
|
},
|
|
6890
7102
|
/**
|
|
@@ -7538,127 +7750,48 @@ var ARMING_MEMBERS = {
|
|
|
7538
7750
|
try {
|
|
7539
7751
|
return sink.dispatch(alarmDelayCommand(mode, config, ctx));
|
|
7540
7752
|
} catch (e) {
|
|
7541
|
-
return Promise.reject(e);
|
|
7542
|
-
}
|
|
7543
|
-
}, "Write the full per-mode alarm/arm-delay configuration.")
|
|
7544
|
-
};
|
|
7545
|
-
var ARMING = {
|
|
7546
|
-
capability: "arming",
|
|
7547
|
-
description: "Station/device guard (arming) mode.",
|
|
7548
|
-
members: ARMING_MEMBERS,
|
|
7549
|
-
properties: propertiesOf(ARMING_MEMBERS),
|
|
7550
|
-
// A reported guard-mode param is the verified proof; every station-codec device also owns an
|
|
7551
|
-
// arming surface as part of its baseline.
|
|
7552
|
-
detection: { evidenceParams: [ARMING_CMD.SET_ARMING], codecs: ["station"] },
|
|
7553
|
-
ownedByStation: true,
|
|
7554
|
-
/**
|
|
7555
|
-
* Station-scoped push events. The hub reports a guard-mode switch and the alarm lifecycle on the
|
|
7556
|
-
* generic `CusPushEvent` channel, so these ids are shared with other families and resolve by
|
|
7557
|
-
* capability (see the barrel's event index).
|
|
7558
|
-
*
|
|
7559
|
-
* The alarm ids carry a static `phase` so one event name covers the whole lifecycle, the same shape
|
|
7560
|
-
* `battery` uses for its threshold pushes. The mode-switch push is known to identify WHICH mode and
|
|
7561
|
-
* what triggered the change, but neither has been observed on the wire here, so this emits the bare
|
|
7562
|
-
* transition and refreshes the `mode` read rather than trusting a decoded field.
|
|
7563
|
-
*/
|
|
7564
|
-
events: [
|
|
7565
|
-
{
|
|
7566
|
-
source: "push",
|
|
7567
|
-
match: CusPushEvent.MODE_SWITCH,
|
|
7568
|
-
emit: "armingModeChanged",
|
|
7569
|
-
refresh: { member: "mode" }
|
|
7570
|
-
},
|
|
7571
|
-
{ source: "push", match: CusPushEvent.ALARM, emit: "alarm", payload: { phase: "triggered" } },
|
|
7572
|
-
{ source: "push", match: CusPushEvent.ALARM_DELAY, emit: "alarm", payload: { phase: "delayed" } }
|
|
7573
|
-
]
|
|
7574
|
-
};
|
|
7575
|
-
|
|
7576
|
-
// dist/model/capabilities/storage.js
|
|
7577
|
-
var STORAGE = {
|
|
7578
|
-
capability: "storage",
|
|
7579
|
-
description: "Local storage (SD card / eMMC / HDD). Behaviour-only until a capacity param is captured.",
|
|
7580
|
-
properties: [],
|
|
7581
|
-
detection: { codecs: ["station"] }
|
|
7582
|
-
};
|
|
7583
|
-
|
|
7584
|
-
// dist/core/raw-dp-writer.js
|
|
7585
|
-
var WIRE = { VARINT: 0, LENGTH_DELIMITED: 2 };
|
|
7586
|
-
function encodeVarint(value) {
|
|
7587
|
-
if (!Number.isSafeInteger(value) || value < 0) {
|
|
7588
|
-
throw new Error(`varint: ${value} is not a non-negative safe integer`);
|
|
7589
|
-
}
|
|
7590
|
-
const out = [];
|
|
7591
|
-
let v = value;
|
|
7592
|
-
while (v > 127) {
|
|
7593
|
-
out.push(v % 128 + 128);
|
|
7594
|
-
v = Math.floor(v / 128);
|
|
7595
|
-
}
|
|
7596
|
-
out.push(v);
|
|
7597
|
-
return out;
|
|
7598
|
-
}
|
|
7599
|
-
function zigzag(value) {
|
|
7600
|
-
if (!Number.isSafeInteger(value))
|
|
7601
|
-
throw new Error(`zigzag: ${value} is not a safe integer`);
|
|
7602
|
-
return value < 0 ? -2 * value - 1 : 2 * value;
|
|
7603
|
-
}
|
|
7604
|
-
var RawDpWriter = class _RawDpWriter {
|
|
7605
|
-
body = [];
|
|
7606
|
-
/** A varint-valued field (`int32`, `uint32`, `bool`, an enum member). */
|
|
7607
|
-
int(field, value) {
|
|
7608
|
-
this.body.push(...encodeVarint(tag(field, WIRE.VARINT)), ...encodeVarint(value));
|
|
7609
|
-
return this;
|
|
7610
|
-
}
|
|
7611
|
-
/** A signed varint field (`sint32`, `sint64`), ZigZag-encoded — see {@link zigzag}. */
|
|
7612
|
-
sint(field, value) {
|
|
7613
|
-
return this.int(field, zigzag(value));
|
|
7614
|
-
}
|
|
7615
|
-
/** A boolean field, on protobuf's own terms: `1` or `0` in a varint. */
|
|
7616
|
-
bool(field, value) {
|
|
7617
|
-
return this.int(field, value ? 1 : 0);
|
|
7618
|
-
}
|
|
7619
|
-
/** A length-delimited field carrying raw bytes — a string, a blob, or a message built elsewhere. */
|
|
7620
|
-
bytes(field, value) {
|
|
7621
|
-
const bytes2 = value instanceof Uint8Array ? Array.from(value) : [...value];
|
|
7622
|
-
this.body.push(...encodeVarint(tag(field, WIRE.LENGTH_DELIMITED)), ...encodeVarint(bytes2.length), ...bytes2);
|
|
7623
|
-
return this;
|
|
7624
|
-
}
|
|
7625
|
-
/**
|
|
7626
|
-
* A nested sub-message, built by `build` into a writer of its own.
|
|
7627
|
-
*
|
|
7628
|
-
* The sub-message is emitted even when `build` writes nothing into it. That is not an oversight: an
|
|
7629
|
-
* EMPTY sub-message is how these protocols say "this subsystem exists and is in its zero state", as
|
|
7630
|
-
* opposed to saying nothing about it, and the readers on the other side of this file depend on the
|
|
7631
|
-
* distinction. Omitting the call is what says nothing about it.
|
|
7632
|
-
*/
|
|
7633
|
-
sub(field, build) {
|
|
7634
|
-
const inner = new _RawDpWriter();
|
|
7635
|
-
build(inner);
|
|
7636
|
-
return this.bytes(field, inner.toBytes());
|
|
7637
|
-
}
|
|
7638
|
-
/** The message body on its own, unframed — for nesting into another writer. */
|
|
7639
|
-
toBytes() {
|
|
7640
|
-
return [...this.body];
|
|
7641
|
-
}
|
|
7753
|
+
return Promise.reject(e);
|
|
7754
|
+
}
|
|
7755
|
+
}, "Write the full per-mode alarm/arm-delay configuration.")
|
|
7756
|
+
};
|
|
7757
|
+
var ARMING = {
|
|
7758
|
+
capability: "arming",
|
|
7759
|
+
description: "Station/device guard (arming) mode.",
|
|
7760
|
+
members: ARMING_MEMBERS,
|
|
7761
|
+
properties: propertiesOf(ARMING_MEMBERS),
|
|
7762
|
+
// A reported guard-mode param is the verified proof; every station-codec device also owns an
|
|
7763
|
+
// arming surface as part of its baseline.
|
|
7764
|
+
detection: { evidenceParams: [ARMING_CMD.SET_ARMING], codecs: ["station"] },
|
|
7765
|
+
ownedByStation: true,
|
|
7642
7766
|
/**
|
|
7643
|
-
* The
|
|
7767
|
+
* Station-scoped push events. The hub reports a guard-mode switch and the alarm lifecycle on the
|
|
7768
|
+
* generic `CusPushEvent` channel, so these ids are shared with other families and resolve by
|
|
7769
|
+
* capability (see the barrel's event index).
|
|
7644
7770
|
*
|
|
7645
|
-
*
|
|
7646
|
-
*
|
|
7771
|
+
* The alarm ids carry a static `phase` so one event name covers the whole lifecycle, the same shape
|
|
7772
|
+
* `battery` uses for its threshold pushes. The mode-switch push is known to identify WHICH mode and
|
|
7773
|
+
* what triggered the change, but neither has been observed on the wire here, so this emits the bare
|
|
7774
|
+
* transition and refreshes the `mode` read rather than trusting a decoded field.
|
|
7647
7775
|
*/
|
|
7648
|
-
|
|
7649
|
-
|
|
7650
|
-
|
|
7776
|
+
events: [
|
|
7777
|
+
{
|
|
7778
|
+
source: "push",
|
|
7779
|
+
match: CusPushEvent.MODE_SWITCH,
|
|
7780
|
+
emit: "armingModeChanged",
|
|
7781
|
+
refresh: { member: "mode" }
|
|
7782
|
+
},
|
|
7783
|
+
{ source: "push", match: CusPushEvent.ALARM, emit: "alarm", payload: { phase: "triggered" } },
|
|
7784
|
+
{ source: "push", match: CusPushEvent.ALARM_DELAY, emit: "alarm", payload: { phase: "delayed" } }
|
|
7785
|
+
]
|
|
7786
|
+
};
|
|
7787
|
+
|
|
7788
|
+
// dist/model/capabilities/storage.js
|
|
7789
|
+
var STORAGE = {
|
|
7790
|
+
capability: "storage",
|
|
7791
|
+
description: "Local storage (SD card / eMMC / HDD). Behaviour-only until a capacity param is captured.",
|
|
7792
|
+
properties: [],
|
|
7793
|
+
detection: { codecs: ["station"] }
|
|
7651
7794
|
};
|
|
7652
|
-
function tag(field, wire) {
|
|
7653
|
-
if (!Number.isSafeInteger(field) || field < 1)
|
|
7654
|
-
throw new Error(`field number: ${field} is not a positive integer`);
|
|
7655
|
-
return field * 8 + wire;
|
|
7656
|
-
}
|
|
7657
|
-
function rawDp(build) {
|
|
7658
|
-
const w = new RawDpWriter();
|
|
7659
|
-
build(w);
|
|
7660
|
-
return w.finish();
|
|
7661
|
-
}
|
|
7662
7795
|
|
|
7663
7796
|
// dist/model/capabilities/vacuum-dock.js
|
|
7664
7797
|
var VACUUM_DOCK_DP = 173;
|
|
@@ -11059,7 +11192,8 @@ function resolveProperties(rec, codec, capabilities) {
|
|
|
11059
11192
|
model: rec.model,
|
|
11060
11193
|
category: rec.category,
|
|
11061
11194
|
capabilities: new Set(capabilities),
|
|
11062
|
-
paramIds
|
|
11195
|
+
paramIds,
|
|
11196
|
+
stationSerial: rec.parentSn
|
|
11063
11197
|
});
|
|
11064
11198
|
}
|
|
11065
11199
|
|
|
@@ -13249,7 +13383,8 @@ var Device = class _Device {
|
|
|
13249
13383
|
codec: this.codec,
|
|
13250
13384
|
model: this.model,
|
|
13251
13385
|
capabilities: new Set(this.capabilities),
|
|
13252
|
-
homeBaseAttached: this.stationSn !== this.sn
|
|
13386
|
+
homeBaseAttached: this.stationSn !== this.sn,
|
|
13387
|
+
stationSerial: this.stationSn
|
|
13253
13388
|
})
|
|
13254
13389
|
};
|
|
13255
13390
|
}
|
|
@@ -13364,58 +13499,6 @@ function parseCleanRecordDetail(blob, codec) {
|
|
|
13364
13499
|
};
|
|
13365
13500
|
}
|
|
13366
13501
|
|
|
13367
|
-
// dist/core/lz4-block.js
|
|
13368
|
-
var MAX_OUTPUT = 64 * 1024 * 1024;
|
|
13369
|
-
function lz4BlockDecompress(block, size) {
|
|
13370
|
-
if (!Number.isSafeInteger(size) || size < 0 || size > MAX_OUTPUT)
|
|
13371
|
-
return void 0;
|
|
13372
|
-
const out = Buffer.alloc(size);
|
|
13373
|
-
const end = block.length;
|
|
13374
|
-
let pos = 0;
|
|
13375
|
-
let written = 0;
|
|
13376
|
-
const extend2 = (base) => {
|
|
13377
|
-
let len = base;
|
|
13378
|
-
if (base !== 15)
|
|
13379
|
-
return len;
|
|
13380
|
-
for (; ; ) {
|
|
13381
|
-
if (pos >= end)
|
|
13382
|
-
return void 0;
|
|
13383
|
-
const extra = block[pos++];
|
|
13384
|
-
len += extra;
|
|
13385
|
-
if (extra !== 255)
|
|
13386
|
-
return len;
|
|
13387
|
-
}
|
|
13388
|
-
};
|
|
13389
|
-
while (pos < end) {
|
|
13390
|
-
const token = block[pos++];
|
|
13391
|
-
const literals = extend2(token >> 4);
|
|
13392
|
-
if (literals === void 0)
|
|
13393
|
-
return void 0;
|
|
13394
|
-
if (pos + literals > end || written + literals > size)
|
|
13395
|
-
return void 0;
|
|
13396
|
-
for (let i = 0; i < literals; i++)
|
|
13397
|
-
out[written++] = block[pos++];
|
|
13398
|
-
if (pos === end)
|
|
13399
|
-
break;
|
|
13400
|
-
if (pos + 2 > end)
|
|
13401
|
-
return void 0;
|
|
13402
|
-
const offset = block[pos] | block[pos + 1] << 8;
|
|
13403
|
-
pos += 2;
|
|
13404
|
-
if (offset === 0 || offset > written)
|
|
13405
|
-
return void 0;
|
|
13406
|
-
const match = extend2(token & 15);
|
|
13407
|
-
if (match === void 0)
|
|
13408
|
-
return void 0;
|
|
13409
|
-
const length = match + 4;
|
|
13410
|
-
if (written + length > size)
|
|
13411
|
-
return void 0;
|
|
13412
|
-
let from = written - offset;
|
|
13413
|
-
for (let i = 0; i < length; i++)
|
|
13414
|
-
out[written++] = out[from++];
|
|
13415
|
-
}
|
|
13416
|
-
return written === size ? out : void 0;
|
|
13417
|
-
}
|
|
13418
|
-
|
|
13419
13502
|
// dist/model/vacuum-map.js
|
|
13420
13503
|
var DOCK_KINDS = ["charger", "station"];
|
|
13421
13504
|
var MAP_QUALITIES = ["incomplete", "rough", "effective", "listFull"];
|
|
@@ -14326,6 +14409,7 @@ __export(codec_exports, {
|
|
|
14326
14409
|
buildLookupWithKeyPayload2: () => buildLookupWithKeyPayload2,
|
|
14327
14410
|
buildRawCommandPayload: () => buildRawCommandPayload,
|
|
14328
14411
|
buildStringCommandPayload: () => buildStringCommandPayload,
|
|
14412
|
+
buildStringPairCommandPayload: () => buildStringPairCommandPayload,
|
|
14329
14413
|
buildVoidCommandPayload: () => buildVoidCommandPayload,
|
|
14330
14414
|
decodeP2PCloudIPs: () => decodeP2PCloudIPs,
|
|
14331
14415
|
decryptP2PData: () => decryptP2PData,
|
|
@@ -14362,6 +14446,11 @@ var ResponseMessageType = {
|
|
|
14362
14446
|
LOOKUP_ADDR: Buffer.from([241, 64]),
|
|
14363
14447
|
LOOKUP_ADDR2: Buffer.from([241, 130]),
|
|
14364
14448
|
CAM_ID: Buffer.from([241, 66]),
|
|
14449
|
+
/**
|
|
14450
|
+
* The device's own address record, sent in answer to CHECK_CAM ahead of CAM_ID: its 20-byte device id, one
|
|
14451
|
+
* record in `encodeSelfAddress`'s shape naming the address it answers from, then 8 zero bytes (44-byte payload).
|
|
14452
|
+
*/
|
|
14453
|
+
CAM_ADDR: Buffer.from([241, 67]),
|
|
14365
14454
|
TURN_SERVER_CAM_ID: Buffer.from([241, 132]),
|
|
14366
14455
|
PING: Buffer.from([241, 224]),
|
|
14367
14456
|
PONG: Buffer.from([241, 225]),
|
|
@@ -14573,6 +14662,12 @@ function buildIntStringCommandPayload(value, valueSub, strValue, channel = 0, ke
|
|
|
14573
14662
|
data
|
|
14574
14663
|
]);
|
|
14575
14664
|
}
|
|
14665
|
+
function buildStringPairCommandPayload(strValue, strValueSub, channel = 0, key, encType = 1) {
|
|
14666
|
+
const encrypted = !!key && key.length === 16;
|
|
14667
|
+
const body = Buffer.concat([Buffer.alloc(5), stringWithLength(strValue), stringWithLength(strValueSub)]);
|
|
14668
|
+
const data = encrypted ? encryptP2PData(paddingP2PData(body), key) : body;
|
|
14669
|
+
return buildRawCommandPayload(data, channel, encrypted ? encType : 0);
|
|
14670
|
+
}
|
|
14576
14671
|
function buildRawCommandPayload(data, channel = 0, signCode = 0, magic = [1, 0], streamId = 0) {
|
|
14577
14672
|
const header = Buffer.allocUnsafe(2);
|
|
14578
14673
|
header.writeUInt16LE(data.length, 0);
|
|
@@ -15204,6 +15299,7 @@ var SEQUENCE_LOOKBACK = 32768;
|
|
|
15204
15299
|
var STALE_RETRANSMIT_DEPTH = 1024;
|
|
15205
15300
|
var PUNCH_PROBE_SOCKETS = 7;
|
|
15206
15301
|
var REORDER_WAIT_MS = 250;
|
|
15302
|
+
var VIDEO_REORDER_WAIT_MS = 700;
|
|
15207
15303
|
var REORDER_MAX_DATAGRAMS = 128;
|
|
15208
15304
|
var MAX_TRACED_DATAGRAM_GAPS = 8;
|
|
15209
15305
|
var CMD_SET_PAYLOAD = 1350;
|
|
@@ -15214,6 +15310,7 @@ var CMD_DATABASE_IMAGE = 1308;
|
|
|
15214
15310
|
var CMD_DATABASE = 1306;
|
|
15215
15311
|
var DB_QUERY = { FULL_TABLE: 1e4, COMBINATION_WITH_AI: 10011 };
|
|
15216
15312
|
var GCM_AAD = Buffer.from("eufy security");
|
|
15313
|
+
var LEVEL2_SEQ_BASE = 16909056;
|
|
15217
15314
|
function paramReport(payload) {
|
|
15218
15315
|
const list = payload?.params;
|
|
15219
15316
|
if (!Array.isArray(list))
|
|
@@ -15228,6 +15325,49 @@ function paramReport(payload) {
|
|
|
15228
15325
|
return Object.keys(out).length ? out : void 0;
|
|
15229
15326
|
}
|
|
15230
15327
|
var traceSequence = 0;
|
|
15328
|
+
var DB_TABLE_TIMEOUT_MS = 15e3;
|
|
15329
|
+
function firstTableRows(text2) {
|
|
15330
|
+
const decoded = Buffer.from(text2, "latin1").toString("utf8");
|
|
15331
|
+
for (let start = decoded.indexOf("{"); start >= 0; start = decoded.indexOf("{", start + 1)) {
|
|
15332
|
+
const end = closingBrace(decoded, start);
|
|
15333
|
+
if (end < 0)
|
|
15334
|
+
return void 0;
|
|
15335
|
+
let parsed;
|
|
15336
|
+
try {
|
|
15337
|
+
parsed = JSON.parse(decoded.slice(start, end + 1));
|
|
15338
|
+
} catch {
|
|
15339
|
+
continue;
|
|
15340
|
+
}
|
|
15341
|
+
const data = parsed?.data;
|
|
15342
|
+
if (Array.isArray(data))
|
|
15343
|
+
return data;
|
|
15344
|
+
}
|
|
15345
|
+
return void 0;
|
|
15346
|
+
}
|
|
15347
|
+
function closingBrace(text2, start) {
|
|
15348
|
+
let depth = 0;
|
|
15349
|
+
let inString = false;
|
|
15350
|
+
let escaped = false;
|
|
15351
|
+
for (let i = start; i < text2.length; i++) {
|
|
15352
|
+
const c = text2[i];
|
|
15353
|
+
if (inString) {
|
|
15354
|
+
if (escaped)
|
|
15355
|
+
escaped = false;
|
|
15356
|
+
else if (c === "\\")
|
|
15357
|
+
escaped = true;
|
|
15358
|
+
else if (c === '"')
|
|
15359
|
+
inString = false;
|
|
15360
|
+
continue;
|
|
15361
|
+
}
|
|
15362
|
+
if (c === '"')
|
|
15363
|
+
inString = true;
|
|
15364
|
+
else if (c === "{")
|
|
15365
|
+
depth++;
|
|
15366
|
+
else if (c === "}" && --depth === 0)
|
|
15367
|
+
return i;
|
|
15368
|
+
}
|
|
15369
|
+
return -1;
|
|
15370
|
+
}
|
|
15231
15371
|
var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
15232
15372
|
cfg;
|
|
15233
15373
|
socket;
|
|
@@ -15262,8 +15402,8 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
15262
15402
|
audioStalled = false;
|
|
15263
15403
|
audioRetransmitTimer;
|
|
15264
15404
|
lastPongData;
|
|
15265
|
-
/** When
|
|
15266
|
-
|
|
15405
|
+
/** When the selected peer last answered outbound P2P traffic on this connection. */
|
|
15406
|
+
lastPeerAt;
|
|
15267
15407
|
/** Whether the silence has already been stated, so it is traced once per connection rather than per read. */
|
|
15268
15408
|
pathStaleTraced = false;
|
|
15269
15409
|
lookupTimer;
|
|
@@ -15332,17 +15472,17 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
15332
15472
|
/**
|
|
15333
15473
|
* How long this connection's path has been silent, or nothing where it has never answered.
|
|
15334
15474
|
*
|
|
15335
|
-
*
|
|
15336
|
-
*
|
|
15475
|
+
* PONG and ACK from the selected peer prove the path answers outbound traffic. `undefined` means no such reply
|
|
15476
|
+
* has arrived since connection, so silence alone does not establish a stale path.
|
|
15337
15477
|
*/
|
|
15338
15478
|
get pathSilentMs() {
|
|
15339
|
-
return this.
|
|
15479
|
+
return this.lastPeerAt === void 0 ? void 0 : Date.now() - this.lastPeerAt;
|
|
15340
15480
|
}
|
|
15341
15481
|
/**
|
|
15342
15482
|
* Whether this path can still be committed to, on the evidence the heartbeat gives.
|
|
15343
15483
|
*
|
|
15344
|
-
* False where
|
|
15345
|
-
* not known to be dead, so it answers true.
|
|
15484
|
+
* False where selected-peer replies arrived and then stopped for {@link PATH_SILENCE_MS}. A connection with
|
|
15485
|
+
* no post-connect reply is not known to be dead, so it answers true.
|
|
15346
15486
|
*
|
|
15347
15487
|
* Traces the silence once per connection, on the read that first observes it.
|
|
15348
15488
|
*/
|
|
@@ -15618,7 +15758,8 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
15618
15758
|
this.trace({
|
|
15619
15759
|
phase: "lookup-channels",
|
|
15620
15760
|
local: !this.cfg.noBroadcast || this.cfg.localAddress !== void 0,
|
|
15621
|
-
cloud: this.cloudLookup !== void 0
|
|
15761
|
+
cloud: this.cloudLookup !== void 0,
|
|
15762
|
+
...this.cloudLookup ? {} : { cloudMissing: dskKey ? "cloud-addresses" : "dsk-key" }
|
|
15622
15763
|
});
|
|
15623
15764
|
this.sendLookups();
|
|
15624
15765
|
this.lookupTimer = setInterval(() => this.sendLookups(), LOOKUP_RETRY_MS);
|
|
@@ -15720,10 +15861,18 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
15720
15861
|
* straddling a level-2 wait on a camera whose failure to deliver video had a separate cause, and did not
|
|
15721
15862
|
* recur across later probes of it. Correlate an unmodelled type against a WORKING session before reading it
|
|
15722
15863
|
* as a cause.
|
|
15864
|
+
*
|
|
15865
|
+
* CAM_ADDR is recognised and dropped: it names the address the device is already answering from, and the
|
|
15866
|
+
* CAM_ID that follows it is what completes the connect.
|
|
15723
15867
|
*/
|
|
15724
15868
|
onMessage(msg, rinfo, socket = this.socket) {
|
|
15725
15869
|
if (!socket)
|
|
15726
15870
|
return;
|
|
15871
|
+
const fromConnectedPeer = this.connected && this.connectAddress?.host === rinfo.address && this.connectAddress.port === rinfo.port;
|
|
15872
|
+
if (fromConnectedPeer && (hasHeader(msg, ResponseMessageType.PONG) || hasHeader(msg, ResponseMessageType.ACK))) {
|
|
15873
|
+
this.lastPeerAt = Date.now();
|
|
15874
|
+
this.pathStaleTraced = false;
|
|
15875
|
+
}
|
|
15727
15876
|
if (!hasHeader(msg, ResponseMessageType.DATA)) {
|
|
15728
15877
|
this.logger.debug(`[p2p] ${this.cfg.stationSn} <<< ${rinfo.address}:${rinfo.port} header=${msg.subarray(0, 2).toString("hex")} len=${msg.length}`);
|
|
15729
15878
|
}
|
|
@@ -15741,8 +15890,6 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
15741
15890
|
this.onConnected({ host: rinfo.address, port: rinfo.port }, socket);
|
|
15742
15891
|
} else if (hasHeader(msg, ResponseMessageType.PONG)) {
|
|
15743
15892
|
this.lastPongData = msg.length > 4 ? msg.subarray(4) : void 0;
|
|
15744
|
-
this.lastPongAt = Date.now();
|
|
15745
|
-
this.pathStaleTraced = false;
|
|
15746
15893
|
} else if (hasHeader(msg, ResponseMessageType.PING)) {
|
|
15747
15894
|
this.send({ host: rinfo.address, port: rinfo.port }, RequestMessageType.PONG, void 0, socket);
|
|
15748
15895
|
} else if (hasHeader(msg, ResponseMessageType.ACK)) {
|
|
@@ -15750,7 +15897,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
15750
15897
|
} else if (hasHeader(msg, ResponseMessageType.DATA)) {
|
|
15751
15898
|
if (this.connected)
|
|
15752
15899
|
this.onData(msg, { host: rinfo.address, port: rinfo.port });
|
|
15753
|
-
} else {
|
|
15900
|
+
} else if (!hasHeader(msg, ResponseMessageType.CAM_ADDR)) {
|
|
15754
15901
|
this.logger.debug(`[p2p] ${this.cfg.stationSn} UNHANDLED payload hex: ${msg.toString("hex")}`);
|
|
15755
15902
|
}
|
|
15756
15903
|
}
|
|
@@ -16041,6 +16188,17 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16041
16188
|
this.seqNumber = this.seqNumber + 1 & 65535;
|
|
16042
16189
|
this.send(this.connectAddress, RequestMessageType.DATA, data);
|
|
16043
16190
|
}
|
|
16191
|
+
/** Send a level-1 {@link buildStringPairCommandPayload} command on `channel`. */
|
|
16192
|
+
sendStringPairCommand(commandType, strValue, strValueSub, channel) {
|
|
16193
|
+
if (!this.connectAddress)
|
|
16194
|
+
throw new Error(`P2P session ${this.cfg.stationSn} is not connected`);
|
|
16195
|
+
const data = Buffer.concat([
|
|
16196
|
+
buildCommandHeader(this.seqNumber, commandType),
|
|
16197
|
+
buildStringPairCommandPayload(strValue, strValueSub, channel, this.level1Key, 1)
|
|
16198
|
+
]);
|
|
16199
|
+
this.seqNumber = this.seqNumber + 1 & 65535;
|
|
16200
|
+
this.send(this.connectAddress, RequestMessageType.DATA, data);
|
|
16201
|
+
}
|
|
16044
16202
|
/**
|
|
16045
16203
|
* Send a **level-2 (AES-256-GCM, signCode 8) control payload** to a HomeBase-attached device. The
|
|
16046
16204
|
* target camera is selected by `channel` (= device_channel) + the `mChannel` envelope — the same
|
|
@@ -16090,8 +16248,17 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16090
16248
|
this.send(this.connectAddress, RequestMessageType.DATA, data);
|
|
16091
16249
|
return true;
|
|
16092
16250
|
}
|
|
16093
|
-
/**
|
|
16094
|
-
|
|
16251
|
+
/**
|
|
16252
|
+
* Stop the realtime media stream (`CMD_STOP_REALTIME_MEDIA`, 1004) on a camera `channel`, in the shape
|
|
16253
|
+
* its runtime topology takes, as {@link startLiveMedia} selects its start:
|
|
16254
|
+
* - `homeBaseAttached`: `CMD_SET_PAYLOAD` (1350) wrapping `{cmd:1004, mChannel:channel}` at level-2.
|
|
16255
|
+
* - own-session at level-2: the direct 1004 frame whose entire plaintext is the camera channel as a
|
|
16256
|
+
* `uint32`, the same 4-byte body as the attached talkback frames. The app sends this frame on closing
|
|
16257
|
+
* an own-session camera's live view; the camera stops streaming on it within a second, and keeps
|
|
16258
|
+
* streaming through the 1350-wrapped form.
|
|
16259
|
+
* - without a level-2 key: the bare 1004 command.
|
|
16260
|
+
*/
|
|
16261
|
+
stopLiveMedia(channel = STATION_CHANNEL3, accountId = "", homeBaseAttached = false) {
|
|
16095
16262
|
this.liveStartedChannels.delete(channel);
|
|
16096
16263
|
for (const [sequence, pending] of this.unackedLiveStarts) {
|
|
16097
16264
|
if (pending.channel === channel)
|
|
@@ -16101,7 +16268,11 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16101
16268
|
clearInterval(this.liveStartRetransmitTimer);
|
|
16102
16269
|
this.liveStartRetransmitTimer = void 0;
|
|
16103
16270
|
}
|
|
16104
|
-
if (this.level2Key) {
|
|
16271
|
+
if (this.level2Key && !homeBaseAttached) {
|
|
16272
|
+
const body = Buffer.allocUnsafe(4);
|
|
16273
|
+
body.writeUInt32LE(channel >>> 0, 0);
|
|
16274
|
+
this.sendRawLevel2Bytes(body, channel, CMD_STOP_REALTIME_MEDIA, 8);
|
|
16275
|
+
} else if (this.level2Key) {
|
|
16105
16276
|
this.sendMediaPayloadLevel2(CMD_STOP_REALTIME_MEDIA, channel, accountId, {});
|
|
16106
16277
|
} else {
|
|
16107
16278
|
this.sendCommand(CMD_STOP_REALTIME_MEDIA, channel);
|
|
@@ -16318,11 +16489,6 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16318
16489
|
this.seqNumber = this.seqNumber + 1 & 65535;
|
|
16319
16490
|
this.send(this.connectAddress, RequestMessageType.DATA, data);
|
|
16320
16491
|
}
|
|
16321
|
-
/**
|
|
16322
|
-
* Encrypt a level-2 command body (signCode 8): `tag(16) ‖ nonce(12) ‖ [seq,03,02,01](4) ‖
|
|
16323
|
-
* ciphertext`, AES-256-GCM under the negotiated session key, AAD "eufy security". Inverse of
|
|
16324
|
-
* `decryptLevel2`. The 4-byte sub-header is cleartext (skipped on decrypt); `seq` is a counter.
|
|
16325
|
-
*/
|
|
16326
16492
|
/**
|
|
16327
16493
|
* Decode a `CMD_VIDEO_FRAME` (1300) payload into clean Annex-B H.264 (the 22-byte frame header
|
|
16328
16494
|
* stripped). Reversed from the V6 app + live H.264 captures: the 22-byte header is
|
|
@@ -16389,6 +16555,11 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16389
16555
|
}
|
|
16390
16556
|
return this.rsaModulusHex;
|
|
16391
16557
|
}
|
|
16558
|
+
/**
|
|
16559
|
+
* Encrypt a level-2 command body (signCode 8): `tag(16) ‖ nonce(12) ‖ seq(4) ‖ ciphertext`,
|
|
16560
|
+
* AES-256-GCM under the negotiated session key, AAD "eufy security". Inverse of `decryptLevel2`.
|
|
16561
|
+
* The 4-byte sub-header is cleartext (skipped on decrypt); `seq` counts up from {@link LEVEL2_SEQ_BASE}.
|
|
16562
|
+
*/
|
|
16392
16563
|
encryptLevel2(plaintext) {
|
|
16393
16564
|
if (!this.level2Key)
|
|
16394
16565
|
return void 0;
|
|
@@ -16396,8 +16567,8 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16396
16567
|
const c = createCipheriv3("aes-256-gcm", this.level2Key, nonce);
|
|
16397
16568
|
c.setAAD(GCM_AAD);
|
|
16398
16569
|
const ct = Buffer.concat([c.update(plaintext), c.final()]);
|
|
16399
|
-
const sub2 = Buffer.
|
|
16400
|
-
|
|
16570
|
+
const sub2 = Buffer.alloc(4);
|
|
16571
|
+
sub2.writeUInt32LE(LEVEL2_SEQ_BASE + this.level2Seq++ >>> 0);
|
|
16401
16572
|
return Buffer.concat([c.getAuthTag(), nonce, sub2, ct]);
|
|
16402
16573
|
}
|
|
16403
16574
|
/** Send a no-arg command frame (e.g. CMD_GATEWAYINFO) on a channel (default: the station channel). */
|
|
@@ -16443,8 +16614,14 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16443
16614
|
* The HomeBase streams back `CMD_DATABASE` (1306) frames `{cmd:10000,count,data:[…]}`,
|
|
16444
16615
|
* level-1-encrypted — decoded and emitted as `dbChunk` (decrypted text) per frame.
|
|
16445
16616
|
* Tables: `familiar_faces`, `person_basic_info`, `event_person_list`, `history_record_info`.
|
|
16617
|
+
*
|
|
16618
|
+
* Throws while a {@link readDatabase} is accumulating: every table answers `{data:[…]}` and the
|
|
16619
|
+
* frames tie no chunk to its request, so a second query's reply would be assembled into the first
|
|
16620
|
+
* one's buffer and answered as its rows.
|
|
16446
16621
|
*/
|
|
16447
16622
|
queryDatabase(table, opts = {}) {
|
|
16623
|
+
if (this.dbReadInFlight)
|
|
16624
|
+
throw new Error(`queryDatabase: ${this.cfg.stationSn} is already reading a table`);
|
|
16448
16625
|
if (!this.connectAddress)
|
|
16449
16626
|
throw new Error("not connected");
|
|
16450
16627
|
const channel = opts.channel ?? STATION_CHANNEL3;
|
|
@@ -16507,6 +16684,49 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16507
16684
|
query: this.fullTableQuery()
|
|
16508
16685
|
});
|
|
16509
16686
|
}
|
|
16687
|
+
/**
|
|
16688
|
+
* Query one on-station table and answer its rows, once the reply is whole.
|
|
16689
|
+
*
|
|
16690
|
+
* The request half of {@link queryDatabase} with its reply assembled: `CMD_DATABASE` arrives as
|
|
16691
|
+
* several frames whose decrypted text is a fragment of one document, so the fragments are
|
|
16692
|
+
* accumulated here and scanned after each one. Answers that document's `data` rows. Rejects when
|
|
16693
|
+
* `signal` aborts, and when `timeoutMs` (default {@link DB_TABLE_TIMEOUT_MS}) elapses with no
|
|
16694
|
+
* complete reply.
|
|
16695
|
+
*
|
|
16696
|
+
* One read at a time per session: while one is accumulating, every {@link queryDatabase} on the
|
|
16697
|
+
* session throws, so a second reply cannot land in this buffer.
|
|
16698
|
+
*/
|
|
16699
|
+
async readDatabase(table, opts = {}) {
|
|
16700
|
+
if (opts.signal?.aborted)
|
|
16701
|
+
throw new Error("readDatabase: aborted");
|
|
16702
|
+
this.queryDatabase(table, { accountId: opts.accountId, query: this.fullTableQuery() });
|
|
16703
|
+
this.dbReadInFlight = true;
|
|
16704
|
+
try {
|
|
16705
|
+
return await new Promise((resolve, reject) => {
|
|
16706
|
+
let text2 = "";
|
|
16707
|
+
const onChunk = (chunk) => {
|
|
16708
|
+
text2 += chunk.text;
|
|
16709
|
+
const rows = firstTableRows(text2);
|
|
16710
|
+
if (rows)
|
|
16711
|
+
settle(() => resolve(rows));
|
|
16712
|
+
};
|
|
16713
|
+
const timer = setTimeout(() => settle(() => reject(new Error(`readDatabase: ${this.cfg.stationSn} sent no complete ${table}`))), opts.timeoutMs ?? DB_TABLE_TIMEOUT_MS);
|
|
16714
|
+
const onAbort = () => settle(() => reject(new Error("readDatabase: aborted")));
|
|
16715
|
+
const settle = (finish) => {
|
|
16716
|
+
clearTimeout(timer);
|
|
16717
|
+
this.off("dbChunk", onChunk);
|
|
16718
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
16719
|
+
finish();
|
|
16720
|
+
};
|
|
16721
|
+
this.on("dbChunk", onChunk);
|
|
16722
|
+
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
16723
|
+
});
|
|
16724
|
+
} finally {
|
|
16725
|
+
this.dbReadInFlight = false;
|
|
16726
|
+
}
|
|
16727
|
+
}
|
|
16728
|
+
/** Whether a {@link readDatabase} is accumulating; see {@link queryDatabase} for what it bars. */
|
|
16729
|
+
dbReadInFlight = false;
|
|
16510
16730
|
/**
|
|
16511
16731
|
* Request the **face feature rows** over P2P (`face_feature_info`, inner `cmd 10000`). Each row
|
|
16512
16732
|
* carries `{person_id, face_name, face_id, face_picture_content, face_feature_file_path}` — where
|
|
@@ -16574,7 +16794,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16574
16794
|
* if the missing one arrives. A logical frame's payload spans datagrams that carry no header of their
|
|
16575
16795
|
* own, so reassembling around a hole is impossible; waiting for it is what keeps the frame whole.
|
|
16576
16796
|
*
|
|
16577
|
-
* Only once
|
|
16797
|
+
* Only once the data type's reorder wait passes, or {@link REORDER_MAX_DATAGRAMS} pile up, is the datagram
|
|
16578
16798
|
* treated as lost: a pending frame is discarded and delivery resumes from the earliest held datagram.
|
|
16579
16799
|
*/
|
|
16580
16800
|
onData(msg, addr) {
|
|
@@ -16629,7 +16849,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16629
16849
|
const timer = setTimeout(() => {
|
|
16630
16850
|
reorder.timer = void 0;
|
|
16631
16851
|
this.abandonHole(dataType);
|
|
16632
|
-
}, REORDER_WAIT_MS);
|
|
16852
|
+
}, dataType === P2PDataType.VIDEO ? VIDEO_REORDER_WAIT_MS : REORDER_WAIT_MS);
|
|
16633
16853
|
timer.unref?.();
|
|
16634
16854
|
reorder.timer = timer;
|
|
16635
16855
|
}
|
|
@@ -17818,7 +18038,7 @@ var LiveStream = class extends EventEmitter3 {
|
|
|
17818
18038
|
clearTimeout(this.stallTimer);
|
|
17819
18039
|
this.stallTimer = void 0;
|
|
17820
18040
|
try {
|
|
17821
|
-
this.session.stopLiveMedia(this.opts.channel, this.opts.accountId);
|
|
18041
|
+
this.session.stopLiveMedia(this.opts.channel, this.opts.accountId, this.opts.homeBaseAttached);
|
|
17822
18042
|
} catch (e) {
|
|
17823
18043
|
this.logger.debug(`[live] stopLiveMedia ignored: ${e instanceof Error ? e.message : e}`);
|
|
17824
18044
|
}
|
|
@@ -18219,6 +18439,190 @@ function annexbToJpeg(annexb, spawn2) {
|
|
|
18219
18439
|
});
|
|
18220
18440
|
}
|
|
18221
18441
|
|
|
18442
|
+
// dist/transport/p2p/recording-download.js
|
|
18443
|
+
import { createDecipheriv as createDecipheriv7 } from "node:crypto";
|
|
18444
|
+
var RECORDING_DATA_TYPE = 3;
|
|
18445
|
+
var VIDEO_HEADER_LEN2 = 22;
|
|
18446
|
+
var AUDIO_HEADER_LEN2 = 16;
|
|
18447
|
+
var AUDIO_NONCE_START = 32;
|
|
18448
|
+
var AUDIO_BODY_START = 44;
|
|
18449
|
+
var AUDIO_CODEC_AAC_LC = 0;
|
|
18450
|
+
var FINISH_CHANNEL = 0;
|
|
18451
|
+
var FIRST_FRAME_WAIT_MS = 2e4;
|
|
18452
|
+
var IDLE_END_MS = 15e3;
|
|
18453
|
+
var DEFAULT_TRANSFER_MS = 12e4;
|
|
18454
|
+
var MAX_TRANSFER_BYTES = 32 * 1024 * 1024;
|
|
18455
|
+
function homeBase2RecordingPath(channel, recording) {
|
|
18456
|
+
if (!/^\d{14}$/.test(recording) || !Number.isInteger(channel) || channel < 0 || channel > 99)
|
|
18457
|
+
return void 0;
|
|
18458
|
+
return `/media/mmcblk0p1/Camera${String(channel).padStart(2, "0")}/${recording}.dat`;
|
|
18459
|
+
}
|
|
18460
|
+
function receiveRecording(session, request) {
|
|
18461
|
+
let resolveFrames;
|
|
18462
|
+
let rejectFrames;
|
|
18463
|
+
const frames = new Promise((resolve, reject) => {
|
|
18464
|
+
resolveFrames = resolve;
|
|
18465
|
+
rejectFrames = reject;
|
|
18466
|
+
});
|
|
18467
|
+
let resolveDrained;
|
|
18468
|
+
const drained = new Promise((resolve) => resolveDrained = resolve);
|
|
18469
|
+
const limitMs = request.timeoutMs ?? DEFAULT_TRANSFER_MS;
|
|
18470
|
+
let collected = [];
|
|
18471
|
+
const started = Date.now();
|
|
18472
|
+
let lastFrameAt = started;
|
|
18473
|
+
let received = false;
|
|
18474
|
+
let bytes2 = 0;
|
|
18475
|
+
let draining;
|
|
18476
|
+
let delivered = false;
|
|
18477
|
+
let listening = true;
|
|
18478
|
+
const deliver = (error) => {
|
|
18479
|
+
if (delivered)
|
|
18480
|
+
return;
|
|
18481
|
+
delivered = true;
|
|
18482
|
+
request.signal?.removeEventListener("abort", onAbort);
|
|
18483
|
+
if (error)
|
|
18484
|
+
rejectFrames(error);
|
|
18485
|
+
else
|
|
18486
|
+
resolveFrames(collected);
|
|
18487
|
+
};
|
|
18488
|
+
const stop = () => {
|
|
18489
|
+
if (!listening)
|
|
18490
|
+
return;
|
|
18491
|
+
listening = false;
|
|
18492
|
+
clearInterval(tick);
|
|
18493
|
+
session.off("data", onData);
|
|
18494
|
+
resolveDrained();
|
|
18495
|
+
};
|
|
18496
|
+
const incomplete = (why) => deliver(new RecordingDownloadError("incomplete", `the recording transfer ${why} before its finish frame`));
|
|
18497
|
+
const drain = (why) => {
|
|
18498
|
+
draining ??= why;
|
|
18499
|
+
collected = [];
|
|
18500
|
+
};
|
|
18501
|
+
const onData = (frame) => {
|
|
18502
|
+
if (frame.commandId === CommandType.CMD_DOWNLOAD_FINISH) {
|
|
18503
|
+
if (frame.channel !== request.channel && frame.channel !== FINISH_CHANNEL)
|
|
18504
|
+
return;
|
|
18505
|
+
if (draining)
|
|
18506
|
+
incomplete(draining);
|
|
18507
|
+
else
|
|
18508
|
+
deliver();
|
|
18509
|
+
return stop();
|
|
18510
|
+
}
|
|
18511
|
+
if (frame.channel !== request.channel)
|
|
18512
|
+
return;
|
|
18513
|
+
if (frame.commandId !== CommandType.CMD_VIDEO_FRAME && frame.commandId !== CommandType.CMD_AUDIO_FRAME)
|
|
18514
|
+
return;
|
|
18515
|
+
if (frame.dataType !== RECORDING_DATA_TYPE)
|
|
18516
|
+
return;
|
|
18517
|
+
received = true;
|
|
18518
|
+
lastFrameAt = Date.now();
|
|
18519
|
+
if (draining)
|
|
18520
|
+
return;
|
|
18521
|
+
collected.push({ commandId: frame.commandId, signCode: frame.signCode, raw: Buffer.from(frame.raw) });
|
|
18522
|
+
bytes2 += frame.raw.length;
|
|
18523
|
+
if (bytes2 >= MAX_TRANSFER_BYTES)
|
|
18524
|
+
drain(`reached ${MAX_TRANSFER_BYTES} bytes`);
|
|
18525
|
+
};
|
|
18526
|
+
const onAbort = () => {
|
|
18527
|
+
deliver(request.signal?.reason instanceof Error ? request.signal.reason : new Error("aborted"));
|
|
18528
|
+
drain("was aborted");
|
|
18529
|
+
};
|
|
18530
|
+
const tick = setInterval(() => {
|
|
18531
|
+
const now2 = Date.now();
|
|
18532
|
+
if (!received && now2 - started > FIRST_FRAME_WAIT_MS) {
|
|
18533
|
+
deliver(new RecordingDownloadError("no-data", `the station sent nothing within ${FIRST_FRAME_WAIT_MS} ms`));
|
|
18534
|
+
stop();
|
|
18535
|
+
} else if (received && now2 - lastFrameAt > IDLE_END_MS) {
|
|
18536
|
+
incomplete(draining ?? `went quiet for ${IDLE_END_MS} ms`);
|
|
18537
|
+
stop();
|
|
18538
|
+
} else if (now2 - started > 2 * limitMs && draining) {
|
|
18539
|
+
incomplete(draining);
|
|
18540
|
+
stop();
|
|
18541
|
+
} else if (now2 - started > limitMs) {
|
|
18542
|
+
drain(`ran past ${limitMs} ms`);
|
|
18543
|
+
}
|
|
18544
|
+
}, 250);
|
|
18545
|
+
if (request.signal?.aborted) {
|
|
18546
|
+
onAbort();
|
|
18547
|
+
stop();
|
|
18548
|
+
return { frames, drained };
|
|
18549
|
+
}
|
|
18550
|
+
request.signal?.addEventListener("abort", onAbort, { once: true });
|
|
18551
|
+
session.on("data", onData);
|
|
18552
|
+
try {
|
|
18553
|
+
session.sendStringPairCommand(CommandType.CMD_DOWNLOAD_VIDEO, request.path, request.accountId, request.channel);
|
|
18554
|
+
} catch (error) {
|
|
18555
|
+
deliver(error instanceof Error ? error : new Error(String(error)));
|
|
18556
|
+
stop();
|
|
18557
|
+
}
|
|
18558
|
+
return { frames, drained };
|
|
18559
|
+
}
|
|
18560
|
+
function openAudio(key, nonce, tag2, body) {
|
|
18561
|
+
try {
|
|
18562
|
+
const decipher = createDecipheriv7("aes-256-gcm", key, nonce);
|
|
18563
|
+
decipher.setAAD(VIDEO_GCM_AAD);
|
|
18564
|
+
decipher.setAuthTag(tag2);
|
|
18565
|
+
return Buffer.concat([decipher.update(body), decipher.final()]);
|
|
18566
|
+
} catch {
|
|
18567
|
+
return void 0;
|
|
18568
|
+
}
|
|
18569
|
+
}
|
|
18570
|
+
function decodeRecording(frames, eccPrivateKeyHex) {
|
|
18571
|
+
const eccPrivateKey = Buffer.from(eccPrivateKeyHex, "hex");
|
|
18572
|
+
if (eccPrivateKey.length !== 32)
|
|
18573
|
+
throw new RecordingDownloadError("undecodable", "the cipher key is not a P-256 key");
|
|
18574
|
+
const decoder = new VideoFrameDecoder(eccPrivateKey);
|
|
18575
|
+
const video = [];
|
|
18576
|
+
const audio = [];
|
|
18577
|
+
const numbers = /* @__PURE__ */ new Set();
|
|
18578
|
+
let firstStamp;
|
|
18579
|
+
let lastStamp;
|
|
18580
|
+
for (const { commandId, raw } of frames) {
|
|
18581
|
+
if (commandId === CommandType.CMD_VIDEO_FRAME) {
|
|
18582
|
+
const header = parseVideoFrameHeader(raw);
|
|
18583
|
+
if (!header)
|
|
18584
|
+
continue;
|
|
18585
|
+
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;
|
|
18586
|
+
if (!h264?.length)
|
|
18587
|
+
continue;
|
|
18588
|
+
video.push(h264);
|
|
18589
|
+
numbers.add(header.sequence);
|
|
18590
|
+
const stamp = raw.readUIntLE(14, 6);
|
|
18591
|
+
firstStamp ??= stamp;
|
|
18592
|
+
lastStamp = stamp;
|
|
18593
|
+
} else if (commandId === CommandType.CMD_AUDIO_FRAME) {
|
|
18594
|
+
const mediaKey = decoder.currentMediaKey;
|
|
18595
|
+
if (!mediaKey || raw.length < AUDIO_BODY_START || raw[5] !== AUDIO_CODEC_AAC_LC)
|
|
18596
|
+
continue;
|
|
18597
|
+
const len = raw.readUInt32LE(0);
|
|
18598
|
+
if (raw.length < AUDIO_BODY_START + len)
|
|
18599
|
+
continue;
|
|
18600
|
+
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));
|
|
18601
|
+
if (aac?.length)
|
|
18602
|
+
audio.push(aac);
|
|
18603
|
+
}
|
|
18604
|
+
}
|
|
18605
|
+
if (!video.length)
|
|
18606
|
+
throw new RecordingDownloadError("undecodable", "no video frame of the recording decoded");
|
|
18607
|
+
const ordered = [...numbers];
|
|
18608
|
+
let missingFrames = 0;
|
|
18609
|
+
for (let i = 1; i < ordered.length; i++) {
|
|
18610
|
+
const step = ordered[i] - ordered[i - 1] & 65535;
|
|
18611
|
+
if (step > 1 && step < 32768)
|
|
18612
|
+
missingFrames += step - 1;
|
|
18613
|
+
}
|
|
18614
|
+
const span = ordered.length > 1 ? ordered[ordered.length - 1] - ordered[0] & 65535 : 0;
|
|
18615
|
+
const durationMs = (lastStamp ?? 0) - (firstStamp ?? 0);
|
|
18616
|
+
return {
|
|
18617
|
+
video: Buffer.concat(video),
|
|
18618
|
+
...audio.length ? { audio: Buffer.concat(audio) } : {},
|
|
18619
|
+
frames: ordered.length,
|
|
18620
|
+
missingFrames,
|
|
18621
|
+
durationMs,
|
|
18622
|
+
fps: durationMs > 0 && span > 0 ? Math.round(span * 1e5 / durationMs) / 100 : 0
|
|
18623
|
+
};
|
|
18624
|
+
}
|
|
18625
|
+
|
|
18222
18626
|
// dist/transport/p2p/shared-live-source.js
|
|
18223
18627
|
import { EventEmitter as EventEmitter4 } from "node:events";
|
|
18224
18628
|
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
@@ -20399,6 +20803,8 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
20399
20803
|
talkbacks = /* @__PURE__ */ new Map();
|
|
20400
20804
|
/** cipher_id → ECC private key (one eufylife get_ciphers call per cipher), shared across (re)opens. */
|
|
20401
20805
|
cipherKeyCache = /* @__PURE__ */ new Map();
|
|
20806
|
+
/** The recording download in flight per station; a station serves one at a time. */
|
|
20807
|
+
recordingDownloads = /* @__PURE__ */ new Map();
|
|
20402
20808
|
constructor(deps) {
|
|
20403
20809
|
this.deps = deps;
|
|
20404
20810
|
this.manager = new SessionManager({
|
|
@@ -20611,28 +21017,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
20611
21017
|
localAddress,
|
|
20612
21018
|
dskKey,
|
|
20613
21019
|
noBroadcast: this.deps.noBroadcast,
|
|
20614
|
-
resolveCipherKey:
|
|
20615
|
-
if (this.cipherKeyCache.has(cipherId))
|
|
20616
|
-
return this.cipherKeyCache.get(cipherId);
|
|
20617
|
-
let ecc;
|
|
20618
|
-
try {
|
|
20619
|
-
const ciphers = await this.deps.mega.getCiphers([cipherId], adminUserId, stationSn);
|
|
20620
|
-
ecc = ciphers.find((c) => Number(c.cipher_id) === cipherId)?.ecc_private_key;
|
|
20621
|
-
if (ecc === void 0 && ciphers[0]?.ecc_private_key !== void 0) {
|
|
20622
|
-
ecc = ciphers[0].ecc_private_key;
|
|
20623
|
-
this.traceOnStation(session, {
|
|
20624
|
-
phase: "cipher-fallback",
|
|
20625
|
-
cipherId,
|
|
20626
|
-
answeredCipherId: Number(ciphers[0].cipher_id)
|
|
20627
|
-
});
|
|
20628
|
-
}
|
|
20629
|
-
} catch (e) {
|
|
20630
|
-
this.deps.onError(e instanceof Error ? e : new Error(String(e)));
|
|
20631
|
-
}
|
|
20632
|
-
if (ecc !== void 0)
|
|
20633
|
-
this.cipherKeyCache.set(cipherId, ecc);
|
|
20634
|
-
return ecc;
|
|
20635
|
-
},
|
|
21020
|
+
resolveCipherKey: (cipherId) => this.cipherKeyFor(session, cipherId, adminUserId, stationSn),
|
|
20636
21021
|
logger: this.deps.logger ?? noopLogger
|
|
20637
21022
|
});
|
|
20638
21023
|
session.on("error", (e) => this.reportError(e));
|
|
@@ -20656,6 +21041,33 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
20656
21041
|
session.on("data", (f) => this.deps.onFrame(stationSn, f));
|
|
20657
21042
|
return session;
|
|
20658
21043
|
}
|
|
21044
|
+
/**
|
|
21045
|
+
* The ECC private key of `cipherId`, from the router's cache or one `get_ciphers` call. When the cloud
|
|
21046
|
+
* answers with another cipher, that one is used and traced on `session`. Only a successful lookup is
|
|
21047
|
+
* cached.
|
|
21048
|
+
*/
|
|
21049
|
+
async cipherKeyFor(session, cipherId, userId, stationSn) {
|
|
21050
|
+
if (this.cipherKeyCache.has(cipherId))
|
|
21051
|
+
return this.cipherKeyCache.get(cipherId);
|
|
21052
|
+
let ecc;
|
|
21053
|
+
try {
|
|
21054
|
+
const ciphers = await this.deps.mega.getCiphers([cipherId], userId, stationSn);
|
|
21055
|
+
ecc = ciphers.find((c) => Number(c.cipher_id) === cipherId)?.ecc_private_key;
|
|
21056
|
+
if (ecc === void 0 && ciphers[0]?.ecc_private_key !== void 0) {
|
|
21057
|
+
ecc = ciphers[0].ecc_private_key;
|
|
21058
|
+
this.traceOnStation(session, {
|
|
21059
|
+
phase: "cipher-fallback",
|
|
21060
|
+
cipherId,
|
|
21061
|
+
answeredCipherId: Number(ciphers[0].cipher_id)
|
|
21062
|
+
});
|
|
21063
|
+
}
|
|
21064
|
+
} catch (e) {
|
|
21065
|
+
this.deps.onError(e instanceof Error ? e : new Error(String(e)));
|
|
21066
|
+
}
|
|
21067
|
+
if (ecc !== void 0)
|
|
21068
|
+
this.cipherKeyCache.set(cipherId, ecc);
|
|
21069
|
+
return ecc;
|
|
21070
|
+
}
|
|
20659
21071
|
/**
|
|
20660
21072
|
* Open (or reuse) a station's P2P session and await its completed handshake. An optional abort only
|
|
20661
21073
|
* stops this wait; session ownership remains with {@link SessionManager} and its normal teardown.
|
|
@@ -20783,7 +21195,9 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
20783
21195
|
* start has no level-1 form for.
|
|
20784
21196
|
*/
|
|
20785
21197
|
mediaProviderFor(sn) {
|
|
21198
|
+
const recordings = this.downloadsRecordings(sn) ? { downloadRecording: (opts) => this.downloadRecording(sn, opts) } : {};
|
|
20786
21199
|
return {
|
|
21200
|
+
...recordings,
|
|
20787
21201
|
snapshotLive: async (opts) => {
|
|
20788
21202
|
const source = await this.sharedLiveSourceFor(sn, opts ?? {});
|
|
20789
21203
|
return captureSnapshotFromShared(source, {
|
|
@@ -20822,6 +21236,63 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
20822
21236
|
}
|
|
20823
21237
|
};
|
|
20824
21238
|
}
|
|
21239
|
+
/**
|
|
21240
|
+
* Whether `sn` is a camera attached to a HomeBase 2 (T8010), the one station whose recording path layout
|
|
21241
|
+
* and frame formats are confirmed.
|
|
21242
|
+
*/
|
|
21243
|
+
downloadsRecordings(sn) {
|
|
21244
|
+
const dev = this.recordFor(sn);
|
|
21245
|
+
if (!dev)
|
|
21246
|
+
return false;
|
|
21247
|
+
const parentSn = stationOf(dev);
|
|
21248
|
+
return parentSn !== sn && this.recordFor(parentSn)?.model === "T8010";
|
|
21249
|
+
}
|
|
21250
|
+
/**
|
|
21251
|
+
* Download one recording a HomeBase 2 holds for an attached camera, then decode it
|
|
21252
|
+
* ({@link decodeRecording}). Downloads queue per station: the station streams one recording at a time
|
|
21253
|
+
* on the camera's channel, so a download starts only once the previous one has drained
|
|
21254
|
+
* ({@link RecordingTransfer.drained}), even when that one's caller already has its answer. An abort
|
|
21255
|
+
* while waiting rejects without sending anything; an abort mid-transfer rejects at once and the station's
|
|
21256
|
+
* turn is held until the transfer drains.
|
|
21257
|
+
*/
|
|
21258
|
+
async downloadRecording(sn, opts) {
|
|
21259
|
+
opts.signal?.throwIfAborted();
|
|
21260
|
+
const parentSn = this.stationKeyOf(sn);
|
|
21261
|
+
const previous = this.recordingDownloads.get(parentSn) ?? Promise.resolve();
|
|
21262
|
+
const started = this.waitTurn(previous, opts.signal).then(() => this.startRecordingDownload(sn, opts));
|
|
21263
|
+
const turn = Promise.allSettled([previous, started.then((t) => t.drained)]).then(() => {
|
|
21264
|
+
if (this.recordingDownloads.get(parentSn) === turn)
|
|
21265
|
+
this.recordingDownloads.delete(parentSn);
|
|
21266
|
+
});
|
|
21267
|
+
this.recordingDownloads.set(parentSn, turn);
|
|
21268
|
+
return started.then((t) => t.recording);
|
|
21269
|
+
}
|
|
21270
|
+
/** Resolve once `previous` settles, or reject as soon as `signal` aborts. */
|
|
21271
|
+
waitTurn(previous, signal) {
|
|
21272
|
+
return abortable(previous.then(() => void 0, () => void 0), signal);
|
|
21273
|
+
}
|
|
21274
|
+
/**
|
|
21275
|
+
* Start one recording download, once the station's previous download has drained: the decoded recording,
|
|
21276
|
+
* and when the station is done sending it.
|
|
21277
|
+
*/
|
|
21278
|
+
async startRecordingDownload(sn, opts) {
|
|
21279
|
+
const { session, parentSn, channel, accountId } = await this.resolveSession(sn, { signal: opts.signal });
|
|
21280
|
+
const path = homeBase2RecordingPath(channel, opts.recording);
|
|
21281
|
+
if (!path)
|
|
21282
|
+
throw new RecordingDownloadError("invalid-recording", `no recording named ${opts.recording}`);
|
|
21283
|
+
const eccKey = await this.cipherKeyFor(session, opts.cipherId, accountId, parentSn);
|
|
21284
|
+
if (!eccKey) {
|
|
21285
|
+
throw new RecordingDownloadError("key-unavailable", `cipher ${opts.cipherId} could not be obtained`);
|
|
21286
|
+
}
|
|
21287
|
+
const transfer = receiveRecording(session, {
|
|
21288
|
+
path,
|
|
21289
|
+
accountId,
|
|
21290
|
+
channel,
|
|
21291
|
+
timeoutMs: opts.timeoutMs,
|
|
21292
|
+
signal: opts.signal
|
|
21293
|
+
});
|
|
21294
|
+
return { recording: transfer.frames.then((frames) => decodeRecording(frames, eccKey)), drained: transfer.drained };
|
|
21295
|
+
}
|
|
20825
21296
|
/**
|
|
20826
21297
|
* Open a {@link Talkback} on a device's camera channel.
|
|
20827
21298
|
*
|
|
@@ -21170,9 +21641,9 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
21170
21641
|
*
|
|
21171
21642
|
* Reached two ways, both idempotent: the session's own `close` event, when it died while still the
|
|
21172
21643
|
* station's registered session, and {@link SessionManagerOpts.onAutoClose}, when the manager closed it
|
|
21173
|
-
* unasked. A close a CALLER made is deliberately not routed here — {@link closeAll}
|
|
21174
|
-
* sources first,
|
|
21175
|
-
* on the replacement session.
|
|
21644
|
+
* unasked. A close a CALLER made is deliberately not routed here — {@link closeAll} and
|
|
21645
|
+
* {@link resolveSession} dispose their own sources first, while {@link replaceUnreachableSession} keeps
|
|
21646
|
+
* its source alive on purpose to rewarm it on the replacement session.
|
|
21176
21647
|
*/
|
|
21177
21648
|
tearDownStation(stationSn) {
|
|
21178
21649
|
this.manager.remove(stationSn);
|
|
@@ -21491,9 +21962,10 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
21491
21962
|
*
|
|
21492
21963
|
* A session whose {@link P2PSession.pathAnswering} is false is closed and re-resolved before it is handed
|
|
21493
21964
|
* over: the station answers every heartbeat, so a path silent past several of them is gone. A session
|
|
21494
|
-
* reporting nothing about its path is not reporting that evidence and is handed over as it is.
|
|
21495
|
-
*
|
|
21496
|
-
*
|
|
21965
|
+
* reporting nothing about its path is not reporting that evidence and is handed over as it is. Cached live
|
|
21966
|
+
* sources on the replaced session are discarded before closing it (see {@link tearDownStation}).
|
|
21967
|
+
* Replaced at most once per resolution, so a station whose replacement is silent too is returned rather
|
|
21968
|
+
* than closed again.
|
|
21497
21969
|
*/
|
|
21498
21970
|
async resolveSession(sn, opts = {}, rebuilt = false) {
|
|
21499
21971
|
const dev = await this.deviceFor(sn);
|
|
@@ -21507,6 +21979,10 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
21507
21979
|
}
|
|
21508
21980
|
if (session.pathAnswering === false && !rebuilt) {
|
|
21509
21981
|
(this.deps.logger ?? noopLogger).debug(`[p2p] ${parentSn} path stopped answering \u2014 rebuilding before use`);
|
|
21982
|
+
for (const [key, sessionKey] of [...this.liveSessionKeys]) {
|
|
21983
|
+
if (sessionKey === parentSn)
|
|
21984
|
+
this.dropLiveSource(key);
|
|
21985
|
+
}
|
|
21510
21986
|
await this.manager.close(parentSn).catch((error) => this.reportError(error instanceof Error ? error : new Error(String(error))));
|
|
21511
21987
|
return await this.resolveSession(sn, opts, true);
|
|
21512
21988
|
}
|
|
@@ -24347,6 +24823,7 @@ var MEDIA_FAILURE_REASONS = [
|
|
|
24347
24823
|
// dist/transport/stored-image-cache.js
|
|
24348
24824
|
var MAX_JPEG_BYTES = 10 * 1024 * 1024;
|
|
24349
24825
|
var MAX_REMEMBERED_URLS = 64;
|
|
24826
|
+
var NOT_YET_PUBLISHED_RETRY_MS = [2e3, 4e3];
|
|
24350
24827
|
var DIAGNOSTIC_INTERVAL_MS = 6e4;
|
|
24351
24828
|
var StoredImageCache = class {
|
|
24352
24829
|
downloader;
|
|
@@ -24384,6 +24861,8 @@ var StoredImageCache = class {
|
|
|
24384
24861
|
if (state.seenUrls.has(url))
|
|
24385
24862
|
return;
|
|
24386
24863
|
state.seenUrls.add(url);
|
|
24864
|
+
clearTimeout(state.retrying);
|
|
24865
|
+
state.retrying = void 0;
|
|
24387
24866
|
while (state.seenUrls.size > MAX_REMEMBERED_URLS) {
|
|
24388
24867
|
const oldest = state.seenUrls.values().next();
|
|
24389
24868
|
if (oldest.done)
|
|
@@ -24395,14 +24874,15 @@ var StoredImageCache = class {
|
|
|
24395
24874
|
url,
|
|
24396
24875
|
sequence: ++state.nextSequence,
|
|
24397
24876
|
observedAt: this.clock(),
|
|
24398
|
-
generation: this.generation
|
|
24877
|
+
generation: this.generation,
|
|
24878
|
+
retries: 0
|
|
24399
24879
|
};
|
|
24400
24880
|
this.pump();
|
|
24401
24881
|
}
|
|
24402
24882
|
/** Return retained bytes without starting or awaiting network work. */
|
|
24403
24883
|
snapshotStored(deviceKey) {
|
|
24404
24884
|
const state = this.devices.get(deviceKey);
|
|
24405
|
-
const reason = state?.queued || state?.inFlight ? "pending" : state?.reason ?? "not-observed";
|
|
24885
|
+
const reason = state?.queued || state?.inFlight || state?.retrying ? "pending" : state?.reason ?? "not-observed";
|
|
24406
24886
|
if (reason !== "pending" && state?.lifecycleError)
|
|
24407
24887
|
return Promise.reject(state.lifecycleError);
|
|
24408
24888
|
if (state?.retained)
|
|
@@ -24436,6 +24916,8 @@ var StoredImageCache = class {
|
|
|
24436
24916
|
if (error !== void 0 && this.isLifecycleError(error)) {
|
|
24437
24917
|
state.lifecycleError = error;
|
|
24438
24918
|
state.reason = void 0;
|
|
24919
|
+
} else if (image === void 0 && this.retryLater(state, candidate, error)) {
|
|
24920
|
+
state.lifecycleError = void 0;
|
|
24439
24921
|
} else if (image === void 0) {
|
|
24440
24922
|
state.lifecycleError = void 0;
|
|
24441
24923
|
state.reason = "download-failed";
|
|
@@ -24452,6 +24934,21 @@ var StoredImageCache = class {
|
|
|
24452
24934
|
}
|
|
24453
24935
|
this.pump();
|
|
24454
24936
|
}
|
|
24937
|
+
/** Schedule the next attempt after a 404; answers whether one was scheduled. */
|
|
24938
|
+
retryLater(state, candidate, error) {
|
|
24939
|
+
const delay = NOT_YET_PUBLISHED_RETRY_MS[candidate.retries];
|
|
24940
|
+
const tag2 = mediaFailureTag(error);
|
|
24941
|
+
if (delay === void 0 || tag2.cause !== "http-status" || tag2.status !== 404 || state.queued)
|
|
24942
|
+
return false;
|
|
24943
|
+
const timer = setTimeout(() => {
|
|
24944
|
+
state.retrying = void 0;
|
|
24945
|
+
state.queued = { ...candidate, retries: candidate.retries + 1 };
|
|
24946
|
+
this.pump();
|
|
24947
|
+
}, delay);
|
|
24948
|
+
timer.unref?.();
|
|
24949
|
+
state.retrying = timer;
|
|
24950
|
+
return true;
|
|
24951
|
+
}
|
|
24455
24952
|
isValidJpeg(image) {
|
|
24456
24953
|
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;
|
|
24457
24954
|
}
|
|
@@ -25047,6 +25544,11 @@ function recordString(raw, key) {
|
|
|
25047
25544
|
const v = typeof nested === "string" && nested ? nested : raw[key];
|
|
25048
25545
|
return typeof v === "string" && v ? v : void 0;
|
|
25049
25546
|
}
|
|
25547
|
+
function adminUserIdFrom(raw) {
|
|
25548
|
+
const member = (raw ?? {}).member;
|
|
25549
|
+
const id = member?.admin_user_id;
|
|
25550
|
+
return typeof id === "string" && id ? id : void 0;
|
|
25551
|
+
}
|
|
25050
25552
|
function tuyaDevIdFrom(raw) {
|
|
25051
25553
|
for (const field of ["tuya_uuid", "tuya_virtual_id", "tuya_device_id", "virtualId"]) {
|
|
25052
25554
|
const v = recordString(raw, field);
|
|
@@ -25204,13 +25706,7 @@ var EufyMega = class extends EventEmitter9 {
|
|
|
25204
25706
|
onError: (e) => this.reportError(e),
|
|
25205
25707
|
// The `a2` account id an `eufy_life` DP frame embeds — the owning member's `admin_user_id`,
|
|
25206
25708
|
// falling back to the logged-in account's user id (the confirmed script path does the same).
|
|
25207
|
-
resolveAccountId: (dev) =>
|
|
25208
|
-
const member = (dev.raw ?? {}).member;
|
|
25209
|
-
const adminId = member?.admin_user_id;
|
|
25210
|
-
if (typeof adminId === "string" && adminId)
|
|
25211
|
-
return adminId;
|
|
25212
|
-
return this.mega.auth?.userId ?? "";
|
|
25213
|
-
},
|
|
25709
|
+
resolveAccountId: (dev) => adminUserIdFrom(dev.raw) ?? this.mega.auth?.userId ?? "",
|
|
25214
25710
|
// Fetch + parse a gallery effect from the HTTP catalogue into the serializable spec. The catalogue
|
|
25215
25711
|
// lives in the http layer (transport/http/light-catalog); the mqtt router owns the frame bytes.
|
|
25216
25712
|
resolvePreset: (presetId) => resolveLightEffect(this.mega, presetId),
|
|
@@ -26657,6 +27153,37 @@ var EufyMega = class extends EventEmitter9 {
|
|
|
26657
27153
|
getP2pSessions() {
|
|
26658
27154
|
return this.p2p.getSessions();
|
|
26659
27155
|
}
|
|
27156
|
+
/**
|
|
27157
|
+
* The people enrolled on a station, read from its own `person_basic_info` table over P2P.
|
|
27158
|
+
*
|
|
27159
|
+
* The cloud roster (`api.getFaces()`) answers empty for an account that keeps its faces on the
|
|
27160
|
+
* station, because they were never uploaded — the station holds them.
|
|
27161
|
+
*
|
|
27162
|
+
* Resolves with the rows the station returned. Rejects on a station that cannot be reached, on a
|
|
27163
|
+
* record stating no `admin_user_id`, and on the reply not arriving whole in time. Assembling that
|
|
27164
|
+
* reply is `P2PSession.readDatabase`'s; this resolves which station and which account id.
|
|
27165
|
+
*
|
|
27166
|
+
* `stranger<n>` names are the station's own placeholders for a face nobody has named, and are
|
|
27167
|
+
* returned as they arrive; a caller that wants only named people filters them.
|
|
27168
|
+
*/
|
|
27169
|
+
async getStationFaces(stationSn, opts = {}) {
|
|
27170
|
+
if (!this.registry.list().length)
|
|
27171
|
+
await this.getDevices();
|
|
27172
|
+
const station = this.p2p.stationKeyOf(stationSn);
|
|
27173
|
+
const accountId = this.adminUserIdOf(station);
|
|
27174
|
+
if (!accountId)
|
|
27175
|
+
throw new Error(`getStationFaces: no admin_user_id on record for ${station}`);
|
|
27176
|
+
await this.p2p.ensureStation(station, opts.signal);
|
|
27177
|
+
const session = this.p2p.getSessions().get(station);
|
|
27178
|
+
if (!session)
|
|
27179
|
+
throw new Error(`getStationFaces: no P2P session for ${station}`);
|
|
27180
|
+
const rows = await session.readDatabase("person_basic_info", { accountId, ...opts });
|
|
27181
|
+
return rows.filter((row) => typeof row === "object" && row !== null);
|
|
27182
|
+
}
|
|
27183
|
+
/** The station's own `admin_user_id`, which its database refuses the query without. */
|
|
27184
|
+
adminUserIdOf(stationSn) {
|
|
27185
|
+
return adminUserIdFrom(this.registry.list().find((d) => d.sn === stationSn)?.raw);
|
|
27186
|
+
}
|
|
26660
27187
|
/**
|
|
26661
27188
|
* The liveness facts for one device — see {@link DeviceState}. Facts, not an `online` verdict: "how
|
|
26662
27189
|
* long is too long" is a threshold that belongs to the caller, and it differs per device class.
|
|
@@ -26817,6 +27344,7 @@ var EufyMega = class extends EventEmitter9 {
|
|
|
26817
27344
|
model: rec.model,
|
|
26818
27345
|
category: dev.category,
|
|
26819
27346
|
serial: sn,
|
|
27347
|
+
stationSerial: rec.parentSn,
|
|
26820
27348
|
// Identity metadata for `info`, for a host's device-registry surface. `name` = the app-shown
|
|
26821
27349
|
// device_name (registry's `name`), not the resolved codec/inferred name. firmware/hardware come
|
|
26822
27350
|
// straight off the device record: `main_sw_version` / `main_hw_version` — the same fields the v6
|
|
@@ -26905,7 +27433,10 @@ var EufyMega = class extends EventEmitter9 {
|
|
|
26905
27433
|
thumbnailUrl: ev.thumbnailUrl,
|
|
26906
27434
|
payload: ev.payload
|
|
26907
27435
|
};
|
|
26908
|
-
|
|
27436
|
+
const caps = this.capsForEvent(ev.deviceSn);
|
|
27437
|
+
const decoded = decodeEvent(signal2, caps);
|
|
27438
|
+
this.opts.logger?.debug(`[push] in: eventType=${ev.eventType} device=${caps ? "known" : ev.deviceSn ? "unknown" : "none"} events=${decoded.length}`);
|
|
27439
|
+
for (const out of decoded) {
|
|
26909
27440
|
this.emitSemantic(out.event, out.payload, { edge: true, refresh: out.refresh });
|
|
26910
27441
|
const dsn = out.payload.deviceSn ?? ev.deviceSn;
|
|
26911
27442
|
if (dsn)
|
|
@@ -27189,26 +27720,16 @@ var SolixClient = class _SolixClient {
|
|
|
27189
27720
|
}
|
|
27190
27721
|
/** Build the encrypted, signed `/passport/login` request body + headers for the negotiated key. */
|
|
27191
27722
|
async postLogin(kx, verifyCode, limitedToken) {
|
|
27192
|
-
const { clientPublicKeyHex, encryptedPassword } = encryptLoginPassword(this.password);
|
|
27193
27723
|
const bodyObj = {
|
|
27194
|
-
email
|
|
27195
|
-
password: encryptedPassword,
|
|
27196
|
-
ab: this.country,
|
|
27197
|
-
client_secret_info: { public_key: clientPublicKeyHex },
|
|
27724
|
+
...loginCredentials(this.email, this.password, this.country),
|
|
27198
27725
|
answer: "",
|
|
27199
27726
|
captcha_id: "",
|
|
27200
27727
|
verify_code: verifyCode ?? "",
|
|
27201
27728
|
login_id: ""
|
|
27202
27729
|
};
|
|
27203
27730
|
const encBody = encryptBody(JSON.stringify(bodyObj), kx.shareKey);
|
|
27204
|
-
const ts = nowSec();
|
|
27205
|
-
const once = genId();
|
|
27206
27731
|
return this.post(this.apiHost, SOLIX_ENDPOINTS.login, encBody, this.authHeaders({
|
|
27207
|
-
|
|
27208
|
-
"x-key-ident": kx.keyIdent,
|
|
27209
|
-
"x-request-ts": ts,
|
|
27210
|
-
"x-request-once": once,
|
|
27211
|
-
"x-signature": signRequest(kx.shareKey, ts, once, encBody),
|
|
27732
|
+
...signedHeaders(kx, encBody),
|
|
27212
27733
|
...limitedToken ? { "x-auth-token": limitedToken } : {}
|
|
27213
27734
|
}));
|
|
27214
27735
|
}
|
|
@@ -27223,13 +27744,12 @@ var SolixClient = class _SolixClient {
|
|
|
27223
27744
|
* observed refusing the header, which is consistent with the two ids agreeing on the accounts seen.
|
|
27224
27745
|
*/
|
|
27225
27746
|
classifyLogin(data, isVerify) {
|
|
27226
|
-
const
|
|
27227
|
-
|
|
27228
|
-
if (!userId || !authToken)
|
|
27747
|
+
const reply = readLoginReply(data);
|
|
27748
|
+
if (!reply)
|
|
27229
27749
|
throw new Error(`Solix login returned no session: ${JSON.stringify(data).slice(0, 160)}`);
|
|
27230
|
-
const
|
|
27231
|
-
if (!isVerify &&
|
|
27232
|
-
this.pending2fa = { limitedToken: authToken, userId, geoKey:
|
|
27750
|
+
const { userId, authToken } = reply;
|
|
27751
|
+
if (!isVerify && reply.twoFactorPending) {
|
|
27752
|
+
this.pending2fa = { limitedToken: authToken, userId, geoKey: reply.geoKey };
|
|
27233
27753
|
return { status: "2fa", method: "code sent by the passport" };
|
|
27234
27754
|
}
|
|
27235
27755
|
this.pending2fa = void 0;
|
|
@@ -27238,7 +27758,7 @@ var SolixClient = class _SolixClient {
|
|
|
27238
27758
|
userId,
|
|
27239
27759
|
gtoken: gtoken(userId),
|
|
27240
27760
|
apiHost: this.apiHost,
|
|
27241
|
-
tokenExpiresAt:
|
|
27761
|
+
tokenExpiresAt: reply.tokenExpiresAt
|
|
27242
27762
|
};
|
|
27243
27763
|
this.persist();
|
|
27244
27764
|
return { status: "ok", session: this.session_ };
|
|
@@ -27393,14 +27913,8 @@ var SolixClient = class _SolixClient {
|
|
|
27393
27913
|
throw new Error("not authenticated \u2014 call login() first");
|
|
27394
27914
|
const kx = await this.keyExchange();
|
|
27395
27915
|
const encBody = encryptBody(JSON.stringify(payload), kx.shareKey);
|
|
27396
|
-
const ts = nowSec();
|
|
27397
|
-
const once = genId();
|
|
27398
27916
|
const env = await this.post(this.apiHost, path, encBody, this.baseHeaders({
|
|
27399
|
-
|
|
27400
|
-
"x-key-ident": kx.keyIdent,
|
|
27401
|
-
"x-request-ts": ts,
|
|
27402
|
-
"x-request-once": once,
|
|
27403
|
-
"x-signature": signRequest(kx.shareKey, ts, once, encBody),
|
|
27917
|
+
...signedHeaders(kx, encBody),
|
|
27404
27918
|
"x-auth-token": this.session_.authToken,
|
|
27405
27919
|
gtoken: this.session_.gtoken
|
|
27406
27920
|
}));
|
|
@@ -28168,6 +28682,7 @@ export {
|
|
|
28168
28682
|
ROOM_SUCTIONS,
|
|
28169
28683
|
RTSP_MEMBERS,
|
|
28170
28684
|
RawDpWriter,
|
|
28685
|
+
RecordingDownloadError,
|
|
28171
28686
|
RecordingQuality,
|
|
28172
28687
|
RtspRecordingMode,
|
|
28173
28688
|
SCENE_INVALID_REASONS,
|
|
@@ -28211,6 +28726,7 @@ export {
|
|
|
28211
28726
|
VACUUM_DOCK_MEMBERS,
|
|
28212
28727
|
VACUUM_SCHEDULE_ACTIONS,
|
|
28213
28728
|
VACUUM_SCHEDULE_WEEKDAYS,
|
|
28729
|
+
VIDEO_GCM_AAD,
|
|
28214
28730
|
VacuumMapStore,
|
|
28215
28731
|
VideoFrameDecoder,
|
|
28216
28732
|
WORKING_MODE_MAPS,
|
|
@@ -28230,6 +28746,7 @@ export {
|
|
|
28230
28746
|
buildEventIndex,
|
|
28231
28747
|
buildModelIndex,
|
|
28232
28748
|
buildRealtimeInit,
|
|
28749
|
+
cameraPowerTier,
|
|
28233
28750
|
captureSnapshotFromShared,
|
|
28234
28751
|
cellAtPoint,
|
|
28235
28752
|
claimedParams,
|