@mega-yfue/eufy-sdk 0.4.0-beta.3 → 0.4.0-beta.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/eufy-mega.d.ts +22 -3
- package/dist/client/types.d.ts +25 -0
- package/dist/core/contracts.d.ts +45 -2
- package/dist/index.js +1096 -577
- package/dist/index.js.map +4 -4
- package/dist/model/capabilities/camera.d.ts +1 -0
- package/dist/model/capabilities/doorbell.d.ts +8 -9
- package/dist/model/capabilities/index.d.ts +1 -1
- package/dist/model/capabilities/manifest.d.ts +18 -2
- package/dist/model/capabilities/motion.d.ts +3 -4
- package/dist/model/capabilities/siren.d.ts +1 -1
- package/dist/model/param-dictionary.d.ts +3 -3
- package/dist/model/types.d.ts +10 -11
- package/dist/transport/http/passport.d.ts +21 -0
- package/dist/transport/http/solix-client.d.ts +5 -6
- package/dist/transport/mqtt/secure-mqtt.d.ts +20 -13
- package/dist/transport/p2p/codec.d.ts +13 -0
- package/dist/transport/p2p/command-router.d.ts +36 -6
- package/dist/transport/p2p/live-trace.d.ts +5 -0
- package/dist/transport/p2p/p2p-session.d.ts +50 -12
- package/dist/transport/p2p/recording-download.d.ts +54 -0
- package/dist/transport/p2p/shared-live-source.d.ts +1 -1
- package/dist/transport/p2p/video.d.ts +2 -0
- package/dist/transport/stored-image-cache.d.ts +2 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1070,6 +1070,409 @@ function normalizePushImage(data, p2pDid) {
|
|
|
1070
1070
|
return decodeImageV1(data, p2pDid) ?? data;
|
|
1071
1071
|
}
|
|
1072
1072
|
|
|
1073
|
+
// dist/core/types.js
|
|
1074
|
+
var PRINTER_CATEGORY_RE = /ankermake|eufymake|fdm|3d_?print/i;
|
|
1075
|
+
function classifyDevice(raw) {
|
|
1076
|
+
const category = raw.category ?? "";
|
|
1077
|
+
const hasP2p = typeof raw.p2p_did === "string" && raw.p2p_did.length > 0;
|
|
1078
|
+
if (category === "eufy_security" || hasP2p)
|
|
1079
|
+
return { category, api: "mega", realtime: "p2p" };
|
|
1080
|
+
if (PRINTER_CATEGORY_RE.test(category))
|
|
1081
|
+
return { category, api: "ankermake", realtime: "ankermake-mqtt" };
|
|
1082
|
+
return { category, api: "mega", realtime: "smqtt" };
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// dist/core/contracts.js
|
|
1086
|
+
var StoredSnapshotUnavailableError = class extends Error {
|
|
1087
|
+
reason;
|
|
1088
|
+
constructor(reason, message, options) {
|
|
1089
|
+
super(message, options);
|
|
1090
|
+
this.reason = reason;
|
|
1091
|
+
this.name = "StoredSnapshotUnavailableError";
|
|
1092
|
+
}
|
|
1093
|
+
};
|
|
1094
|
+
var RETRYABLE_LIVE_SNAPSHOT_REASONS = [
|
|
1095
|
+
"no-keyframe",
|
|
1096
|
+
"source-failed",
|
|
1097
|
+
"undecodable-burst"
|
|
1098
|
+
];
|
|
1099
|
+
var LiveSnapshotUnavailableError = class extends Error {
|
|
1100
|
+
reason;
|
|
1101
|
+
/** Whether another attempt could plausibly succeed without the host changing anything. */
|
|
1102
|
+
retryable;
|
|
1103
|
+
constructor(reason, message, options) {
|
|
1104
|
+
super(message, options);
|
|
1105
|
+
this.reason = reason;
|
|
1106
|
+
this.name = "LiveSnapshotUnavailableError";
|
|
1107
|
+
this.retryable = RETRYABLE_LIVE_SNAPSHOT_REASONS.includes(reason);
|
|
1108
|
+
}
|
|
1109
|
+
};
|
|
1110
|
+
var CameraDisabledError = class extends Error {
|
|
1111
|
+
camera;
|
|
1112
|
+
constructor(camera, options) {
|
|
1113
|
+
super(`camera${camera ? ` ${camera}` : ""} is disabled \u2014 no live stream possible`, options);
|
|
1114
|
+
this.camera = camera;
|
|
1115
|
+
this.name = "CameraDisabledError";
|
|
1116
|
+
}
|
|
1117
|
+
};
|
|
1118
|
+
var StationKeyUnavailableError = class extends Error {
|
|
1119
|
+
stationSn;
|
|
1120
|
+
/** Always true: the negotiation is per connection, so a later one may still produce a key. */
|
|
1121
|
+
retryable = true;
|
|
1122
|
+
constructor(stationSn, options) {
|
|
1123
|
+
super(`station ${stationSn} did not provide its session key, so nothing that requires one could be sent`, options);
|
|
1124
|
+
this.stationSn = stationSn;
|
|
1125
|
+
this.name = "StationKeyUnavailableError";
|
|
1126
|
+
}
|
|
1127
|
+
};
|
|
1128
|
+
var StationUnreachableError = class extends Error {
|
|
1129
|
+
stationSn;
|
|
1130
|
+
waitedMs;
|
|
1131
|
+
/** Always true: a station unreachable now may answer on a later attempt. */
|
|
1132
|
+
retryable = true;
|
|
1133
|
+
constructor(stationSn, waitedMs, options) {
|
|
1134
|
+
super(`station ${stationSn}'s P2P session did not connect within ${waitedMs}ms, so nothing could be sent to it`, options);
|
|
1135
|
+
this.stationSn = stationSn;
|
|
1136
|
+
this.waitedMs = waitedMs;
|
|
1137
|
+
this.name = "StationUnreachableError";
|
|
1138
|
+
}
|
|
1139
|
+
};
|
|
1140
|
+
var DeviceChannelUnresolvedError = class extends Error {
|
|
1141
|
+
sn;
|
|
1142
|
+
stationSn;
|
|
1143
|
+
constructor(sn, stationSn, options) {
|
|
1144
|
+
super(`${sn} has no usable channel on station ${stationSn}, so nothing was sent to it`, options);
|
|
1145
|
+
this.sn = sn;
|
|
1146
|
+
this.stationSn = stationSn;
|
|
1147
|
+
this.name = "DeviceChannelUnresolvedError";
|
|
1148
|
+
}
|
|
1149
|
+
};
|
|
1150
|
+
var RecordingDownloadError = class extends Error {
|
|
1151
|
+
reason;
|
|
1152
|
+
constructor(reason, message, options) {
|
|
1153
|
+
super(message, options);
|
|
1154
|
+
this.reason = reason;
|
|
1155
|
+
this.name = "RecordingDownloadError";
|
|
1156
|
+
}
|
|
1157
|
+
};
|
|
1158
|
+
var LiveStreamStartError = class extends Error {
|
|
1159
|
+
reason;
|
|
1160
|
+
stage;
|
|
1161
|
+
timeoutMs;
|
|
1162
|
+
attempts;
|
|
1163
|
+
constructor(failure) {
|
|
1164
|
+
super(`live stream failed to start (${failure.reason} at ${failure.stage} after ${failure.attempts} attempts; ${failure.timeoutMs}ms deadline)`, failure.cause === void 0 ? void 0 : { cause: failure.cause });
|
|
1165
|
+
this.name = "LiveStreamStartError";
|
|
1166
|
+
this.reason = failure.reason;
|
|
1167
|
+
this.stage = failure.stage;
|
|
1168
|
+
this.timeoutMs = failure.timeoutMs;
|
|
1169
|
+
this.attempts = failure.attempts;
|
|
1170
|
+
}
|
|
1171
|
+
};
|
|
1172
|
+
var StateConvergenceError = class extends Error {
|
|
1173
|
+
sn;
|
|
1174
|
+
property;
|
|
1175
|
+
param;
|
|
1176
|
+
expected;
|
|
1177
|
+
observed;
|
|
1178
|
+
timeoutMs;
|
|
1179
|
+
constructor(failure) {
|
|
1180
|
+
super(`device ${failure.sn} did not report ${failure.property} as ${String(failure.expected)} within ${failure.timeoutMs}ms (param ${failure.param} read ${String(failure.observed)})`);
|
|
1181
|
+
this.name = "StateConvergenceError";
|
|
1182
|
+
this.sn = failure.sn;
|
|
1183
|
+
this.property = failure.property;
|
|
1184
|
+
this.param = failure.param;
|
|
1185
|
+
this.expected = failure.expected;
|
|
1186
|
+
this.observed = failure.observed;
|
|
1187
|
+
this.timeoutMs = failure.timeoutMs;
|
|
1188
|
+
}
|
|
1189
|
+
};
|
|
1190
|
+
var COMMAND_OBSERVATION = /* @__PURE__ */ Symbol("command-observation");
|
|
1191
|
+
function observeCommand(command, observation) {
|
|
1192
|
+
return Object.defineProperty(command, COMMAND_OBSERVATION, { configurable: true, value: observation });
|
|
1193
|
+
}
|
|
1194
|
+
function commandObservation(command) {
|
|
1195
|
+
return command[COMMAND_OBSERVATION];
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// dist/core/raw-dp-writer.js
|
|
1199
|
+
var WIRE = { VARINT: 0, LENGTH_DELIMITED: 2 };
|
|
1200
|
+
function encodeVarint(value) {
|
|
1201
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
1202
|
+
throw new Error(`varint: ${value} is not a non-negative safe integer`);
|
|
1203
|
+
}
|
|
1204
|
+
const out = [];
|
|
1205
|
+
let v = value;
|
|
1206
|
+
while (v > 127) {
|
|
1207
|
+
out.push(v % 128 + 128);
|
|
1208
|
+
v = Math.floor(v / 128);
|
|
1209
|
+
}
|
|
1210
|
+
out.push(v);
|
|
1211
|
+
return out;
|
|
1212
|
+
}
|
|
1213
|
+
function zigzag(value) {
|
|
1214
|
+
if (!Number.isSafeInteger(value))
|
|
1215
|
+
throw new Error(`zigzag: ${value} is not a safe integer`);
|
|
1216
|
+
return value < 0 ? -2 * value - 1 : 2 * value;
|
|
1217
|
+
}
|
|
1218
|
+
var RawDpWriter = class _RawDpWriter {
|
|
1219
|
+
body = [];
|
|
1220
|
+
/** A varint-valued field (`int32`, `uint32`, `bool`, an enum member). */
|
|
1221
|
+
int(field, value) {
|
|
1222
|
+
this.body.push(...encodeVarint(tag(field, WIRE.VARINT)), ...encodeVarint(value));
|
|
1223
|
+
return this;
|
|
1224
|
+
}
|
|
1225
|
+
/** A signed varint field (`sint32`, `sint64`), ZigZag-encoded — see {@link zigzag}. */
|
|
1226
|
+
sint(field, value) {
|
|
1227
|
+
return this.int(field, zigzag(value));
|
|
1228
|
+
}
|
|
1229
|
+
/** A boolean field, on protobuf's own terms: `1` or `0` in a varint. */
|
|
1230
|
+
bool(field, value) {
|
|
1231
|
+
return this.int(field, value ? 1 : 0);
|
|
1232
|
+
}
|
|
1233
|
+
/** A length-delimited field carrying raw bytes — a string, a blob, or a message built elsewhere. */
|
|
1234
|
+
bytes(field, value) {
|
|
1235
|
+
const bytes2 = value instanceof Uint8Array ? Array.from(value) : [...value];
|
|
1236
|
+
this.body.push(...encodeVarint(tag(field, WIRE.LENGTH_DELIMITED)), ...encodeVarint(bytes2.length), ...bytes2);
|
|
1237
|
+
return this;
|
|
1238
|
+
}
|
|
1239
|
+
/**
|
|
1240
|
+
* A nested sub-message, built by `build` into a writer of its own.
|
|
1241
|
+
*
|
|
1242
|
+
* The sub-message is emitted even when `build` writes nothing into it. That is not an oversight: an
|
|
1243
|
+
* EMPTY sub-message is how these protocols say "this subsystem exists and is in its zero state", as
|
|
1244
|
+
* opposed to saying nothing about it, and the readers on the other side of this file depend on the
|
|
1245
|
+
* distinction. Omitting the call is what says nothing about it.
|
|
1246
|
+
*/
|
|
1247
|
+
sub(field, build) {
|
|
1248
|
+
const inner = new _RawDpWriter();
|
|
1249
|
+
build(inner);
|
|
1250
|
+
return this.bytes(field, inner.toBytes());
|
|
1251
|
+
}
|
|
1252
|
+
/** The message body on its own, unframed — for nesting into another writer. */
|
|
1253
|
+
toBytes() {
|
|
1254
|
+
return [...this.body];
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1257
|
+
* The finished DP value: `varint(bodyLength) ++ body`, base64-encoded.
|
|
1258
|
+
*
|
|
1259
|
+
* This is the envelope `RawDpCodec.decode` unwraps, so a value produced here and read back through the
|
|
1260
|
+
* codec is a genuine round-trip rather than two implementations of the same guess.
|
|
1261
|
+
*/
|
|
1262
|
+
finish() {
|
|
1263
|
+
return Buffer.from([...encodeVarint(this.body.length), ...this.body]).toString("base64");
|
|
1264
|
+
}
|
|
1265
|
+
};
|
|
1266
|
+
function tag(field, wire) {
|
|
1267
|
+
if (!Number.isSafeInteger(field) || field < 1)
|
|
1268
|
+
throw new Error(`field number: ${field} is not a positive integer`);
|
|
1269
|
+
return field * 8 + wire;
|
|
1270
|
+
}
|
|
1271
|
+
function rawDp(build) {
|
|
1272
|
+
const w = new RawDpWriter();
|
|
1273
|
+
build(w);
|
|
1274
|
+
return w.finish();
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
// dist/core/raw-dp-hex.js
|
|
1278
|
+
function hexToRawDp(hex) {
|
|
1279
|
+
if (hex.length === 0 || hex.length % 2 !== 0 || !/^[0-9a-fA-F]+$/.test(hex))
|
|
1280
|
+
return void 0;
|
|
1281
|
+
return Buffer.from(hex, "hex").toString("base64");
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
// dist/core/util.js
|
|
1285
|
+
var Timer = class {
|
|
1286
|
+
handle;
|
|
1287
|
+
/** (Re)arm to run `fn` after `ms`, cancelling any already-pending fire. */
|
|
1288
|
+
arm(ms, fn) {
|
|
1289
|
+
this.cancel();
|
|
1290
|
+
this.handle = setTimeout(() => {
|
|
1291
|
+
this.handle = void 0;
|
|
1292
|
+
fn();
|
|
1293
|
+
}, ms);
|
|
1294
|
+
this.handle.unref?.();
|
|
1295
|
+
}
|
|
1296
|
+
/** Cancel a pending fire. No-op if not armed. */
|
|
1297
|
+
cancel() {
|
|
1298
|
+
if (this.handle) {
|
|
1299
|
+
clearTimeout(this.handle);
|
|
1300
|
+
this.handle = void 0;
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
/** Whether a fire is currently scheduled. */
|
|
1304
|
+
get pending() {
|
|
1305
|
+
return this.handle !== void 0;
|
|
1306
|
+
}
|
|
1307
|
+
};
|
|
1308
|
+
function assertNever(value) {
|
|
1309
|
+
throw new Error(`unhandled variant: ${JSON.stringify(value)}`);
|
|
1310
|
+
}
|
|
1311
|
+
function asBool(value) {
|
|
1312
|
+
if (typeof value === "string")
|
|
1313
|
+
return value === "1" || value.toLowerCase() === "true";
|
|
1314
|
+
return value === true || value === 1;
|
|
1315
|
+
}
|
|
1316
|
+
function clamp(value, min, max) {
|
|
1317
|
+
if (Number.isNaN(value))
|
|
1318
|
+
return min;
|
|
1319
|
+
return Math.max(min, Math.min(max, Math.round(value)));
|
|
1320
|
+
}
|
|
1321
|
+
function structuralEqual(a, b) {
|
|
1322
|
+
if (Object.is(a, b))
|
|
1323
|
+
return true;
|
|
1324
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null)
|
|
1325
|
+
return false;
|
|
1326
|
+
const aArr = Array.isArray(a);
|
|
1327
|
+
const bArr = Array.isArray(b);
|
|
1328
|
+
if (aArr !== bArr)
|
|
1329
|
+
return false;
|
|
1330
|
+
if (aArr && bArr) {
|
|
1331
|
+
if (a.length !== b.length)
|
|
1332
|
+
return false;
|
|
1333
|
+
for (let i = 0; i < a.length; i++)
|
|
1334
|
+
if (!structuralEqual(a[i], b[i]))
|
|
1335
|
+
return false;
|
|
1336
|
+
return true;
|
|
1337
|
+
}
|
|
1338
|
+
const aObj = a;
|
|
1339
|
+
const bObj = b;
|
|
1340
|
+
const aKeys = Object.keys(aObj);
|
|
1341
|
+
const bKeys = Object.keys(bObj);
|
|
1342
|
+
if (aKeys.length !== bKeys.length)
|
|
1343
|
+
return false;
|
|
1344
|
+
for (const k of aKeys) {
|
|
1345
|
+
if (!Object.prototype.hasOwnProperty.call(bObj, k))
|
|
1346
|
+
return false;
|
|
1347
|
+
if (!structuralEqual(aObj[k], bObj[k]))
|
|
1348
|
+
return false;
|
|
1349
|
+
}
|
|
1350
|
+
return true;
|
|
1351
|
+
}
|
|
1352
|
+
function coerceEnumValue(enumObj, value) {
|
|
1353
|
+
const n = Number(value);
|
|
1354
|
+
return Number.isInteger(n) && Object.values(enumObj).includes(n) ? n : void 0;
|
|
1355
|
+
}
|
|
1356
|
+
function enumValues(enumObj) {
|
|
1357
|
+
return Object.values(enumObj).join("/");
|
|
1358
|
+
}
|
|
1359
|
+
function enumLabels(enumObj) {
|
|
1360
|
+
return Object.fromEntries(Object.entries(enumObj).map(([name, value]) => [value, name]));
|
|
1361
|
+
}
|
|
1362
|
+
function jsonObject(text2) {
|
|
1363
|
+
if (typeof text2 !== "string")
|
|
1364
|
+
return void 0;
|
|
1365
|
+
try {
|
|
1366
|
+
const v = JSON.parse(text2);
|
|
1367
|
+
return v && typeof v === "object" && !Array.isArray(v) ? v : void 0;
|
|
1368
|
+
} catch {
|
|
1369
|
+
return void 0;
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
function u16le(n) {
|
|
1373
|
+
const b = Buffer.alloc(2);
|
|
1374
|
+
b.writeUInt16LE(n & 65535);
|
|
1375
|
+
return b;
|
|
1376
|
+
}
|
|
1377
|
+
function u16be(n) {
|
|
1378
|
+
const b = Buffer.alloc(2);
|
|
1379
|
+
b.writeUInt16BE(n & 65535);
|
|
1380
|
+
return b;
|
|
1381
|
+
}
|
|
1382
|
+
function u32le(n) {
|
|
1383
|
+
const b = Buffer.alloc(4);
|
|
1384
|
+
b.writeUInt32LE(n >>> 0);
|
|
1385
|
+
return b;
|
|
1386
|
+
}
|
|
1387
|
+
function u32be(n) {
|
|
1388
|
+
const b = Buffer.alloc(4);
|
|
1389
|
+
b.writeUInt32BE(n >>> 0);
|
|
1390
|
+
return b;
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
// dist/core/lz4-block.js
|
|
1394
|
+
var MAX_OUTPUT = 64 * 1024 * 1024;
|
|
1395
|
+
function lz4BlockDecompress(block, size) {
|
|
1396
|
+
if (!Number.isSafeInteger(size) || size < 0 || size > MAX_OUTPUT)
|
|
1397
|
+
return void 0;
|
|
1398
|
+
const out = Buffer.alloc(size);
|
|
1399
|
+
const end = block.length;
|
|
1400
|
+
let pos = 0;
|
|
1401
|
+
let written = 0;
|
|
1402
|
+
const extend2 = (base) => {
|
|
1403
|
+
let len = base;
|
|
1404
|
+
if (base !== 15)
|
|
1405
|
+
return len;
|
|
1406
|
+
for (; ; ) {
|
|
1407
|
+
if (pos >= end)
|
|
1408
|
+
return void 0;
|
|
1409
|
+
const extra = block[pos++];
|
|
1410
|
+
len += extra;
|
|
1411
|
+
if (extra !== 255)
|
|
1412
|
+
return len;
|
|
1413
|
+
}
|
|
1414
|
+
};
|
|
1415
|
+
while (pos < end) {
|
|
1416
|
+
const token = block[pos++];
|
|
1417
|
+
const literals = extend2(token >> 4);
|
|
1418
|
+
if (literals === void 0)
|
|
1419
|
+
return void 0;
|
|
1420
|
+
if (pos + literals > end || written + literals > size)
|
|
1421
|
+
return void 0;
|
|
1422
|
+
for (let i = 0; i < literals; i++)
|
|
1423
|
+
out[written++] = block[pos++];
|
|
1424
|
+
if (pos === end)
|
|
1425
|
+
break;
|
|
1426
|
+
if (pos + 2 > end)
|
|
1427
|
+
return void 0;
|
|
1428
|
+
const offset = block[pos] | block[pos + 1] << 8;
|
|
1429
|
+
pos += 2;
|
|
1430
|
+
if (offset === 0 || offset > written)
|
|
1431
|
+
return void 0;
|
|
1432
|
+
const match = extend2(token & 15);
|
|
1433
|
+
if (match === void 0)
|
|
1434
|
+
return void 0;
|
|
1435
|
+
const length = match + 4;
|
|
1436
|
+
if (written + length > size)
|
|
1437
|
+
return void 0;
|
|
1438
|
+
let from = written - offset;
|
|
1439
|
+
for (let i = 0; i < length; i++)
|
|
1440
|
+
out[written++] = out[from++];
|
|
1441
|
+
}
|
|
1442
|
+
return written === size ? out : void 0;
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
// dist/transport/http/passport.js
|
|
1446
|
+
function loginCredentials(email, password, country) {
|
|
1447
|
+
const { clientPublicKeyHex, encryptedPassword } = encryptLoginPassword(password);
|
|
1448
|
+
return { email, password: encryptedPassword, ab: country, client_secret_info: { public_key: clientPublicKeyHex } };
|
|
1449
|
+
}
|
|
1450
|
+
function readLoginReply(data) {
|
|
1451
|
+
const userId = data.ap_cloud_user_id ?? data.user_id ?? data.userId;
|
|
1452
|
+
const authToken = data.auth_token ?? data.token;
|
|
1453
|
+
if (!userId || !authToken)
|
|
1454
|
+
return void 0;
|
|
1455
|
+
return {
|
|
1456
|
+
userId,
|
|
1457
|
+
accountUserId: data.user_id ?? data.userId,
|
|
1458
|
+
authToken,
|
|
1459
|
+
geoKey: data.geo_key,
|
|
1460
|
+
tokenExpiresAt: Number(data.token_expires_at ?? 0) || 0,
|
|
1461
|
+
twoFactorPending: !!(data.fa_info ?? {}).info
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
function signedHeaders(entry, encBody) {
|
|
1465
|
+
const ts = nowSec();
|
|
1466
|
+
const once = genId();
|
|
1467
|
+
return {
|
|
1468
|
+
"x-encryption-info": "algo_ecdh",
|
|
1469
|
+
"x-key-ident": entry.keyIdent,
|
|
1470
|
+
"x-request-ts": ts,
|
|
1471
|
+
"x-request-once": once,
|
|
1472
|
+
"x-signature": signRequest(entry.shareKey, ts, once, encBody)
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1073
1476
|
// dist/transport/http/mega-client.js
|
|
1074
1477
|
function parseMaybeJson(text2) {
|
|
1075
1478
|
if (text2.length === 0)
|
|
@@ -1437,17 +1840,11 @@ ${this.auth_?.authToken ?? ""}`;
|
|
|
1437
1840
|
const order = preferred === "text/plain" ? ["text/plain", "application/json"] : ["application/json", "text/plain"];
|
|
1438
1841
|
let last;
|
|
1439
1842
|
for (const contentType of order) {
|
|
1440
|
-
const ts = nowSec();
|
|
1441
|
-
const once = genId();
|
|
1442
1843
|
const headers = {
|
|
1443
1844
|
...this.baseHeaders(),
|
|
1444
1845
|
"content-type": contentType,
|
|
1445
|
-
|
|
1846
|
+
...signedHeaders(entry, encBody),
|
|
1446
1847
|
"x-replay-info": "replay",
|
|
1447
|
-
"x-key-ident": entry.keyIdent,
|
|
1448
|
-
"x-request-ts": ts,
|
|
1449
|
-
"x-request-once": once,
|
|
1450
|
-
"x-signature": signRequest(entry.shareKey, ts, once, encBody),
|
|
1451
1848
|
// Per-call header overrides (e.g. app-name=eufy_security to request a security-scoped
|
|
1452
1849
|
// MQTT cert on an existing eufy_mega session — see EufyMega.getUserMqttInfo).
|
|
1453
1850
|
...headerOverrides
|
|
@@ -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();
|
|
@@ -2125,6 +2522,10 @@ function solixUserTopics(appName, userId) {
|
|
|
2125
2522
|
|
|
2126
2523
|
// dist/transport/mqtt/secure-mqtt.js
|
|
2127
2524
|
var SUBACK_FAILURE = 128;
|
|
2525
|
+
function isRefusal(err) {
|
|
2526
|
+
const granted = err?.packet?.granted;
|
|
2527
|
+
return Array.isArray(granted) && granted.some((g) => typeof g === "number" && (g & SUBACK_FAILURE) !== 0);
|
|
2528
|
+
}
|
|
2128
2529
|
function isNotAuthorized(err) {
|
|
2129
2530
|
return err instanceof Error && /connection refused/i.test(err.message) && /not authori[sz]ed/i.test(err.message);
|
|
2130
2531
|
}
|
|
@@ -2227,19 +2628,16 @@ var SecureMqtt = class extends EventEmitter {
|
|
|
2227
2628
|
}
|
|
2228
2629
|
/**
|
|
2229
2630
|
* Subscribe every inbound leg this device's line uses (see `topics.ts` — one `/res` for most lines,
|
|
2230
|
-
* four topics for `eufy_life`).
|
|
2631
|
+
* four topics for `eufy_life` and the clean line).
|
|
2231
2632
|
*
|
|
2232
|
-
* The grants are INSPECTED, not assumed: AWS IoT
|
|
2233
|
-
* SUBACK_FAILURE
|
|
2234
|
-
*
|
|
2235
|
-
*
|
|
2236
|
-
* line that grants its state channel but refuses (say) the OTA leg still works.
|
|
2633
|
+
* The grants are INSPECTED, not assumed: AWS IoT refuses a policy-denied filter with a
|
|
2634
|
+
* {@link SUBACK_FAILURE} grant rather than failing the connection. A denied topic is reported via
|
|
2635
|
+
* `error` naming the credential scope; only an all-denied device throws, so a line that grants its
|
|
2636
|
+
* state channel but refuses (say) a business leg still works.
|
|
2237
2637
|
*/
|
|
2238
2638
|
async subscribeDevice(device) {
|
|
2239
|
-
if (!this.client)
|
|
2240
|
-
throw new Error("SecureMqtt not connected");
|
|
2241
2639
|
const topics = [...subscribeTopics(device)];
|
|
2242
|
-
const { denied } =
|
|
2640
|
+
const { denied } = await this.subscribeEach(topics);
|
|
2243
2641
|
const scope = this.o.credentials.app_name ?? "default";
|
|
2244
2642
|
if (denied.length === topics.length) {
|
|
2245
2643
|
throw new Error(`subscribe ${device.sn}: every topic denied on credential scope "${scope}" \u2014 this line's topics are granted to a different scope (${denied.join(", ")})`);
|
|
@@ -2250,25 +2648,43 @@ var SecureMqtt = class extends EventEmitter {
|
|
|
2250
2648
|
}
|
|
2251
2649
|
/**
|
|
2252
2650
|
* Subscribe to explicit topic filters, returning the topics that were granted. A scope-denied filter
|
|
2253
|
-
*
|
|
2254
|
-
*
|
|
2255
|
-
*
|
|
2651
|
+
* is dropped from the result instead of throwing — callers that need every leg check the returned
|
|
2652
|
+
* list. Used by lines whose topic vocabulary isn't the eufy `subscribeTopics` shape (e.g. Anker Solix
|
|
2653
|
+
* `dt/{app}/{pn}/{sn}`).
|
|
2256
2654
|
*/
|
|
2257
2655
|
async subscribe(topics) {
|
|
2258
|
-
|
|
2259
|
-
throw new Error("SecureMqtt not connected");
|
|
2260
|
-
return this.partitionGrants(await this.client.subscribeAsync(topics, { qos: 1 })).granted;
|
|
2656
|
+
return (await this.subscribeEach(topics)).granted;
|
|
2261
2657
|
}
|
|
2262
2658
|
/**
|
|
2263
|
-
*
|
|
2264
|
-
*
|
|
2265
|
-
*
|
|
2659
|
+
* Subscribe each filter in a SUBSCRIBE of its own and split the outcome into granted vs refused.
|
|
2660
|
+
*
|
|
2661
|
+
* One request per filter because the MQTT engine treats a SUBACK as all-or-nothing: one refused grant
|
|
2662
|
+
* rejects the whole request, and it forgets EVERY filter of that request for resubscription after a
|
|
2663
|
+
* reconnect — so a batch holding one denied leg would lose the granted legs on the next drop. Alone,
|
|
2664
|
+
* a refused filter costs only itself. A rejection that carries no SUBACK is a transport failure and
|
|
2665
|
+
* is thrown.
|
|
2666
|
+
*
|
|
2667
|
+
* The cost is one SUBSCRIBE and one SUBACK per filter instead of one per device — four for a
|
|
2668
|
+
* four-leg line. They are sent concurrently, so the wall-clock cost is one round trip. Folding them
|
|
2669
|
+
* back into one request brings back the lost resubscription.
|
|
2266
2670
|
*/
|
|
2267
|
-
|
|
2671
|
+
async subscribeEach(topics) {
|
|
2672
|
+
const client = this.client;
|
|
2673
|
+
if (!client)
|
|
2674
|
+
throw new Error("SecureMqtt not connected");
|
|
2675
|
+
const outcomes = await Promise.allSettled(topics.map((topic) => client.subscribeAsync(topic, { qos: 1 })));
|
|
2268
2676
|
const granted = [];
|
|
2269
2677
|
const denied = [];
|
|
2270
|
-
|
|
2271
|
-
|
|
2678
|
+
outcomes.forEach((outcome, i) => {
|
|
2679
|
+
const topic = topics[i];
|
|
2680
|
+
if (outcome.status === "fulfilled") {
|
|
2681
|
+
granted.push(topic);
|
|
2682
|
+
return;
|
|
2683
|
+
}
|
|
2684
|
+
if (!isRefusal(outcome.reason))
|
|
2685
|
+
throw outcome.reason;
|
|
2686
|
+
denied.push(topic);
|
|
2687
|
+
});
|
|
2272
2688
|
return { granted, denied };
|
|
2273
2689
|
}
|
|
2274
2690
|
/**
|
|
@@ -2283,134 +2699,25 @@ var SecureMqtt = class extends EventEmitter {
|
|
|
2283
2699
|
* don't share subscribe/publish routing).
|
|
2284
2700
|
*/
|
|
2285
2701
|
async publish(topic, body, opts = {}) {
|
|
2286
|
-
if (!this.client)
|
|
2287
|
-
throw new Error("SecureMqtt not connected");
|
|
2288
|
-
await this.client.publishAsync(topic, body, { qos: opts.qos ?? 1 });
|
|
2289
|
-
}
|
|
2290
|
-
async disconnect() {
|
|
2291
|
-
this.connecting = void 0;
|
|
2292
|
-
await this.client?.endAsync(true);
|
|
2293
|
-
this.client = void 0;
|
|
2294
|
-
}
|
|
2295
|
-
};
|
|
2296
|
-
|
|
2297
|
-
// dist/transport/mqtt/app-client-id.js
|
|
2298
|
-
import { createHash as createHash4 } from "node:crypto";
|
|
2299
|
-
function buildAppShapedClientId(input) {
|
|
2300
|
-
const ts = input.timestamp ?? Math.floor(Date.now() / 1e3);
|
|
2301
|
-
return `android-${input.appName}-${input.uid}-${input.mqttUuid}-${ts}`;
|
|
2302
|
-
}
|
|
2303
|
-
function mqttUuidFrom(installId) {
|
|
2304
|
-
return createHash4("sha256").update(installId).digest("hex").slice(0, 16);
|
|
2305
|
-
}
|
|
2306
|
-
|
|
2307
|
-
// dist/core/util.js
|
|
2308
|
-
var Timer = class {
|
|
2309
|
-
handle;
|
|
2310
|
-
/** (Re)arm to run `fn` after `ms`, cancelling any already-pending fire. */
|
|
2311
|
-
arm(ms, fn) {
|
|
2312
|
-
this.cancel();
|
|
2313
|
-
this.handle = setTimeout(() => {
|
|
2314
|
-
this.handle = void 0;
|
|
2315
|
-
fn();
|
|
2316
|
-
}, ms);
|
|
2317
|
-
this.handle.unref?.();
|
|
2318
|
-
}
|
|
2319
|
-
/** Cancel a pending fire. No-op if not armed. */
|
|
2320
|
-
cancel() {
|
|
2321
|
-
if (this.handle) {
|
|
2322
|
-
clearTimeout(this.handle);
|
|
2323
|
-
this.handle = void 0;
|
|
2324
|
-
}
|
|
2325
|
-
}
|
|
2326
|
-
/** Whether a fire is currently scheduled. */
|
|
2327
|
-
get pending() {
|
|
2328
|
-
return this.handle !== void 0;
|
|
2329
|
-
}
|
|
2330
|
-
};
|
|
2331
|
-
function assertNever(value) {
|
|
2332
|
-
throw new Error(`unhandled variant: ${JSON.stringify(value)}`);
|
|
2333
|
-
}
|
|
2334
|
-
function asBool(value) {
|
|
2335
|
-
if (typeof value === "string")
|
|
2336
|
-
return value === "1" || value.toLowerCase() === "true";
|
|
2337
|
-
return value === true || value === 1;
|
|
2338
|
-
}
|
|
2339
|
-
function clamp(value, min, max) {
|
|
2340
|
-
if (Number.isNaN(value))
|
|
2341
|
-
return min;
|
|
2342
|
-
return Math.max(min, Math.min(max, Math.round(value)));
|
|
2343
|
-
}
|
|
2344
|
-
function structuralEqual(a, b) {
|
|
2345
|
-
if (Object.is(a, b))
|
|
2346
|
-
return true;
|
|
2347
|
-
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null)
|
|
2348
|
-
return false;
|
|
2349
|
-
const aArr = Array.isArray(a);
|
|
2350
|
-
const bArr = Array.isArray(b);
|
|
2351
|
-
if (aArr !== bArr)
|
|
2352
|
-
return false;
|
|
2353
|
-
if (aArr && bArr) {
|
|
2354
|
-
if (a.length !== b.length)
|
|
2355
|
-
return false;
|
|
2356
|
-
for (let i = 0; i < a.length; i++)
|
|
2357
|
-
if (!structuralEqual(a[i], b[i]))
|
|
2358
|
-
return false;
|
|
2359
|
-
return true;
|
|
2360
|
-
}
|
|
2361
|
-
const aObj = a;
|
|
2362
|
-
const bObj = b;
|
|
2363
|
-
const aKeys = Object.keys(aObj);
|
|
2364
|
-
const bKeys = Object.keys(bObj);
|
|
2365
|
-
if (aKeys.length !== bKeys.length)
|
|
2366
|
-
return false;
|
|
2367
|
-
for (const k of aKeys) {
|
|
2368
|
-
if (!Object.prototype.hasOwnProperty.call(bObj, k))
|
|
2369
|
-
return false;
|
|
2370
|
-
if (!structuralEqual(aObj[k], bObj[k]))
|
|
2371
|
-
return false;
|
|
2372
|
-
}
|
|
2373
|
-
return true;
|
|
2374
|
-
}
|
|
2375
|
-
function coerceEnumValue(enumObj, value) {
|
|
2376
|
-
const n = Number(value);
|
|
2377
|
-
return Number.isInteger(n) && Object.values(enumObj).includes(n) ? n : void 0;
|
|
2378
|
-
}
|
|
2379
|
-
function enumValues(enumObj) {
|
|
2380
|
-
return Object.values(enumObj).join("/");
|
|
2381
|
-
}
|
|
2382
|
-
function enumLabels(enumObj) {
|
|
2383
|
-
return Object.fromEntries(Object.entries(enumObj).map(([name, value]) => [value, name]));
|
|
2384
|
-
}
|
|
2385
|
-
function jsonObject(text2) {
|
|
2386
|
-
if (typeof text2 !== "string")
|
|
2387
|
-
return void 0;
|
|
2388
|
-
try {
|
|
2389
|
-
const v = JSON.parse(text2);
|
|
2390
|
-
return v && typeof v === "object" && !Array.isArray(v) ? v : void 0;
|
|
2391
|
-
} catch {
|
|
2392
|
-
return void 0;
|
|
2393
|
-
}
|
|
2394
|
-
}
|
|
2395
|
-
function u16le(n) {
|
|
2396
|
-
const b = Buffer.alloc(2);
|
|
2397
|
-
b.writeUInt16LE(n & 65535);
|
|
2398
|
-
return b;
|
|
2399
|
-
}
|
|
2400
|
-
function u16be(n) {
|
|
2401
|
-
const b = Buffer.alloc(2);
|
|
2402
|
-
b.writeUInt16BE(n & 65535);
|
|
2403
|
-
return b;
|
|
2404
|
-
}
|
|
2405
|
-
function u32le(n) {
|
|
2406
|
-
const b = Buffer.alloc(4);
|
|
2407
|
-
b.writeUInt32LE(n >>> 0);
|
|
2408
|
-
return b;
|
|
2702
|
+
if (!this.client)
|
|
2703
|
+
throw new Error("SecureMqtt not connected");
|
|
2704
|
+
await this.client.publishAsync(topic, body, { qos: opts.qos ?? 1 });
|
|
2705
|
+
}
|
|
2706
|
+
async disconnect() {
|
|
2707
|
+
this.connecting = void 0;
|
|
2708
|
+
await this.client?.endAsync(true);
|
|
2709
|
+
this.client = void 0;
|
|
2710
|
+
}
|
|
2711
|
+
};
|
|
2712
|
+
|
|
2713
|
+
// dist/transport/mqtt/app-client-id.js
|
|
2714
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
2715
|
+
function buildAppShapedClientId(input) {
|
|
2716
|
+
const ts = input.timestamp ?? Math.floor(Date.now() / 1e3);
|
|
2717
|
+
return `android-${input.appName}-${input.uid}-${input.mqttUuid}-${ts}`;
|
|
2409
2718
|
}
|
|
2410
|
-
function
|
|
2411
|
-
|
|
2412
|
-
b.writeUInt32BE(n >>> 0);
|
|
2413
|
-
return b;
|
|
2719
|
+
function mqttUuidFrom(installId) {
|
|
2720
|
+
return createHash4("sha256").update(installId).digest("hex").slice(0, 16);
|
|
2414
2721
|
}
|
|
2415
2722
|
|
|
2416
2723
|
// dist/transport/mqtt/availability.js
|
|
@@ -2537,13 +2844,6 @@ function parseAiotDpReport(raw) {
|
|
|
2537
2844
|
return Object.keys(out).length ? out : void 0;
|
|
2538
2845
|
}
|
|
2539
2846
|
|
|
2540
|
-
// dist/core/raw-dp-hex.js
|
|
2541
|
-
function hexToRawDp(hex) {
|
|
2542
|
-
if (hex.length === 0 || hex.length % 2 !== 0 || !/^[0-9a-fA-F]+$/.test(hex))
|
|
2543
|
-
return void 0;
|
|
2544
|
-
return Buffer.from(hex, "hex").toString("base64");
|
|
2545
|
-
}
|
|
2546
|
-
|
|
2547
2847
|
// dist/transport/mqtt/biz-stream.js
|
|
2548
2848
|
var BIZ_PROTOCOL = {
|
|
2549
2849
|
/** Map-stream data: the frames this module parses. */
|
|
@@ -2727,18 +3027,6 @@ var DeviceType = {
|
|
|
2727
3027
|
CAMERA_C35: 10035
|
|
2728
3028
|
};
|
|
2729
3029
|
|
|
2730
|
-
// dist/core/types.js
|
|
2731
|
-
var PRINTER_CATEGORY_RE = /ankermake|eufymake|fdm|3d_?print/i;
|
|
2732
|
-
function classifyDevice(raw) {
|
|
2733
|
-
const category = raw.category ?? "";
|
|
2734
|
-
const hasP2p = typeof raw.p2p_did === "string" && raw.p2p_did.length > 0;
|
|
2735
|
-
if (category === "eufy_security" || hasP2p)
|
|
2736
|
-
return { category, api: "mega", realtime: "p2p" };
|
|
2737
|
-
if (PRINTER_CATEGORY_RE.test(category))
|
|
2738
|
-
return { category, api: "ankermake", realtime: "ankermake-mqtt" };
|
|
2739
|
-
return { category, api: "mega", realtime: "smqtt" };
|
|
2740
|
-
}
|
|
2741
|
-
|
|
2742
3030
|
// dist/model/classify.js
|
|
2743
3031
|
var STATION_TYPES = /* @__PURE__ */ new Set([
|
|
2744
3032
|
DeviceType.STATION,
|
|
@@ -2877,111 +3165,6 @@ function inferName(rec) {
|
|
|
2877
3165
|
return void 0;
|
|
2878
3166
|
}
|
|
2879
3167
|
|
|
2880
|
-
// dist/core/contracts.js
|
|
2881
|
-
var StoredSnapshotUnavailableError = class extends Error {
|
|
2882
|
-
reason;
|
|
2883
|
-
constructor(reason, message, options) {
|
|
2884
|
-
super(message, options);
|
|
2885
|
-
this.reason = reason;
|
|
2886
|
-
this.name = "StoredSnapshotUnavailableError";
|
|
2887
|
-
}
|
|
2888
|
-
};
|
|
2889
|
-
var RETRYABLE_LIVE_SNAPSHOT_REASONS = [
|
|
2890
|
-
"no-keyframe",
|
|
2891
|
-
"source-failed",
|
|
2892
|
-
"undecodable-burst"
|
|
2893
|
-
];
|
|
2894
|
-
var LiveSnapshotUnavailableError = class extends Error {
|
|
2895
|
-
reason;
|
|
2896
|
-
/** Whether another attempt could plausibly succeed without the host changing anything. */
|
|
2897
|
-
retryable;
|
|
2898
|
-
constructor(reason, message, options) {
|
|
2899
|
-
super(message, options);
|
|
2900
|
-
this.reason = reason;
|
|
2901
|
-
this.name = "LiveSnapshotUnavailableError";
|
|
2902
|
-
this.retryable = RETRYABLE_LIVE_SNAPSHOT_REASONS.includes(reason);
|
|
2903
|
-
}
|
|
2904
|
-
};
|
|
2905
|
-
var CameraDisabledError = class extends Error {
|
|
2906
|
-
camera;
|
|
2907
|
-
constructor(camera, options) {
|
|
2908
|
-
super(`camera${camera ? ` ${camera}` : ""} is disabled \u2014 no live stream possible`, options);
|
|
2909
|
-
this.camera = camera;
|
|
2910
|
-
this.name = "CameraDisabledError";
|
|
2911
|
-
}
|
|
2912
|
-
};
|
|
2913
|
-
var StationKeyUnavailableError = class extends Error {
|
|
2914
|
-
stationSn;
|
|
2915
|
-
/** Always true: the negotiation is per connection, so a later one may still produce a key. */
|
|
2916
|
-
retryable = true;
|
|
2917
|
-
constructor(stationSn, options) {
|
|
2918
|
-
super(`station ${stationSn} did not provide its session key, so nothing that requires one could be sent`, options);
|
|
2919
|
-
this.stationSn = stationSn;
|
|
2920
|
-
this.name = "StationKeyUnavailableError";
|
|
2921
|
-
}
|
|
2922
|
-
};
|
|
2923
|
-
var StationUnreachableError = class extends Error {
|
|
2924
|
-
stationSn;
|
|
2925
|
-
waitedMs;
|
|
2926
|
-
/** Always true: a station unreachable now may answer on a later attempt. */
|
|
2927
|
-
retryable = true;
|
|
2928
|
-
constructor(stationSn, waitedMs, options) {
|
|
2929
|
-
super(`station ${stationSn}'s P2P session did not connect within ${waitedMs}ms, so nothing could be sent to it`, options);
|
|
2930
|
-
this.stationSn = stationSn;
|
|
2931
|
-
this.waitedMs = waitedMs;
|
|
2932
|
-
this.name = "StationUnreachableError";
|
|
2933
|
-
}
|
|
2934
|
-
};
|
|
2935
|
-
var DeviceChannelUnresolvedError = class extends Error {
|
|
2936
|
-
sn;
|
|
2937
|
-
stationSn;
|
|
2938
|
-
constructor(sn, stationSn, options) {
|
|
2939
|
-
super(`${sn} has no usable channel on station ${stationSn}, so nothing was sent to it`, options);
|
|
2940
|
-
this.sn = sn;
|
|
2941
|
-
this.stationSn = stationSn;
|
|
2942
|
-
this.name = "DeviceChannelUnresolvedError";
|
|
2943
|
-
}
|
|
2944
|
-
};
|
|
2945
|
-
var LiveStreamStartError = class extends Error {
|
|
2946
|
-
reason;
|
|
2947
|
-
stage;
|
|
2948
|
-
timeoutMs;
|
|
2949
|
-
attempts;
|
|
2950
|
-
constructor(failure) {
|
|
2951
|
-
super(`live stream failed to start (${failure.reason} at ${failure.stage} after ${failure.attempts} attempts; ${failure.timeoutMs}ms deadline)`, failure.cause === void 0 ? void 0 : { cause: failure.cause });
|
|
2952
|
-
this.name = "LiveStreamStartError";
|
|
2953
|
-
this.reason = failure.reason;
|
|
2954
|
-
this.stage = failure.stage;
|
|
2955
|
-
this.timeoutMs = failure.timeoutMs;
|
|
2956
|
-
this.attempts = failure.attempts;
|
|
2957
|
-
}
|
|
2958
|
-
};
|
|
2959
|
-
var StateConvergenceError = class extends Error {
|
|
2960
|
-
sn;
|
|
2961
|
-
property;
|
|
2962
|
-
param;
|
|
2963
|
-
expected;
|
|
2964
|
-
observed;
|
|
2965
|
-
timeoutMs;
|
|
2966
|
-
constructor(failure) {
|
|
2967
|
-
super(`device ${failure.sn} did not report ${failure.property} as ${String(failure.expected)} within ${failure.timeoutMs}ms (param ${failure.param} read ${String(failure.observed)})`);
|
|
2968
|
-
this.name = "StateConvergenceError";
|
|
2969
|
-
this.sn = failure.sn;
|
|
2970
|
-
this.property = failure.property;
|
|
2971
|
-
this.param = failure.param;
|
|
2972
|
-
this.expected = failure.expected;
|
|
2973
|
-
this.observed = failure.observed;
|
|
2974
|
-
this.timeoutMs = failure.timeoutMs;
|
|
2975
|
-
}
|
|
2976
|
-
};
|
|
2977
|
-
var COMMAND_OBSERVATION = /* @__PURE__ */ Symbol("command-observation");
|
|
2978
|
-
function observeCommand(command, observation) {
|
|
2979
|
-
return Object.defineProperty(command, COMMAND_OBSERVATION, { configurable: true, value: observation });
|
|
2980
|
-
}
|
|
2981
|
-
function commandObservation(command) {
|
|
2982
|
-
return command[COMMAND_OBSERVATION];
|
|
2983
|
-
}
|
|
2984
|
-
|
|
2985
3168
|
// dist/model/capabilities/access.js
|
|
2986
3169
|
function setScalar(param, value, ctx, form = "auto") {
|
|
2987
3170
|
return { kind: "set-param", param, value, form, channel: ctx.channel };
|
|
@@ -3254,11 +3437,17 @@ var unreflectedMembers = (surface) => statement(surface, UNREFLECTED);
|
|
|
3254
3437
|
// dist/model/capabilities/manifest.js
|
|
3255
3438
|
function describeBound(modules, bound, ctx) {
|
|
3256
3439
|
const out = [];
|
|
3440
|
+
const anyBound = Object.values(bound).some((obj) => obj && typeof obj === "object");
|
|
3257
3441
|
for (const m of modules) {
|
|
3258
3442
|
const accessor = camelCase(m.capability);
|
|
3259
3443
|
const obj = bound[accessor];
|
|
3260
|
-
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
|
+
}
|
|
3261
3449
|
continue;
|
|
3450
|
+
}
|
|
3262
3451
|
const descriptors = Object.getOwnPropertyDescriptors(obj);
|
|
3263
3452
|
const reads2 = [];
|
|
3264
3453
|
const actions = [];
|
|
@@ -3781,7 +3970,6 @@ var MOTION_CMD = {
|
|
|
3781
3970
|
* payload key AND pass mChannel 0 explicitly; the two are separate choices, not linked.)
|
|
3782
3971
|
*
|
|
3783
3972
|
* ⚠️ Replay + readback confirmed on a HomeBase-attached T8425 (1719 `0`→`1`→`0`), NOT byte-captured.
|
|
3784
|
-
* Provenance is `apk`, not `verified`: a divergent-but-also-accepted frame can't be ruled out.
|
|
3785
3973
|
*/
|
|
3786
3974
|
HUMAN_ONLY_AT_NIGHT: 1719,
|
|
3787
3975
|
/**
|
|
@@ -3795,7 +3983,7 @@ var MOTION_CMD = {
|
|
|
3795
3983
|
* form, so a feature that is on reads as off.
|
|
3796
3984
|
* Decoded by {@link decodeRadarWdSwitch} to match the app.
|
|
3797
3985
|
*
|
|
3798
|
-
* ⚠️ 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.
|
|
3799
3987
|
*/
|
|
3800
3988
|
LOITERING_DETECTION: 2706,
|
|
3801
3989
|
/**
|
|
@@ -4119,7 +4307,7 @@ var MOTION_MEMBERS = {
|
|
|
4119
4307
|
param: MOTION_CMD.HUMAN_ONLY_AT_NIGHT,
|
|
4120
4308
|
type: "bool",
|
|
4121
4309
|
kind: "boolean",
|
|
4122
|
-
provenance: "
|
|
4310
|
+
provenance: "verified",
|
|
4123
4311
|
description: "Restrict AI classification to night-time only (1719). \u26A0\uFE0F Replay + readback confirmed on a HomeBase-attached T8425, not byte-captured.",
|
|
4124
4312
|
requires: [MOTION_CMD.HUMAN_ONLY_AT_NIGHT],
|
|
4125
4313
|
write: (v, ctx) => {
|
|
@@ -4135,7 +4323,7 @@ var MOTION_MEMBERS = {
|
|
|
4135
4323
|
param: MOTION_CMD.LOITERING_DETECTION,
|
|
4136
4324
|
type: "bool",
|
|
4137
4325
|
kind: "boolean",
|
|
4138
|
-
provenance: "
|
|
4326
|
+
provenance: "verified",
|
|
4139
4327
|
coerce: (raw) => decodeRadarWdSwitch(raw) ?? false,
|
|
4140
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.",
|
|
4141
4329
|
requires: [MOTION_CMD.LOITERING_DETECTION],
|
|
@@ -4241,6 +4429,7 @@ var MOTION = {
|
|
|
4241
4429
|
};
|
|
4242
4430
|
|
|
4243
4431
|
// dist/model/capabilities/person-detection.js
|
|
4432
|
+
var IDENTIFIED_PERSON_CLAIM = { homeBaseAttached: true };
|
|
4244
4433
|
var PERSON_DETECTION = {
|
|
4245
4434
|
capability: "person_detection",
|
|
4246
4435
|
description: "AI human detection enable switch and live detected state.",
|
|
@@ -4257,8 +4446,18 @@ var PERSON_DETECTION = {
|
|
|
4257
4446
|
*/
|
|
4258
4447
|
events: [
|
|
4259
4448
|
{ source: "push", match: DoorbellPushEvent.FACE_DETECTION, emit: "personDetected" },
|
|
4260
|
-
{
|
|
4261
|
-
|
|
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
|
+
}
|
|
4262
4461
|
]
|
|
4263
4462
|
};
|
|
4264
4463
|
|
|
@@ -4326,7 +4525,7 @@ function decodePowerSource(raw) {
|
|
|
4326
4525
|
function recordSetting(param, value, ctx) {
|
|
4327
4526
|
return setStationScalar(param, value, ctx.channel);
|
|
4328
4527
|
}
|
|
4329
|
-
var MAINS_CAMERA_MODELS = ["T8425", "T8419", "T8410", "T84A1"];
|
|
4528
|
+
var MAINS_CAMERA_MODELS = ["T8425", "T8419", "T8410", "T84A1", "T8423"];
|
|
4330
4529
|
function cameraPowerTier(model, capabilities) {
|
|
4331
4530
|
return capabilities.has("battery") && notMainsCamera({ model }) ? "battery" : "wired";
|
|
4332
4531
|
}
|
|
@@ -5536,6 +5735,7 @@ var CAMERA_MEMBERS = {
|
|
|
5536
5735
|
refuseWhenDisabled(ctx, read);
|
|
5537
5736
|
return m.recordFragments({ powered: poweredOf(ctx), ...opts });
|
|
5538
5737
|
}), "Continuous fragmented-MP4 (CMAF) recording."),
|
|
5738
|
+
downloadRecording: provided("media", (m) => m.downloadRecording && ((opts) => m.downloadRecording(opts)), "Download a recording the station holds \u2192 H.264 and AAC elementary streams."),
|
|
5539
5739
|
/**
|
|
5540
5740
|
* Push audio from the host to this camera's speaker. Gated on the **speaker** param specifically —
|
|
5541
5741
|
* not the `audio` capability, which resolves on a microphone alone and would advertise a speaker the
|
|
@@ -5577,7 +5777,7 @@ var SIREN_CMD = {
|
|
|
5577
5777
|
ALARM_TEST: 1826,
|
|
5578
5778
|
/** Manually stop a sounding alarm (app `APP_CMD_SIREN_SENSOR_MANUAL_STOP_ALARM`). */
|
|
5579
5779
|
MANUAL_STOP: 1871,
|
|
5580
|
-
/** Station-family HomeBase duration alarm, verified live on T8010 (app `SET_TONE_FILE`). */
|
|
5780
|
+
/** Station-family HomeBase duration alarm, verified live on T8010 and T8030 (app `SET_TONE_FILE`). */
|
|
5581
5781
|
HOMEBASE_TONE: 1201,
|
|
5582
5782
|
/** HomeBase alarm volume percentage (app `CMD_SET_HUB_SPK_VOLUME`). */
|
|
5583
5783
|
HUB_SPK_VOLUME: 1235,
|
|
@@ -5592,7 +5792,7 @@ var SirenAlarmDuration = { Min1: 60, Min5: 300, Min10: 600, Min15: 900 };
|
|
|
5592
5792
|
var HubAlarmTone = { Tone1: 1, Tone2: 2 };
|
|
5593
5793
|
var HUB_ALARM_EVIDENCE = [SIREN_CMD.HUB_ALARM_TONE, 1282];
|
|
5594
5794
|
function isStationAlarmOutput(ctx) {
|
|
5595
|
-
return ctx.deviceType === DeviceType.STATION;
|
|
5795
|
+
return ctx.deviceType === DeviceType.STATION || ctx.deviceType === DeviceType.HB3;
|
|
5596
5796
|
}
|
|
5597
5797
|
function isEvidencedStationAlarm(ctx) {
|
|
5598
5798
|
return isStationAlarmOutput(ctx) && HUB_ALARM_EVIDENCE.some((param) => ctx.paramIds.has(param));
|
|
@@ -6714,9 +6914,9 @@ function parseQuickResponses(voiceList) {
|
|
|
6714
6914
|
var DOORBELL_MEMBERS = {
|
|
6715
6915
|
/**
|
|
6716
6916
|
* The HOMEBASE as the doorbell's chime — the hub plays the ring, not the wired chime box
|
|
6717
|
-
* `mechanicalChimeSwitch` drives. `provenance` is "verified" on our own decrypt of the app's frame
|
|
6718
|
-
*
|
|
6719
|
-
*
|
|
6917
|
+
* `mechanicalChimeSwitch` drives. `provenance` is "verified" on our own decrypt of the app's frame:
|
|
6918
|
+
* direct-binary `[ch][value][acct]`, 1=on/0=off, the same shape as its 1703 sibling — captured, not
|
|
6919
|
+
* inferred from the shared param range.
|
|
6720
6920
|
*/
|
|
6721
6921
|
chimeSwitch: {
|
|
6722
6922
|
param: DOORBELL_CMD.CHIME_SWITCH,
|
|
@@ -6727,9 +6927,8 @@ var DOORBELL_MEMBERS = {
|
|
|
6727
6927
|
write: (v, ctx) => setScalar(DOORBELL_CMD.CHIME_SWITCH, asBool(v) ? 1 : 0, ctx, "direct-binary")
|
|
6728
6928
|
},
|
|
6729
6929
|
/**
|
|
6730
|
-
* `provenance` is "verified"
|
|
6731
|
-
*
|
|
6732
|
-
* `[ch][value][acct]`, 1=on/0=off — verified live on a T8214 (ON then OFF).
|
|
6930
|
+
* `provenance` is "verified" on our own P2P decrypt of the write wire: direct-binary
|
|
6931
|
+
* `[ch][value][acct]`, 1=on/0=off, verified live on a T8214 (ON then OFF).
|
|
6733
6932
|
*/
|
|
6734
6933
|
mechanicalChimeSwitch: {
|
|
6735
6934
|
param: DOORBELL_CMD.MECHANICAL_CHIME_SWITCH,
|
|
@@ -6760,7 +6959,7 @@ var DOORBELL_MEMBERS = {
|
|
|
6760
6959
|
type: "number",
|
|
6761
6960
|
unit: "%",
|
|
6762
6961
|
kind: "percent",
|
|
6763
|
-
provenance: "
|
|
6962
|
+
provenance: "verified",
|
|
6764
6963
|
writtenElsewhere: true,
|
|
6765
6964
|
description: "Ringtone volume (1708; confirmed on T8214, observed value 80). The WRITE is `audio`'s setRingtoneVolume \u2014 that capability owns every volume wire."
|
|
6766
6965
|
},
|
|
@@ -6865,7 +7064,7 @@ var DOORBELL_MEMBERS = {
|
|
|
6865
7064
|
param: 1710,
|
|
6866
7065
|
type: "string",
|
|
6867
7066
|
kind: "text",
|
|
6868
|
-
provenance: "
|
|
7067
|
+
provenance: "verified",
|
|
6869
7068
|
description: "Notification config JSON {notification_motion_onoff,notification_ring_onoff,notification_style} (1710; confirmed on T8214)."
|
|
6870
7069
|
},
|
|
6871
7070
|
/**
|
|
@@ -7526,120 +7725,41 @@ var ARMING_MEMBERS = {
|
|
|
7526
7725
|
var ARMING = {
|
|
7527
7726
|
capability: "arming",
|
|
7528
7727
|
description: "Station/device guard (arming) mode.",
|
|
7529
|
-
members: ARMING_MEMBERS,
|
|
7530
|
-
properties: propertiesOf(ARMING_MEMBERS),
|
|
7531
|
-
// A reported guard-mode param is the verified proof; every station-codec device also owns an
|
|
7532
|
-
// arming surface as part of its baseline.
|
|
7533
|
-
detection: { evidenceParams: [ARMING_CMD.SET_ARMING], codecs: ["station"] },
|
|
7534
|
-
ownedByStation: true,
|
|
7535
|
-
/**
|
|
7536
|
-
* Station-scoped push events. The hub reports a guard-mode switch and the alarm lifecycle on the
|
|
7537
|
-
* generic `CusPushEvent` channel, so these ids are shared with other families and resolve by
|
|
7538
|
-
* capability (see the barrel's event index).
|
|
7539
|
-
*
|
|
7540
|
-
* The alarm ids carry a static `phase` so one event name covers the whole lifecycle, the same shape
|
|
7541
|
-
* `battery` uses for its threshold pushes. The mode-switch push is known to identify WHICH mode and
|
|
7542
|
-
* what triggered the change, but neither has been observed on the wire here, so this emits the bare
|
|
7543
|
-
* transition and refreshes the `mode` read rather than trusting a decoded field.
|
|
7544
|
-
*/
|
|
7545
|
-
events: [
|
|
7546
|
-
{
|
|
7547
|
-
source: "push",
|
|
7548
|
-
match: CusPushEvent.MODE_SWITCH,
|
|
7549
|
-
emit: "armingModeChanged",
|
|
7550
|
-
refresh: { member: "mode" }
|
|
7551
|
-
},
|
|
7552
|
-
{ source: "push", match: CusPushEvent.ALARM, emit: "alarm", payload: { phase: "triggered" } },
|
|
7553
|
-
{ source: "push", match: CusPushEvent.ALARM_DELAY, emit: "alarm", payload: { phase: "delayed" } }
|
|
7554
|
-
]
|
|
7555
|
-
};
|
|
7556
|
-
|
|
7557
|
-
// dist/model/capabilities/storage.js
|
|
7558
|
-
var STORAGE = {
|
|
7559
|
-
capability: "storage",
|
|
7560
|
-
description: "Local storage (SD card / eMMC / HDD). Behaviour-only until a capacity param is captured.",
|
|
7561
|
-
properties: [],
|
|
7562
|
-
detection: { codecs: ["station"] }
|
|
7563
|
-
};
|
|
7564
|
-
|
|
7565
|
-
// dist/core/raw-dp-writer.js
|
|
7566
|
-
var WIRE = { VARINT: 0, LENGTH_DELIMITED: 2 };
|
|
7567
|
-
function encodeVarint(value) {
|
|
7568
|
-
if (!Number.isSafeInteger(value) || value < 0) {
|
|
7569
|
-
throw new Error(`varint: ${value} is not a non-negative safe integer`);
|
|
7570
|
-
}
|
|
7571
|
-
const out = [];
|
|
7572
|
-
let v = value;
|
|
7573
|
-
while (v > 127) {
|
|
7574
|
-
out.push(v % 128 + 128);
|
|
7575
|
-
v = Math.floor(v / 128);
|
|
7576
|
-
}
|
|
7577
|
-
out.push(v);
|
|
7578
|
-
return out;
|
|
7579
|
-
}
|
|
7580
|
-
function zigzag(value) {
|
|
7581
|
-
if (!Number.isSafeInteger(value))
|
|
7582
|
-
throw new Error(`zigzag: ${value} is not a safe integer`);
|
|
7583
|
-
return value < 0 ? -2 * value - 1 : 2 * value;
|
|
7584
|
-
}
|
|
7585
|
-
var RawDpWriter = class _RawDpWriter {
|
|
7586
|
-
body = [];
|
|
7587
|
-
/** A varint-valued field (`int32`, `uint32`, `bool`, an enum member). */
|
|
7588
|
-
int(field, value) {
|
|
7589
|
-
this.body.push(...encodeVarint(tag(field, WIRE.VARINT)), ...encodeVarint(value));
|
|
7590
|
-
return this;
|
|
7591
|
-
}
|
|
7592
|
-
/** A signed varint field (`sint32`, `sint64`), ZigZag-encoded — see {@link zigzag}. */
|
|
7593
|
-
sint(field, value) {
|
|
7594
|
-
return this.int(field, zigzag(value));
|
|
7595
|
-
}
|
|
7596
|
-
/** A boolean field, on protobuf's own terms: `1` or `0` in a varint. */
|
|
7597
|
-
bool(field, value) {
|
|
7598
|
-
return this.int(field, value ? 1 : 0);
|
|
7599
|
-
}
|
|
7600
|
-
/** A length-delimited field carrying raw bytes — a string, a blob, or a message built elsewhere. */
|
|
7601
|
-
bytes(field, value) {
|
|
7602
|
-
const bytes2 = value instanceof Uint8Array ? Array.from(value) : [...value];
|
|
7603
|
-
this.body.push(...encodeVarint(tag(field, WIRE.LENGTH_DELIMITED)), ...encodeVarint(bytes2.length), ...bytes2);
|
|
7604
|
-
return this;
|
|
7605
|
-
}
|
|
7606
|
-
/**
|
|
7607
|
-
* A nested sub-message, built by `build` into a writer of its own.
|
|
7608
|
-
*
|
|
7609
|
-
* The sub-message is emitted even when `build` writes nothing into it. That is not an oversight: an
|
|
7610
|
-
* EMPTY sub-message is how these protocols say "this subsystem exists and is in its zero state", as
|
|
7611
|
-
* opposed to saying nothing about it, and the readers on the other side of this file depend on the
|
|
7612
|
-
* distinction. Omitting the call is what says nothing about it.
|
|
7613
|
-
*/
|
|
7614
|
-
sub(field, build) {
|
|
7615
|
-
const inner = new _RawDpWriter();
|
|
7616
|
-
build(inner);
|
|
7617
|
-
return this.bytes(field, inner.toBytes());
|
|
7618
|
-
}
|
|
7619
|
-
/** The message body on its own, unframed — for nesting into another writer. */
|
|
7620
|
-
toBytes() {
|
|
7621
|
-
return [...this.body];
|
|
7622
|
-
}
|
|
7728
|
+
members: ARMING_MEMBERS,
|
|
7729
|
+
properties: propertiesOf(ARMING_MEMBERS),
|
|
7730
|
+
// A reported guard-mode param is the verified proof; every station-codec device also owns an
|
|
7731
|
+
// arming surface as part of its baseline.
|
|
7732
|
+
detection: { evidenceParams: [ARMING_CMD.SET_ARMING], codecs: ["station"] },
|
|
7733
|
+
ownedByStation: true,
|
|
7623
7734
|
/**
|
|
7624
|
-
* The
|
|
7735
|
+
* Station-scoped push events. The hub reports a guard-mode switch and the alarm lifecycle on the
|
|
7736
|
+
* generic `CusPushEvent` channel, so these ids are shared with other families and resolve by
|
|
7737
|
+
* capability (see the barrel's event index).
|
|
7625
7738
|
*
|
|
7626
|
-
*
|
|
7627
|
-
*
|
|
7739
|
+
* The alarm ids carry a static `phase` so one event name covers the whole lifecycle, the same shape
|
|
7740
|
+
* `battery` uses for its threshold pushes. The mode-switch push is known to identify WHICH mode and
|
|
7741
|
+
* what triggered the change, but neither has been observed on the wire here, so this emits the bare
|
|
7742
|
+
* transition and refreshes the `mode` read rather than trusting a decoded field.
|
|
7628
7743
|
*/
|
|
7629
|
-
|
|
7630
|
-
|
|
7631
|
-
|
|
7744
|
+
events: [
|
|
7745
|
+
{
|
|
7746
|
+
source: "push",
|
|
7747
|
+
match: CusPushEvent.MODE_SWITCH,
|
|
7748
|
+
emit: "armingModeChanged",
|
|
7749
|
+
refresh: { member: "mode" }
|
|
7750
|
+
},
|
|
7751
|
+
{ source: "push", match: CusPushEvent.ALARM, emit: "alarm", payload: { phase: "triggered" } },
|
|
7752
|
+
{ source: "push", match: CusPushEvent.ALARM_DELAY, emit: "alarm", payload: { phase: "delayed" } }
|
|
7753
|
+
]
|
|
7754
|
+
};
|
|
7755
|
+
|
|
7756
|
+
// dist/model/capabilities/storage.js
|
|
7757
|
+
var STORAGE = {
|
|
7758
|
+
capability: "storage",
|
|
7759
|
+
description: "Local storage (SD card / eMMC / HDD). Behaviour-only until a capacity param is captured.",
|
|
7760
|
+
properties: [],
|
|
7761
|
+
detection: { codecs: ["station"] }
|
|
7632
7762
|
};
|
|
7633
|
-
function tag(field, wire) {
|
|
7634
|
-
if (!Number.isSafeInteger(field) || field < 1)
|
|
7635
|
-
throw new Error(`field number: ${field} is not a positive integer`);
|
|
7636
|
-
return field * 8 + wire;
|
|
7637
|
-
}
|
|
7638
|
-
function rawDp(build) {
|
|
7639
|
-
const w = new RawDpWriter();
|
|
7640
|
-
build(w);
|
|
7641
|
-
return w.finish();
|
|
7642
|
-
}
|
|
7643
7763
|
|
|
7644
7764
|
// dist/model/capabilities/vacuum-dock.js
|
|
7645
7765
|
var VACUUM_DOCK_DP = 173;
|
|
@@ -13345,58 +13465,6 @@ function parseCleanRecordDetail(blob, codec) {
|
|
|
13345
13465
|
};
|
|
13346
13466
|
}
|
|
13347
13467
|
|
|
13348
|
-
// dist/core/lz4-block.js
|
|
13349
|
-
var MAX_OUTPUT = 64 * 1024 * 1024;
|
|
13350
|
-
function lz4BlockDecompress(block, size) {
|
|
13351
|
-
if (!Number.isSafeInteger(size) || size < 0 || size > MAX_OUTPUT)
|
|
13352
|
-
return void 0;
|
|
13353
|
-
const out = Buffer.alloc(size);
|
|
13354
|
-
const end = block.length;
|
|
13355
|
-
let pos = 0;
|
|
13356
|
-
let written = 0;
|
|
13357
|
-
const extend2 = (base) => {
|
|
13358
|
-
let len = base;
|
|
13359
|
-
if (base !== 15)
|
|
13360
|
-
return len;
|
|
13361
|
-
for (; ; ) {
|
|
13362
|
-
if (pos >= end)
|
|
13363
|
-
return void 0;
|
|
13364
|
-
const extra = block[pos++];
|
|
13365
|
-
len += extra;
|
|
13366
|
-
if (extra !== 255)
|
|
13367
|
-
return len;
|
|
13368
|
-
}
|
|
13369
|
-
};
|
|
13370
|
-
while (pos < end) {
|
|
13371
|
-
const token = block[pos++];
|
|
13372
|
-
const literals = extend2(token >> 4);
|
|
13373
|
-
if (literals === void 0)
|
|
13374
|
-
return void 0;
|
|
13375
|
-
if (pos + literals > end || written + literals > size)
|
|
13376
|
-
return void 0;
|
|
13377
|
-
for (let i = 0; i < literals; i++)
|
|
13378
|
-
out[written++] = block[pos++];
|
|
13379
|
-
if (pos === end)
|
|
13380
|
-
break;
|
|
13381
|
-
if (pos + 2 > end)
|
|
13382
|
-
return void 0;
|
|
13383
|
-
const offset = block[pos] | block[pos + 1] << 8;
|
|
13384
|
-
pos += 2;
|
|
13385
|
-
if (offset === 0 || offset > written)
|
|
13386
|
-
return void 0;
|
|
13387
|
-
const match = extend2(token & 15);
|
|
13388
|
-
if (match === void 0)
|
|
13389
|
-
return void 0;
|
|
13390
|
-
const length = match + 4;
|
|
13391
|
-
if (written + length > size)
|
|
13392
|
-
return void 0;
|
|
13393
|
-
let from = written - offset;
|
|
13394
|
-
for (let i = 0; i < length; i++)
|
|
13395
|
-
out[written++] = out[from++];
|
|
13396
|
-
}
|
|
13397
|
-
return written === size ? out : void 0;
|
|
13398
|
-
}
|
|
13399
|
-
|
|
13400
13468
|
// dist/model/vacuum-map.js
|
|
13401
13469
|
var DOCK_KINDS = ["charger", "station"];
|
|
13402
13470
|
var MAP_QUALITIES = ["incomplete", "rough", "effective", "listFull"];
|
|
@@ -14307,6 +14375,7 @@ __export(codec_exports, {
|
|
|
14307
14375
|
buildLookupWithKeyPayload2: () => buildLookupWithKeyPayload2,
|
|
14308
14376
|
buildRawCommandPayload: () => buildRawCommandPayload,
|
|
14309
14377
|
buildStringCommandPayload: () => buildStringCommandPayload,
|
|
14378
|
+
buildStringPairCommandPayload: () => buildStringPairCommandPayload,
|
|
14310
14379
|
buildVoidCommandPayload: () => buildVoidCommandPayload,
|
|
14311
14380
|
decodeP2PCloudIPs: () => decodeP2PCloudIPs,
|
|
14312
14381
|
decryptP2PData: () => decryptP2PData,
|
|
@@ -14343,6 +14412,11 @@ var ResponseMessageType = {
|
|
|
14343
14412
|
LOOKUP_ADDR: Buffer.from([241, 64]),
|
|
14344
14413
|
LOOKUP_ADDR2: Buffer.from([241, 130]),
|
|
14345
14414
|
CAM_ID: Buffer.from([241, 66]),
|
|
14415
|
+
/**
|
|
14416
|
+
* The device's own address record, sent in answer to CHECK_CAM ahead of CAM_ID: its 20-byte device id, one
|
|
14417
|
+
* record in `encodeSelfAddress`'s shape naming the address it answers from, then 8 zero bytes (44-byte payload).
|
|
14418
|
+
*/
|
|
14419
|
+
CAM_ADDR: Buffer.from([241, 67]),
|
|
14346
14420
|
TURN_SERVER_CAM_ID: Buffer.from([241, 132]),
|
|
14347
14421
|
PING: Buffer.from([241, 224]),
|
|
14348
14422
|
PONG: Buffer.from([241, 225]),
|
|
@@ -14554,6 +14628,12 @@ function buildIntStringCommandPayload(value, valueSub, strValue, channel = 0, ke
|
|
|
14554
14628
|
data
|
|
14555
14629
|
]);
|
|
14556
14630
|
}
|
|
14631
|
+
function buildStringPairCommandPayload(strValue, strValueSub, channel = 0, key, encType = 1) {
|
|
14632
|
+
const encrypted = !!key && key.length === 16;
|
|
14633
|
+
const body = Buffer.concat([Buffer.alloc(5), stringWithLength(strValue), stringWithLength(strValueSub)]);
|
|
14634
|
+
const data = encrypted ? encryptP2PData(paddingP2PData(body), key) : body;
|
|
14635
|
+
return buildRawCommandPayload(data, channel, encrypted ? encType : 0);
|
|
14636
|
+
}
|
|
14557
14637
|
function buildRawCommandPayload(data, channel = 0, signCode = 0, magic = [1, 0], streamId = 0) {
|
|
14558
14638
|
const header = Buffer.allocUnsafe(2);
|
|
14559
14639
|
header.writeUInt16LE(data.length, 0);
|
|
@@ -15159,6 +15239,7 @@ var LOCAL_LOOKUP_PORT = 32108;
|
|
|
15159
15239
|
var HEARTBEAT_MS = 5e3;
|
|
15160
15240
|
var PATH_SILENCE_MS = HEARTBEAT_MS * 3;
|
|
15161
15241
|
var LOOKUP_RETRY_MS = 1e3;
|
|
15242
|
+
var RECEIVE_BUFFER_BYTES = 4 * 1024 * 1024;
|
|
15162
15243
|
var CONNECT_TIMEOUT_MS = 15e3;
|
|
15163
15244
|
var STATION_CHANNEL3 = 255;
|
|
15164
15245
|
var CMD_GATEWAYINFO = 1100;
|
|
@@ -15184,6 +15265,7 @@ var SEQUENCE_LOOKBACK = 32768;
|
|
|
15184
15265
|
var STALE_RETRANSMIT_DEPTH = 1024;
|
|
15185
15266
|
var PUNCH_PROBE_SOCKETS = 7;
|
|
15186
15267
|
var REORDER_WAIT_MS = 250;
|
|
15268
|
+
var VIDEO_REORDER_WAIT_MS = 700;
|
|
15187
15269
|
var REORDER_MAX_DATAGRAMS = 128;
|
|
15188
15270
|
var MAX_TRACED_DATAGRAM_GAPS = 8;
|
|
15189
15271
|
var CMD_SET_PAYLOAD = 1350;
|
|
@@ -15194,6 +15276,7 @@ var CMD_DATABASE_IMAGE = 1308;
|
|
|
15194
15276
|
var CMD_DATABASE = 1306;
|
|
15195
15277
|
var DB_QUERY = { FULL_TABLE: 1e4, COMBINATION_WITH_AI: 10011 };
|
|
15196
15278
|
var GCM_AAD = Buffer.from("eufy security");
|
|
15279
|
+
var LEVEL2_SEQ_BASE = 16909056;
|
|
15197
15280
|
function paramReport(payload) {
|
|
15198
15281
|
const list = payload?.params;
|
|
15199
15282
|
if (!Array.isArray(list))
|
|
@@ -15208,12 +15291,57 @@ function paramReport(payload) {
|
|
|
15208
15291
|
return Object.keys(out).length ? out : void 0;
|
|
15209
15292
|
}
|
|
15210
15293
|
var traceSequence = 0;
|
|
15294
|
+
var DB_TABLE_TIMEOUT_MS = 15e3;
|
|
15295
|
+
function firstTableRows(text2) {
|
|
15296
|
+
const decoded = Buffer.from(text2, "latin1").toString("utf8");
|
|
15297
|
+
for (let start = decoded.indexOf("{"); start >= 0; start = decoded.indexOf("{", start + 1)) {
|
|
15298
|
+
const end = closingBrace(decoded, start);
|
|
15299
|
+
if (end < 0)
|
|
15300
|
+
return void 0;
|
|
15301
|
+
let parsed;
|
|
15302
|
+
try {
|
|
15303
|
+
parsed = JSON.parse(decoded.slice(start, end + 1));
|
|
15304
|
+
} catch {
|
|
15305
|
+
continue;
|
|
15306
|
+
}
|
|
15307
|
+
const data = parsed?.data;
|
|
15308
|
+
if (Array.isArray(data))
|
|
15309
|
+
return data;
|
|
15310
|
+
}
|
|
15311
|
+
return void 0;
|
|
15312
|
+
}
|
|
15313
|
+
function closingBrace(text2, start) {
|
|
15314
|
+
let depth = 0;
|
|
15315
|
+
let inString = false;
|
|
15316
|
+
let escaped = false;
|
|
15317
|
+
for (let i = start; i < text2.length; i++) {
|
|
15318
|
+
const c = text2[i];
|
|
15319
|
+
if (inString) {
|
|
15320
|
+
if (escaped)
|
|
15321
|
+
escaped = false;
|
|
15322
|
+
else if (c === "\\")
|
|
15323
|
+
escaped = true;
|
|
15324
|
+
else if (c === '"')
|
|
15325
|
+
inString = false;
|
|
15326
|
+
continue;
|
|
15327
|
+
}
|
|
15328
|
+
if (c === '"')
|
|
15329
|
+
inString = true;
|
|
15330
|
+
else if (c === "{")
|
|
15331
|
+
depth++;
|
|
15332
|
+
else if (c === "}" && --depth === 0)
|
|
15333
|
+
return i;
|
|
15334
|
+
}
|
|
15335
|
+
return -1;
|
|
15336
|
+
}
|
|
15211
15337
|
var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
15212
15338
|
cfg;
|
|
15213
15339
|
socket;
|
|
15214
15340
|
connected = false;
|
|
15215
15341
|
connecting = false;
|
|
15216
15342
|
closed = false;
|
|
15343
|
+
/** Whether a short receive buffer has been reported, so a reconnect does not repeat the same warning. */
|
|
15344
|
+
receiveBufferReported = false;
|
|
15217
15345
|
connectAddress;
|
|
15218
15346
|
seqNumber = 0;
|
|
15219
15347
|
/**
|
|
@@ -15240,8 +15368,8 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
15240
15368
|
audioStalled = false;
|
|
15241
15369
|
audioRetransmitTimer;
|
|
15242
15370
|
lastPongData;
|
|
15243
|
-
/** When
|
|
15244
|
-
|
|
15371
|
+
/** When the selected peer last answered outbound P2P traffic on this connection. */
|
|
15372
|
+
lastPeerAt;
|
|
15245
15373
|
/** Whether the silence has already been stated, so it is traced once per connection rather than per read. */
|
|
15246
15374
|
pathStaleTraced = false;
|
|
15247
15375
|
lookupTimer;
|
|
@@ -15310,17 +15438,17 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
15310
15438
|
/**
|
|
15311
15439
|
* How long this connection's path has been silent, or nothing where it has never answered.
|
|
15312
15440
|
*
|
|
15313
|
-
*
|
|
15314
|
-
*
|
|
15441
|
+
* PONG and ACK from the selected peer prove the path answers outbound traffic. `undefined` means no such reply
|
|
15442
|
+
* has arrived since connection, so silence alone does not establish a stale path.
|
|
15315
15443
|
*/
|
|
15316
15444
|
get pathSilentMs() {
|
|
15317
|
-
return this.
|
|
15445
|
+
return this.lastPeerAt === void 0 ? void 0 : Date.now() - this.lastPeerAt;
|
|
15318
15446
|
}
|
|
15319
15447
|
/**
|
|
15320
15448
|
* Whether this path can still be committed to, on the evidence the heartbeat gives.
|
|
15321
15449
|
*
|
|
15322
|
-
* False where
|
|
15323
|
-
* not known to be dead, so it answers true.
|
|
15450
|
+
* False where selected-peer replies arrived and then stopped for {@link PATH_SILENCE_MS}. A connection with
|
|
15451
|
+
* no post-connect reply is not known to be dead, so it answers true.
|
|
15324
15452
|
*
|
|
15325
15453
|
* Traces the silence once per connection, on the read that first observes it.
|
|
15326
15454
|
*/
|
|
@@ -15536,6 +15664,26 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
15536
15664
|
get isConnected() {
|
|
15537
15665
|
return this.connected;
|
|
15538
15666
|
}
|
|
15667
|
+
/**
|
|
15668
|
+
* Ask the OS for {@link RECEIVE_BUFFER_BYTES} on a bound socket, and warn once per session when it grants
|
|
15669
|
+
* less or refuses.
|
|
15670
|
+
*
|
|
15671
|
+
* The request is made here rather than through `createSocket`'s `recvBufferSize`: Node applies that option
|
|
15672
|
+
* inside the bind callback, where a refusal is thrown out of reach of this session and ends the process.
|
|
15673
|
+
*/
|
|
15674
|
+
requestReceiveBuffer(socket) {
|
|
15675
|
+
let granted;
|
|
15676
|
+
try {
|
|
15677
|
+
socket.setRecvBufferSize(RECEIVE_BUFFER_BYTES);
|
|
15678
|
+
granted = socket.getRecvBufferSize();
|
|
15679
|
+
} catch {
|
|
15680
|
+
granted = 0;
|
|
15681
|
+
}
|
|
15682
|
+
if (granted >= RECEIVE_BUFFER_BYTES || this.receiveBufferReported)
|
|
15683
|
+
return;
|
|
15684
|
+
this.receiveBufferReported = true;
|
|
15685
|
+
this.logger.warn(`[p2p] ${this.cfg.stationSn} UDP receive buffer below the ${RECEIVE_BUFFER_BYTES} bytes requested` + (granted > 0 ? ` (granted ${granted})` : " (request refused)") + `; live video can lose keyframes. Raise the OS limit (net.core.rmem_max on Linux, kern.ipc.maxsockbuf on BSD) to at least ${RECEIVE_BUFFER_BYTES}.`);
|
|
15686
|
+
}
|
|
15539
15687
|
/** Open the socket and start the lookup → hole-punch handshake. */
|
|
15540
15688
|
async connect() {
|
|
15541
15689
|
if (this.connecting || this.connected)
|
|
@@ -15559,6 +15707,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
15559
15707
|
socket.setBroadcast(true);
|
|
15560
15708
|
} catch {
|
|
15561
15709
|
}
|
|
15710
|
+
this.requestReceiveBuffer(socket);
|
|
15562
15711
|
resolve();
|
|
15563
15712
|
});
|
|
15564
15713
|
});
|
|
@@ -15575,7 +15724,8 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
15575
15724
|
this.trace({
|
|
15576
15725
|
phase: "lookup-channels",
|
|
15577
15726
|
local: !this.cfg.noBroadcast || this.cfg.localAddress !== void 0,
|
|
15578
|
-
cloud: this.cloudLookup !== void 0
|
|
15727
|
+
cloud: this.cloudLookup !== void 0,
|
|
15728
|
+
...this.cloudLookup ? {} : { cloudMissing: dskKey ? "cloud-addresses" : "dsk-key" }
|
|
15579
15729
|
});
|
|
15580
15730
|
this.sendLookups();
|
|
15581
15731
|
this.lookupTimer = setInterval(() => this.sendLookups(), LOOKUP_RETRY_MS);
|
|
@@ -15677,10 +15827,18 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
15677
15827
|
* straddling a level-2 wait on a camera whose failure to deliver video had a separate cause, and did not
|
|
15678
15828
|
* recur across later probes of it. Correlate an unmodelled type against a WORKING session before reading it
|
|
15679
15829
|
* as a cause.
|
|
15830
|
+
*
|
|
15831
|
+
* CAM_ADDR is recognised and dropped: it names the address the device is already answering from, and the
|
|
15832
|
+
* CAM_ID that follows it is what completes the connect.
|
|
15680
15833
|
*/
|
|
15681
15834
|
onMessage(msg, rinfo, socket = this.socket) {
|
|
15682
15835
|
if (!socket)
|
|
15683
15836
|
return;
|
|
15837
|
+
const fromConnectedPeer = this.connected && this.connectAddress?.host === rinfo.address && this.connectAddress.port === rinfo.port;
|
|
15838
|
+
if (fromConnectedPeer && (hasHeader(msg, ResponseMessageType.PONG) || hasHeader(msg, ResponseMessageType.ACK))) {
|
|
15839
|
+
this.lastPeerAt = Date.now();
|
|
15840
|
+
this.pathStaleTraced = false;
|
|
15841
|
+
}
|
|
15684
15842
|
if (!hasHeader(msg, ResponseMessageType.DATA)) {
|
|
15685
15843
|
this.logger.debug(`[p2p] ${this.cfg.stationSn} <<< ${rinfo.address}:${rinfo.port} header=${msg.subarray(0, 2).toString("hex")} len=${msg.length}`);
|
|
15686
15844
|
}
|
|
@@ -15698,8 +15856,6 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
15698
15856
|
this.onConnected({ host: rinfo.address, port: rinfo.port }, socket);
|
|
15699
15857
|
} else if (hasHeader(msg, ResponseMessageType.PONG)) {
|
|
15700
15858
|
this.lastPongData = msg.length > 4 ? msg.subarray(4) : void 0;
|
|
15701
|
-
this.lastPongAt = Date.now();
|
|
15702
|
-
this.pathStaleTraced = false;
|
|
15703
15859
|
} else if (hasHeader(msg, ResponseMessageType.PING)) {
|
|
15704
15860
|
this.send({ host: rinfo.address, port: rinfo.port }, RequestMessageType.PONG, void 0, socket);
|
|
15705
15861
|
} else if (hasHeader(msg, ResponseMessageType.ACK)) {
|
|
@@ -15707,7 +15863,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
15707
15863
|
} else if (hasHeader(msg, ResponseMessageType.DATA)) {
|
|
15708
15864
|
if (this.connected)
|
|
15709
15865
|
this.onData(msg, { host: rinfo.address, port: rinfo.port });
|
|
15710
|
-
} else {
|
|
15866
|
+
} else if (!hasHeader(msg, ResponseMessageType.CAM_ADDR)) {
|
|
15711
15867
|
this.logger.debug(`[p2p] ${this.cfg.stationSn} UNHANDLED payload hex: ${msg.toString("hex")}`);
|
|
15712
15868
|
}
|
|
15713
15869
|
}
|
|
@@ -15998,6 +16154,17 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
15998
16154
|
this.seqNumber = this.seqNumber + 1 & 65535;
|
|
15999
16155
|
this.send(this.connectAddress, RequestMessageType.DATA, data);
|
|
16000
16156
|
}
|
|
16157
|
+
/** Send a level-1 {@link buildStringPairCommandPayload} command on `channel`. */
|
|
16158
|
+
sendStringPairCommand(commandType, strValue, strValueSub, channel) {
|
|
16159
|
+
if (!this.connectAddress)
|
|
16160
|
+
throw new Error(`P2P session ${this.cfg.stationSn} is not connected`);
|
|
16161
|
+
const data = Buffer.concat([
|
|
16162
|
+
buildCommandHeader(this.seqNumber, commandType),
|
|
16163
|
+
buildStringPairCommandPayload(strValue, strValueSub, channel, this.level1Key, 1)
|
|
16164
|
+
]);
|
|
16165
|
+
this.seqNumber = this.seqNumber + 1 & 65535;
|
|
16166
|
+
this.send(this.connectAddress, RequestMessageType.DATA, data);
|
|
16167
|
+
}
|
|
16001
16168
|
/**
|
|
16002
16169
|
* Send a **level-2 (AES-256-GCM, signCode 8) control payload** to a HomeBase-attached device. The
|
|
16003
16170
|
* target camera is selected by `channel` (= device_channel) + the `mChannel` envelope — the same
|
|
@@ -16275,11 +16442,6 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16275
16442
|
this.seqNumber = this.seqNumber + 1 & 65535;
|
|
16276
16443
|
this.send(this.connectAddress, RequestMessageType.DATA, data);
|
|
16277
16444
|
}
|
|
16278
|
-
/**
|
|
16279
|
-
* Encrypt a level-2 command body (signCode 8): `tag(16) ‖ nonce(12) ‖ [seq,03,02,01](4) ‖
|
|
16280
|
-
* ciphertext`, AES-256-GCM under the negotiated session key, AAD "eufy security". Inverse of
|
|
16281
|
-
* `decryptLevel2`. The 4-byte sub-header is cleartext (skipped on decrypt); `seq` is a counter.
|
|
16282
|
-
*/
|
|
16283
16445
|
/**
|
|
16284
16446
|
* Decode a `CMD_VIDEO_FRAME` (1300) payload into clean Annex-B H.264 (the 22-byte frame header
|
|
16285
16447
|
* stripped). Reversed from the V6 app + live H.264 captures: the 22-byte header is
|
|
@@ -16346,6 +16508,11 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16346
16508
|
}
|
|
16347
16509
|
return this.rsaModulusHex;
|
|
16348
16510
|
}
|
|
16511
|
+
/**
|
|
16512
|
+
* Encrypt a level-2 command body (signCode 8): `tag(16) ‖ nonce(12) ‖ seq(4) ‖ ciphertext`,
|
|
16513
|
+
* AES-256-GCM under the negotiated session key, AAD "eufy security". Inverse of `decryptLevel2`.
|
|
16514
|
+
* The 4-byte sub-header is cleartext (skipped on decrypt); `seq` counts up from {@link LEVEL2_SEQ_BASE}.
|
|
16515
|
+
*/
|
|
16349
16516
|
encryptLevel2(plaintext) {
|
|
16350
16517
|
if (!this.level2Key)
|
|
16351
16518
|
return void 0;
|
|
@@ -16353,8 +16520,8 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16353
16520
|
const c = createCipheriv3("aes-256-gcm", this.level2Key, nonce);
|
|
16354
16521
|
c.setAAD(GCM_AAD);
|
|
16355
16522
|
const ct = Buffer.concat([c.update(plaintext), c.final()]);
|
|
16356
|
-
const sub2 = Buffer.
|
|
16357
|
-
|
|
16523
|
+
const sub2 = Buffer.alloc(4);
|
|
16524
|
+
sub2.writeUInt32LE(LEVEL2_SEQ_BASE + this.level2Seq++ >>> 0);
|
|
16358
16525
|
return Buffer.concat([c.getAuthTag(), nonce, sub2, ct]);
|
|
16359
16526
|
}
|
|
16360
16527
|
/** Send a no-arg command frame (e.g. CMD_GATEWAYINFO) on a channel (default: the station channel). */
|
|
@@ -16400,8 +16567,14 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16400
16567
|
* The HomeBase streams back `CMD_DATABASE` (1306) frames `{cmd:10000,count,data:[…]}`,
|
|
16401
16568
|
* level-1-encrypted — decoded and emitted as `dbChunk` (decrypted text) per frame.
|
|
16402
16569
|
* Tables: `familiar_faces`, `person_basic_info`, `event_person_list`, `history_record_info`.
|
|
16570
|
+
*
|
|
16571
|
+
* Throws while a {@link readDatabase} is accumulating: every table answers `{data:[…]}` and the
|
|
16572
|
+
* frames tie no chunk to its request, so a second query's reply would be assembled into the first
|
|
16573
|
+
* one's buffer and answered as its rows.
|
|
16403
16574
|
*/
|
|
16404
16575
|
queryDatabase(table, opts = {}) {
|
|
16576
|
+
if (this.dbReadInFlight)
|
|
16577
|
+
throw new Error(`queryDatabase: ${this.cfg.stationSn} is already reading a table`);
|
|
16405
16578
|
if (!this.connectAddress)
|
|
16406
16579
|
throw new Error("not connected");
|
|
16407
16580
|
const channel = opts.channel ?? STATION_CHANNEL3;
|
|
@@ -16464,6 +16637,49 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16464
16637
|
query: this.fullTableQuery()
|
|
16465
16638
|
});
|
|
16466
16639
|
}
|
|
16640
|
+
/**
|
|
16641
|
+
* Query one on-station table and answer its rows, once the reply is whole.
|
|
16642
|
+
*
|
|
16643
|
+
* The request half of {@link queryDatabase} with its reply assembled: `CMD_DATABASE` arrives as
|
|
16644
|
+
* several frames whose decrypted text is a fragment of one document, so the fragments are
|
|
16645
|
+
* accumulated here and scanned after each one. Answers that document's `data` rows. Rejects when
|
|
16646
|
+
* `signal` aborts, and when `timeoutMs` (default {@link DB_TABLE_TIMEOUT_MS}) elapses with no
|
|
16647
|
+
* complete reply.
|
|
16648
|
+
*
|
|
16649
|
+
* One read at a time per session: while one is accumulating, every {@link queryDatabase} on the
|
|
16650
|
+
* session throws, so a second reply cannot land in this buffer.
|
|
16651
|
+
*/
|
|
16652
|
+
async readDatabase(table, opts = {}) {
|
|
16653
|
+
if (opts.signal?.aborted)
|
|
16654
|
+
throw new Error("readDatabase: aborted");
|
|
16655
|
+
this.queryDatabase(table, { accountId: opts.accountId, query: this.fullTableQuery() });
|
|
16656
|
+
this.dbReadInFlight = true;
|
|
16657
|
+
try {
|
|
16658
|
+
return await new Promise((resolve, reject) => {
|
|
16659
|
+
let text2 = "";
|
|
16660
|
+
const onChunk = (chunk) => {
|
|
16661
|
+
text2 += chunk.text;
|
|
16662
|
+
const rows = firstTableRows(text2);
|
|
16663
|
+
if (rows)
|
|
16664
|
+
settle(() => resolve(rows));
|
|
16665
|
+
};
|
|
16666
|
+
const timer = setTimeout(() => settle(() => reject(new Error(`readDatabase: ${this.cfg.stationSn} sent no complete ${table}`))), opts.timeoutMs ?? DB_TABLE_TIMEOUT_MS);
|
|
16667
|
+
const onAbort = () => settle(() => reject(new Error("readDatabase: aborted")));
|
|
16668
|
+
const settle = (finish) => {
|
|
16669
|
+
clearTimeout(timer);
|
|
16670
|
+
this.off("dbChunk", onChunk);
|
|
16671
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
16672
|
+
finish();
|
|
16673
|
+
};
|
|
16674
|
+
this.on("dbChunk", onChunk);
|
|
16675
|
+
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
16676
|
+
});
|
|
16677
|
+
} finally {
|
|
16678
|
+
this.dbReadInFlight = false;
|
|
16679
|
+
}
|
|
16680
|
+
}
|
|
16681
|
+
/** Whether a {@link readDatabase} is accumulating; see {@link queryDatabase} for what it bars. */
|
|
16682
|
+
dbReadInFlight = false;
|
|
16467
16683
|
/**
|
|
16468
16684
|
* Request the **face feature rows** over P2P (`face_feature_info`, inner `cmd 10000`). Each row
|
|
16469
16685
|
* carries `{person_id, face_name, face_id, face_picture_content, face_feature_file_path}` — where
|
|
@@ -16531,7 +16747,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16531
16747
|
* if the missing one arrives. A logical frame's payload spans datagrams that carry no header of their
|
|
16532
16748
|
* own, so reassembling around a hole is impossible; waiting for it is what keeps the frame whole.
|
|
16533
16749
|
*
|
|
16534
|
-
* Only once
|
|
16750
|
+
* Only once the data type's reorder wait passes, or {@link REORDER_MAX_DATAGRAMS} pile up, is the datagram
|
|
16535
16751
|
* treated as lost: a pending frame is discarded and delivery resumes from the earliest held datagram.
|
|
16536
16752
|
*/
|
|
16537
16753
|
onData(msg, addr) {
|
|
@@ -16586,7 +16802,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16586
16802
|
const timer = setTimeout(() => {
|
|
16587
16803
|
reorder.timer = void 0;
|
|
16588
16804
|
this.abandonHole(dataType);
|
|
16589
|
-
}, REORDER_WAIT_MS);
|
|
16805
|
+
}, dataType === P2PDataType.VIDEO ? VIDEO_REORDER_WAIT_MS : REORDER_WAIT_MS);
|
|
16590
16806
|
timer.unref?.();
|
|
16591
16807
|
reorder.timer = timer;
|
|
16592
16808
|
}
|
|
@@ -18176,11 +18392,195 @@ function annexbToJpeg(annexb, spawn2) {
|
|
|
18176
18392
|
});
|
|
18177
18393
|
}
|
|
18178
18394
|
|
|
18395
|
+
// dist/transport/p2p/recording-download.js
|
|
18396
|
+
import { createDecipheriv as createDecipheriv7 } from "node:crypto";
|
|
18397
|
+
var RECORDING_DATA_TYPE = 3;
|
|
18398
|
+
var VIDEO_HEADER_LEN2 = 22;
|
|
18399
|
+
var AUDIO_HEADER_LEN2 = 16;
|
|
18400
|
+
var AUDIO_NONCE_START = 32;
|
|
18401
|
+
var AUDIO_BODY_START = 44;
|
|
18402
|
+
var AUDIO_CODEC_AAC_LC = 0;
|
|
18403
|
+
var FINISH_CHANNEL = 0;
|
|
18404
|
+
var FIRST_FRAME_WAIT_MS = 2e4;
|
|
18405
|
+
var IDLE_END_MS = 15e3;
|
|
18406
|
+
var DEFAULT_TRANSFER_MS = 12e4;
|
|
18407
|
+
var MAX_TRANSFER_BYTES = 32 * 1024 * 1024;
|
|
18408
|
+
function homeBase2RecordingPath(channel, recording) {
|
|
18409
|
+
if (!/^\d{14}$/.test(recording) || !Number.isInteger(channel) || channel < 0 || channel > 99)
|
|
18410
|
+
return void 0;
|
|
18411
|
+
return `/media/mmcblk0p1/Camera${String(channel).padStart(2, "0")}/${recording}.dat`;
|
|
18412
|
+
}
|
|
18413
|
+
function receiveRecording(session, request) {
|
|
18414
|
+
let resolveFrames;
|
|
18415
|
+
let rejectFrames;
|
|
18416
|
+
const frames = new Promise((resolve, reject) => {
|
|
18417
|
+
resolveFrames = resolve;
|
|
18418
|
+
rejectFrames = reject;
|
|
18419
|
+
});
|
|
18420
|
+
let resolveDrained;
|
|
18421
|
+
const drained = new Promise((resolve) => resolveDrained = resolve);
|
|
18422
|
+
const limitMs = request.timeoutMs ?? DEFAULT_TRANSFER_MS;
|
|
18423
|
+
let collected = [];
|
|
18424
|
+
const started = Date.now();
|
|
18425
|
+
let lastFrameAt = started;
|
|
18426
|
+
let received = false;
|
|
18427
|
+
let bytes2 = 0;
|
|
18428
|
+
let draining;
|
|
18429
|
+
let delivered = false;
|
|
18430
|
+
let listening = true;
|
|
18431
|
+
const deliver = (error) => {
|
|
18432
|
+
if (delivered)
|
|
18433
|
+
return;
|
|
18434
|
+
delivered = true;
|
|
18435
|
+
request.signal?.removeEventListener("abort", onAbort);
|
|
18436
|
+
if (error)
|
|
18437
|
+
rejectFrames(error);
|
|
18438
|
+
else
|
|
18439
|
+
resolveFrames(collected);
|
|
18440
|
+
};
|
|
18441
|
+
const stop = () => {
|
|
18442
|
+
if (!listening)
|
|
18443
|
+
return;
|
|
18444
|
+
listening = false;
|
|
18445
|
+
clearInterval(tick);
|
|
18446
|
+
session.off("data", onData);
|
|
18447
|
+
resolveDrained();
|
|
18448
|
+
};
|
|
18449
|
+
const incomplete = (why) => deliver(new RecordingDownloadError("incomplete", `the recording transfer ${why} before its finish frame`));
|
|
18450
|
+
const drain = (why) => {
|
|
18451
|
+
draining ??= why;
|
|
18452
|
+
collected = [];
|
|
18453
|
+
};
|
|
18454
|
+
const onData = (frame) => {
|
|
18455
|
+
if (frame.commandId === CommandType.CMD_DOWNLOAD_FINISH) {
|
|
18456
|
+
if (frame.channel !== request.channel && frame.channel !== FINISH_CHANNEL)
|
|
18457
|
+
return;
|
|
18458
|
+
if (draining)
|
|
18459
|
+
incomplete(draining);
|
|
18460
|
+
else
|
|
18461
|
+
deliver();
|
|
18462
|
+
return stop();
|
|
18463
|
+
}
|
|
18464
|
+
if (frame.channel !== request.channel)
|
|
18465
|
+
return;
|
|
18466
|
+
if (frame.commandId !== CommandType.CMD_VIDEO_FRAME && frame.commandId !== CommandType.CMD_AUDIO_FRAME)
|
|
18467
|
+
return;
|
|
18468
|
+
if (frame.dataType !== RECORDING_DATA_TYPE)
|
|
18469
|
+
return;
|
|
18470
|
+
received = true;
|
|
18471
|
+
lastFrameAt = Date.now();
|
|
18472
|
+
if (draining)
|
|
18473
|
+
return;
|
|
18474
|
+
collected.push({ commandId: frame.commandId, signCode: frame.signCode, raw: Buffer.from(frame.raw) });
|
|
18475
|
+
bytes2 += frame.raw.length;
|
|
18476
|
+
if (bytes2 >= MAX_TRANSFER_BYTES)
|
|
18477
|
+
drain(`reached ${MAX_TRANSFER_BYTES} bytes`);
|
|
18478
|
+
};
|
|
18479
|
+
const onAbort = () => {
|
|
18480
|
+
deliver(request.signal?.reason instanceof Error ? request.signal.reason : new Error("aborted"));
|
|
18481
|
+
drain("was aborted");
|
|
18482
|
+
};
|
|
18483
|
+
const tick = setInterval(() => {
|
|
18484
|
+
const now2 = Date.now();
|
|
18485
|
+
if (!received && now2 - started > FIRST_FRAME_WAIT_MS) {
|
|
18486
|
+
deliver(new RecordingDownloadError("no-data", `the station sent nothing within ${FIRST_FRAME_WAIT_MS} ms`));
|
|
18487
|
+
stop();
|
|
18488
|
+
} else if (received && now2 - lastFrameAt > IDLE_END_MS) {
|
|
18489
|
+
incomplete(draining ?? `went quiet for ${IDLE_END_MS} ms`);
|
|
18490
|
+
stop();
|
|
18491
|
+
} else if (now2 - started > 2 * limitMs && draining) {
|
|
18492
|
+
incomplete(draining);
|
|
18493
|
+
stop();
|
|
18494
|
+
} else if (now2 - started > limitMs) {
|
|
18495
|
+
drain(`ran past ${limitMs} ms`);
|
|
18496
|
+
}
|
|
18497
|
+
}, 250);
|
|
18498
|
+
if (request.signal?.aborted) {
|
|
18499
|
+
onAbort();
|
|
18500
|
+
stop();
|
|
18501
|
+
return { frames, drained };
|
|
18502
|
+
}
|
|
18503
|
+
request.signal?.addEventListener("abort", onAbort, { once: true });
|
|
18504
|
+
session.on("data", onData);
|
|
18505
|
+
try {
|
|
18506
|
+
session.sendStringPairCommand(CommandType.CMD_DOWNLOAD_VIDEO, request.path, request.accountId, request.channel);
|
|
18507
|
+
} catch (error) {
|
|
18508
|
+
deliver(error instanceof Error ? error : new Error(String(error)));
|
|
18509
|
+
stop();
|
|
18510
|
+
}
|
|
18511
|
+
return { frames, drained };
|
|
18512
|
+
}
|
|
18513
|
+
function openAudio(key, nonce, tag2, body) {
|
|
18514
|
+
try {
|
|
18515
|
+
const decipher = createDecipheriv7("aes-256-gcm", key, nonce);
|
|
18516
|
+
decipher.setAAD(VIDEO_GCM_AAD);
|
|
18517
|
+
decipher.setAuthTag(tag2);
|
|
18518
|
+
return Buffer.concat([decipher.update(body), decipher.final()]);
|
|
18519
|
+
} catch {
|
|
18520
|
+
return void 0;
|
|
18521
|
+
}
|
|
18522
|
+
}
|
|
18523
|
+
function decodeRecording(frames, eccPrivateKeyHex) {
|
|
18524
|
+
const eccPrivateKey = Buffer.from(eccPrivateKeyHex, "hex");
|
|
18525
|
+
if (eccPrivateKey.length !== 32)
|
|
18526
|
+
throw new RecordingDownloadError("undecodable", "the cipher key is not a P-256 key");
|
|
18527
|
+
const decoder = new VideoFrameDecoder(eccPrivateKey);
|
|
18528
|
+
const video = [];
|
|
18529
|
+
const audio = [];
|
|
18530
|
+
const numbers = /* @__PURE__ */ new Set();
|
|
18531
|
+
let firstStamp;
|
|
18532
|
+
let lastStamp;
|
|
18533
|
+
for (const { commandId, raw } of frames) {
|
|
18534
|
+
if (commandId === CommandType.CMD_VIDEO_FRAME) {
|
|
18535
|
+
const header = parseVideoFrameHeader(raw);
|
|
18536
|
+
if (!header)
|
|
18537
|
+
continue;
|
|
18538
|
+
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;
|
|
18539
|
+
if (!h264?.length)
|
|
18540
|
+
continue;
|
|
18541
|
+
video.push(h264);
|
|
18542
|
+
numbers.add(header.sequence);
|
|
18543
|
+
const stamp = raw.readUIntLE(14, 6);
|
|
18544
|
+
firstStamp ??= stamp;
|
|
18545
|
+
lastStamp = stamp;
|
|
18546
|
+
} else if (commandId === CommandType.CMD_AUDIO_FRAME) {
|
|
18547
|
+
const mediaKey = decoder.currentMediaKey;
|
|
18548
|
+
if (!mediaKey || raw.length < AUDIO_BODY_START || raw[5] !== AUDIO_CODEC_AAC_LC)
|
|
18549
|
+
continue;
|
|
18550
|
+
const len = raw.readUInt32LE(0);
|
|
18551
|
+
if (raw.length < AUDIO_BODY_START + len)
|
|
18552
|
+
continue;
|
|
18553
|
+
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));
|
|
18554
|
+
if (aac?.length)
|
|
18555
|
+
audio.push(aac);
|
|
18556
|
+
}
|
|
18557
|
+
}
|
|
18558
|
+
if (!video.length)
|
|
18559
|
+
throw new RecordingDownloadError("undecodable", "no video frame of the recording decoded");
|
|
18560
|
+
const ordered = [...numbers];
|
|
18561
|
+
let missingFrames = 0;
|
|
18562
|
+
for (let i = 1; i < ordered.length; i++) {
|
|
18563
|
+
const step = ordered[i] - ordered[i - 1] & 65535;
|
|
18564
|
+
if (step > 1 && step < 32768)
|
|
18565
|
+
missingFrames += step - 1;
|
|
18566
|
+
}
|
|
18567
|
+
const span = ordered.length > 1 ? ordered[ordered.length - 1] - ordered[0] & 65535 : 0;
|
|
18568
|
+
const durationMs = (lastStamp ?? 0) - (firstStamp ?? 0);
|
|
18569
|
+
return {
|
|
18570
|
+
video: Buffer.concat(video),
|
|
18571
|
+
...audio.length ? { audio: Buffer.concat(audio) } : {},
|
|
18572
|
+
frames: ordered.length,
|
|
18573
|
+
missingFrames,
|
|
18574
|
+
durationMs,
|
|
18575
|
+
fps: durationMs > 0 && span > 0 ? Math.round(span * 1e5 / durationMs) / 100 : 0
|
|
18576
|
+
};
|
|
18577
|
+
}
|
|
18578
|
+
|
|
18179
18579
|
// dist/transport/p2p/shared-live-source.js
|
|
18180
18580
|
import { EventEmitter as EventEmitter4 } from "node:events";
|
|
18181
|
-
|
|
18182
|
-
|
|
18183
|
-
return Number.isFinite(
|
|
18581
|
+
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
18582
|
+
function timerMs(value, fallback) {
|
|
18583
|
+
return value !== void 0 && Number.isFinite(value) && value >= 1 && value <= MAX_TIMER_DELAY_MS ? value : fallback;
|
|
18184
18584
|
}
|
|
18185
18585
|
function sameConfig(a, b) {
|
|
18186
18586
|
return b !== void 0 && a.codec === b.codec && a.width === b.width && a.height === b.height;
|
|
@@ -18433,14 +18833,14 @@ var SharedLiveSource = class {
|
|
|
18433
18833
|
tag;
|
|
18434
18834
|
constructor(opts) {
|
|
18435
18835
|
this.opts = opts;
|
|
18436
|
-
this.lingerMs = opts.lingerMs
|
|
18836
|
+
this.lingerMs = opts.lingerMs === 0 ? 1 : timerMs(opts.lingerMs, 8e3);
|
|
18437
18837
|
this.maxQueue = opts.maxQueue ?? 900;
|
|
18438
|
-
this.preBufferMs =
|
|
18439
|
-
this.warmRetryMs = opts.warmRetryMs
|
|
18440
|
-
this.warmTimeoutMs = opts.warmTimeoutMs
|
|
18838
|
+
this.preBufferMs = timerMs((opts.preBufferSeconds ?? 0) * 1e3, 0);
|
|
18839
|
+
this.warmRetryMs = timerMs(opts.warmRetryMs, 2e3);
|
|
18840
|
+
this.warmTimeoutMs = timerMs(opts.warmTimeoutMs, 2e4);
|
|
18441
18841
|
this.powered = opts.powered ?? "wired";
|
|
18442
|
-
this.batteryBudgetMs = opts.batteryBudgetMs
|
|
18443
|
-
this.budgetGraceMs = opts.budgetGraceMs
|
|
18842
|
+
this.batteryBudgetMs = timerMs(opts.batteryBudgetMs, 45e3);
|
|
18843
|
+
this.budgetGraceMs = timerMs(opts.budgetGraceMs, 1e4);
|
|
18444
18844
|
this.logger = opts.logger ?? noopLogger;
|
|
18445
18845
|
this.tag = opts.label ? `[live ${opts.label}]` : "[live]";
|
|
18446
18846
|
this.traceId = `pull-${++pullSequence}`;
|
|
@@ -18688,7 +19088,7 @@ var SharedLiveSource = class {
|
|
|
18688
19088
|
if (this.disposed || !this.stream)
|
|
18689
19089
|
return;
|
|
18690
19090
|
this.clearBudget();
|
|
18691
|
-
this.budgetTimer.arm(ms
|
|
19091
|
+
this.budgetTimer.arm(timerMs(ms, this.batteryBudgetMs), () => this.onBudgetExpire());
|
|
18692
19092
|
}
|
|
18693
19093
|
/** Settle a reuse watch on any frame — the join already holds a decodable picture. */
|
|
18694
19094
|
settleReuseWatch() {
|
|
@@ -18829,7 +19229,7 @@ var SharedLiveSource = class {
|
|
|
18829
19229
|
return this.bufferedMedia(seconds).filter((item) => item.kind === "video").map((item) => item.frame);
|
|
18830
19230
|
}
|
|
18831
19231
|
bufferedMedia(seconds) {
|
|
18832
|
-
const requested = Math.min(
|
|
19232
|
+
const requested = Number.isFinite(seconds) ? Math.min(seconds * 1e3, this.preBufferMs) : 0;
|
|
18833
19233
|
if (requested <= 0 || !this.ring.length)
|
|
18834
19234
|
return [];
|
|
18835
19235
|
const start = this.windowStart(Date.now() - requested);
|
|
@@ -20356,6 +20756,8 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
20356
20756
|
talkbacks = /* @__PURE__ */ new Map();
|
|
20357
20757
|
/** cipher_id → ECC private key (one eufylife get_ciphers call per cipher), shared across (re)opens. */
|
|
20358
20758
|
cipherKeyCache = /* @__PURE__ */ new Map();
|
|
20759
|
+
/** The recording download in flight per station; a station serves one at a time. */
|
|
20760
|
+
recordingDownloads = /* @__PURE__ */ new Map();
|
|
20359
20761
|
constructor(deps) {
|
|
20360
20762
|
this.deps = deps;
|
|
20361
20763
|
this.manager = new SessionManager({
|
|
@@ -20568,28 +20970,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
20568
20970
|
localAddress,
|
|
20569
20971
|
dskKey,
|
|
20570
20972
|
noBroadcast: this.deps.noBroadcast,
|
|
20571
|
-
resolveCipherKey:
|
|
20572
|
-
if (this.cipherKeyCache.has(cipherId))
|
|
20573
|
-
return this.cipherKeyCache.get(cipherId);
|
|
20574
|
-
let ecc;
|
|
20575
|
-
try {
|
|
20576
|
-
const ciphers = await this.deps.mega.getCiphers([cipherId], adminUserId, stationSn);
|
|
20577
|
-
ecc = ciphers.find((c) => Number(c.cipher_id) === cipherId)?.ecc_private_key;
|
|
20578
|
-
if (ecc === void 0 && ciphers[0]?.ecc_private_key !== void 0) {
|
|
20579
|
-
ecc = ciphers[0].ecc_private_key;
|
|
20580
|
-
this.traceOnStation(session, {
|
|
20581
|
-
phase: "cipher-fallback",
|
|
20582
|
-
cipherId,
|
|
20583
|
-
answeredCipherId: Number(ciphers[0].cipher_id)
|
|
20584
|
-
});
|
|
20585
|
-
}
|
|
20586
|
-
} catch (e) {
|
|
20587
|
-
this.deps.onError(e instanceof Error ? e : new Error(String(e)));
|
|
20588
|
-
}
|
|
20589
|
-
if (ecc !== void 0)
|
|
20590
|
-
this.cipherKeyCache.set(cipherId, ecc);
|
|
20591
|
-
return ecc;
|
|
20592
|
-
},
|
|
20973
|
+
resolveCipherKey: (cipherId) => this.cipherKeyFor(session, cipherId, adminUserId, stationSn),
|
|
20593
20974
|
logger: this.deps.logger ?? noopLogger
|
|
20594
20975
|
});
|
|
20595
20976
|
session.on("error", (e) => this.reportError(e));
|
|
@@ -20613,6 +20994,33 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
20613
20994
|
session.on("data", (f) => this.deps.onFrame(stationSn, f));
|
|
20614
20995
|
return session;
|
|
20615
20996
|
}
|
|
20997
|
+
/**
|
|
20998
|
+
* The ECC private key of `cipherId`, from the router's cache or one `get_ciphers` call. When the cloud
|
|
20999
|
+
* answers with another cipher, that one is used and traced on `session`. Only a successful lookup is
|
|
21000
|
+
* cached.
|
|
21001
|
+
*/
|
|
21002
|
+
async cipherKeyFor(session, cipherId, userId, stationSn) {
|
|
21003
|
+
if (this.cipherKeyCache.has(cipherId))
|
|
21004
|
+
return this.cipherKeyCache.get(cipherId);
|
|
21005
|
+
let ecc;
|
|
21006
|
+
try {
|
|
21007
|
+
const ciphers = await this.deps.mega.getCiphers([cipherId], userId, stationSn);
|
|
21008
|
+
ecc = ciphers.find((c) => Number(c.cipher_id) === cipherId)?.ecc_private_key;
|
|
21009
|
+
if (ecc === void 0 && ciphers[0]?.ecc_private_key !== void 0) {
|
|
21010
|
+
ecc = ciphers[0].ecc_private_key;
|
|
21011
|
+
this.traceOnStation(session, {
|
|
21012
|
+
phase: "cipher-fallback",
|
|
21013
|
+
cipherId,
|
|
21014
|
+
answeredCipherId: Number(ciphers[0].cipher_id)
|
|
21015
|
+
});
|
|
21016
|
+
}
|
|
21017
|
+
} catch (e) {
|
|
21018
|
+
this.deps.onError(e instanceof Error ? e : new Error(String(e)));
|
|
21019
|
+
}
|
|
21020
|
+
if (ecc !== void 0)
|
|
21021
|
+
this.cipherKeyCache.set(cipherId, ecc);
|
|
21022
|
+
return ecc;
|
|
21023
|
+
}
|
|
20616
21024
|
/**
|
|
20617
21025
|
* Open (or reuse) a station's P2P session and await its completed handshake. An optional abort only
|
|
20618
21026
|
* stops this wait; session ownership remains with {@link SessionManager} and its normal teardown.
|
|
@@ -20740,7 +21148,9 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
20740
21148
|
* start has no level-1 form for.
|
|
20741
21149
|
*/
|
|
20742
21150
|
mediaProviderFor(sn) {
|
|
21151
|
+
const recordings = this.downloadsRecordings(sn) ? { downloadRecording: (opts) => this.downloadRecording(sn, opts) } : {};
|
|
20743
21152
|
return {
|
|
21153
|
+
...recordings,
|
|
20744
21154
|
snapshotLive: async (opts) => {
|
|
20745
21155
|
const source = await this.sharedLiveSourceFor(sn, opts ?? {});
|
|
20746
21156
|
return captureSnapshotFromShared(source, {
|
|
@@ -20779,6 +21189,63 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
20779
21189
|
}
|
|
20780
21190
|
};
|
|
20781
21191
|
}
|
|
21192
|
+
/**
|
|
21193
|
+
* Whether `sn` is a camera attached to a HomeBase 2 (T8010), the one station whose recording path layout
|
|
21194
|
+
* and frame formats are confirmed.
|
|
21195
|
+
*/
|
|
21196
|
+
downloadsRecordings(sn) {
|
|
21197
|
+
const dev = this.recordFor(sn);
|
|
21198
|
+
if (!dev)
|
|
21199
|
+
return false;
|
|
21200
|
+
const parentSn = stationOf(dev);
|
|
21201
|
+
return parentSn !== sn && this.recordFor(parentSn)?.model === "T8010";
|
|
21202
|
+
}
|
|
21203
|
+
/**
|
|
21204
|
+
* Download one recording a HomeBase 2 holds for an attached camera, then decode it
|
|
21205
|
+
* ({@link decodeRecording}). Downloads queue per station: the station streams one recording at a time
|
|
21206
|
+
* on the camera's channel, so a download starts only once the previous one has drained
|
|
21207
|
+
* ({@link RecordingTransfer.drained}), even when that one's caller already has its answer. An abort
|
|
21208
|
+
* while waiting rejects without sending anything; an abort mid-transfer rejects at once and the station's
|
|
21209
|
+
* turn is held until the transfer drains.
|
|
21210
|
+
*/
|
|
21211
|
+
async downloadRecording(sn, opts) {
|
|
21212
|
+
opts.signal?.throwIfAborted();
|
|
21213
|
+
const parentSn = this.stationKeyOf(sn);
|
|
21214
|
+
const previous = this.recordingDownloads.get(parentSn) ?? Promise.resolve();
|
|
21215
|
+
const started = this.waitTurn(previous, opts.signal).then(() => this.startRecordingDownload(sn, opts));
|
|
21216
|
+
const turn = Promise.allSettled([previous, started.then((t) => t.drained)]).then(() => {
|
|
21217
|
+
if (this.recordingDownloads.get(parentSn) === turn)
|
|
21218
|
+
this.recordingDownloads.delete(parentSn);
|
|
21219
|
+
});
|
|
21220
|
+
this.recordingDownloads.set(parentSn, turn);
|
|
21221
|
+
return started.then((t) => t.recording);
|
|
21222
|
+
}
|
|
21223
|
+
/** Resolve once `previous` settles, or reject as soon as `signal` aborts. */
|
|
21224
|
+
waitTurn(previous, signal) {
|
|
21225
|
+
return abortable(previous.then(() => void 0, () => void 0), signal);
|
|
21226
|
+
}
|
|
21227
|
+
/**
|
|
21228
|
+
* Start one recording download, once the station's previous download has drained: the decoded recording,
|
|
21229
|
+
* and when the station is done sending it.
|
|
21230
|
+
*/
|
|
21231
|
+
async startRecordingDownload(sn, opts) {
|
|
21232
|
+
const { session, parentSn, channel, accountId } = await this.resolveSession(sn, { signal: opts.signal });
|
|
21233
|
+
const path = homeBase2RecordingPath(channel, opts.recording);
|
|
21234
|
+
if (!path)
|
|
21235
|
+
throw new RecordingDownloadError("invalid-recording", `no recording named ${opts.recording}`);
|
|
21236
|
+
const eccKey = await this.cipherKeyFor(session, opts.cipherId, accountId, parentSn);
|
|
21237
|
+
if (!eccKey) {
|
|
21238
|
+
throw new RecordingDownloadError("key-unavailable", `cipher ${opts.cipherId} could not be obtained`);
|
|
21239
|
+
}
|
|
21240
|
+
const transfer = receiveRecording(session, {
|
|
21241
|
+
path,
|
|
21242
|
+
accountId,
|
|
21243
|
+
channel,
|
|
21244
|
+
timeoutMs: opts.timeoutMs,
|
|
21245
|
+
signal: opts.signal
|
|
21246
|
+
});
|
|
21247
|
+
return { recording: transfer.frames.then((frames) => decodeRecording(frames, eccKey)), drained: transfer.drained };
|
|
21248
|
+
}
|
|
20782
21249
|
/**
|
|
20783
21250
|
* Open a {@link Talkback} on a device's camera channel.
|
|
20784
21251
|
*
|
|
@@ -21127,9 +21594,9 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
21127
21594
|
*
|
|
21128
21595
|
* Reached two ways, both idempotent: the session's own `close` event, when it died while still the
|
|
21129
21596
|
* station's registered session, and {@link SessionManagerOpts.onAutoClose}, when the manager closed it
|
|
21130
|
-
* unasked. A close a CALLER made is deliberately not routed here — {@link closeAll}
|
|
21131
|
-
* sources first,
|
|
21132
|
-
* on the replacement session.
|
|
21597
|
+
* unasked. A close a CALLER made is deliberately not routed here — {@link closeAll} and
|
|
21598
|
+
* {@link resolveSession} dispose their own sources first, while {@link replaceUnreachableSession} keeps
|
|
21599
|
+
* its source alive on purpose to rewarm it on the replacement session.
|
|
21133
21600
|
*/
|
|
21134
21601
|
tearDownStation(stationSn) {
|
|
21135
21602
|
this.manager.remove(stationSn);
|
|
@@ -21448,9 +21915,10 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
21448
21915
|
*
|
|
21449
21916
|
* A session whose {@link P2PSession.pathAnswering} is false is closed and re-resolved before it is handed
|
|
21450
21917
|
* over: the station answers every heartbeat, so a path silent past several of them is gone. A session
|
|
21451
|
-
* reporting nothing about its path is not reporting that evidence and is handed over as it is.
|
|
21452
|
-
*
|
|
21453
|
-
*
|
|
21918
|
+
* reporting nothing about its path is not reporting that evidence and is handed over as it is. Cached live
|
|
21919
|
+
* sources on the replaced session are discarded before closing it (see {@link tearDownStation}).
|
|
21920
|
+
* Replaced at most once per resolution, so a station whose replacement is silent too is returned rather
|
|
21921
|
+
* than closed again.
|
|
21454
21922
|
*/
|
|
21455
21923
|
async resolveSession(sn, opts = {}, rebuilt = false) {
|
|
21456
21924
|
const dev = await this.deviceFor(sn);
|
|
@@ -21464,6 +21932,10 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
21464
21932
|
}
|
|
21465
21933
|
if (session.pathAnswering === false && !rebuilt) {
|
|
21466
21934
|
(this.deps.logger ?? noopLogger).debug(`[p2p] ${parentSn} path stopped answering \u2014 rebuilding before use`);
|
|
21935
|
+
for (const [key, sessionKey] of [...this.liveSessionKeys]) {
|
|
21936
|
+
if (sessionKey === parentSn)
|
|
21937
|
+
this.dropLiveSource(key);
|
|
21938
|
+
}
|
|
21467
21939
|
await this.manager.close(parentSn).catch((error) => this.reportError(error instanceof Error ? error : new Error(String(error))));
|
|
21468
21940
|
return await this.resolveSession(sn, opts, true);
|
|
21469
21941
|
}
|
|
@@ -24304,6 +24776,7 @@ var MEDIA_FAILURE_REASONS = [
|
|
|
24304
24776
|
// dist/transport/stored-image-cache.js
|
|
24305
24777
|
var MAX_JPEG_BYTES = 10 * 1024 * 1024;
|
|
24306
24778
|
var MAX_REMEMBERED_URLS = 64;
|
|
24779
|
+
var NOT_YET_PUBLISHED_RETRY_MS = [2e3, 4e3];
|
|
24307
24780
|
var DIAGNOSTIC_INTERVAL_MS = 6e4;
|
|
24308
24781
|
var StoredImageCache = class {
|
|
24309
24782
|
downloader;
|
|
@@ -24341,6 +24814,8 @@ var StoredImageCache = class {
|
|
|
24341
24814
|
if (state.seenUrls.has(url))
|
|
24342
24815
|
return;
|
|
24343
24816
|
state.seenUrls.add(url);
|
|
24817
|
+
clearTimeout(state.retrying);
|
|
24818
|
+
state.retrying = void 0;
|
|
24344
24819
|
while (state.seenUrls.size > MAX_REMEMBERED_URLS) {
|
|
24345
24820
|
const oldest = state.seenUrls.values().next();
|
|
24346
24821
|
if (oldest.done)
|
|
@@ -24352,14 +24827,15 @@ var StoredImageCache = class {
|
|
|
24352
24827
|
url,
|
|
24353
24828
|
sequence: ++state.nextSequence,
|
|
24354
24829
|
observedAt: this.clock(),
|
|
24355
|
-
generation: this.generation
|
|
24830
|
+
generation: this.generation,
|
|
24831
|
+
retries: 0
|
|
24356
24832
|
};
|
|
24357
24833
|
this.pump();
|
|
24358
24834
|
}
|
|
24359
24835
|
/** Return retained bytes without starting or awaiting network work. */
|
|
24360
24836
|
snapshotStored(deviceKey) {
|
|
24361
24837
|
const state = this.devices.get(deviceKey);
|
|
24362
|
-
const reason = state?.queued || state?.inFlight ? "pending" : state?.reason ?? "not-observed";
|
|
24838
|
+
const reason = state?.queued || state?.inFlight || state?.retrying ? "pending" : state?.reason ?? "not-observed";
|
|
24363
24839
|
if (reason !== "pending" && state?.lifecycleError)
|
|
24364
24840
|
return Promise.reject(state.lifecycleError);
|
|
24365
24841
|
if (state?.retained)
|
|
@@ -24393,6 +24869,8 @@ var StoredImageCache = class {
|
|
|
24393
24869
|
if (error !== void 0 && this.isLifecycleError(error)) {
|
|
24394
24870
|
state.lifecycleError = error;
|
|
24395
24871
|
state.reason = void 0;
|
|
24872
|
+
} else if (image === void 0 && this.retryLater(state, candidate, error)) {
|
|
24873
|
+
state.lifecycleError = void 0;
|
|
24396
24874
|
} else if (image === void 0) {
|
|
24397
24875
|
state.lifecycleError = void 0;
|
|
24398
24876
|
state.reason = "download-failed";
|
|
@@ -24409,6 +24887,21 @@ var StoredImageCache = class {
|
|
|
24409
24887
|
}
|
|
24410
24888
|
this.pump();
|
|
24411
24889
|
}
|
|
24890
|
+
/** Schedule the next attempt after a 404; answers whether one was scheduled. */
|
|
24891
|
+
retryLater(state, candidate, error) {
|
|
24892
|
+
const delay = NOT_YET_PUBLISHED_RETRY_MS[candidate.retries];
|
|
24893
|
+
const tag2 = mediaFailureTag(error);
|
|
24894
|
+
if (delay === void 0 || tag2.cause !== "http-status" || tag2.status !== 404 || state.queued)
|
|
24895
|
+
return false;
|
|
24896
|
+
const timer = setTimeout(() => {
|
|
24897
|
+
state.retrying = void 0;
|
|
24898
|
+
state.queued = { ...candidate, retries: candidate.retries + 1 };
|
|
24899
|
+
this.pump();
|
|
24900
|
+
}, delay);
|
|
24901
|
+
timer.unref?.();
|
|
24902
|
+
state.retrying = timer;
|
|
24903
|
+
return true;
|
|
24904
|
+
}
|
|
24412
24905
|
isValidJpeg(image) {
|
|
24413
24906
|
return Buffer.isBuffer(image) && image.length > 0 && image.length <= MAX_JPEG_BYTES && image.length >= 5 && image[0] === 255 && image[1] === 216 && image[2] === 255 && image[image.length - 2] === 255 && image[image.length - 1] === 217;
|
|
24414
24907
|
}
|
|
@@ -25004,6 +25497,11 @@ function recordString(raw, key) {
|
|
|
25004
25497
|
const v = typeof nested === "string" && nested ? nested : raw[key];
|
|
25005
25498
|
return typeof v === "string" && v ? v : void 0;
|
|
25006
25499
|
}
|
|
25500
|
+
function adminUserIdFrom(raw) {
|
|
25501
|
+
const member = (raw ?? {}).member;
|
|
25502
|
+
const id = member?.admin_user_id;
|
|
25503
|
+
return typeof id === "string" && id ? id : void 0;
|
|
25504
|
+
}
|
|
25007
25505
|
function tuyaDevIdFrom(raw) {
|
|
25008
25506
|
for (const field of ["tuya_uuid", "tuya_virtual_id", "tuya_device_id", "virtualId"]) {
|
|
25009
25507
|
const v = recordString(raw, field);
|
|
@@ -25161,13 +25659,7 @@ var EufyMega = class extends EventEmitter9 {
|
|
|
25161
25659
|
onError: (e) => this.reportError(e),
|
|
25162
25660
|
// The `a2` account id an `eufy_life` DP frame embeds — the owning member's `admin_user_id`,
|
|
25163
25661
|
// falling back to the logged-in account's user id (the confirmed script path does the same).
|
|
25164
|
-
resolveAccountId: (dev) =>
|
|
25165
|
-
const member = (dev.raw ?? {}).member;
|
|
25166
|
-
const adminId = member?.admin_user_id;
|
|
25167
|
-
if (typeof adminId === "string" && adminId)
|
|
25168
|
-
return adminId;
|
|
25169
|
-
return this.mega.auth?.userId ?? "";
|
|
25170
|
-
},
|
|
25662
|
+
resolveAccountId: (dev) => adminUserIdFrom(dev.raw) ?? this.mega.auth?.userId ?? "",
|
|
25171
25663
|
// Fetch + parse a gallery effect from the HTTP catalogue into the serializable spec. The catalogue
|
|
25172
25664
|
// lives in the http layer (transport/http/light-catalog); the mqtt router owns the frame bytes.
|
|
25173
25665
|
resolvePreset: (presetId) => resolveLightEffect(this.mega, presetId),
|
|
@@ -26614,6 +27106,37 @@ var EufyMega = class extends EventEmitter9 {
|
|
|
26614
27106
|
getP2pSessions() {
|
|
26615
27107
|
return this.p2p.getSessions();
|
|
26616
27108
|
}
|
|
27109
|
+
/**
|
|
27110
|
+
* The people enrolled on a station, read from its own `person_basic_info` table over P2P.
|
|
27111
|
+
*
|
|
27112
|
+
* The cloud roster (`api.getFaces()`) answers empty for an account that keeps its faces on the
|
|
27113
|
+
* station, because they were never uploaded — the station holds them.
|
|
27114
|
+
*
|
|
27115
|
+
* Resolves with the rows the station returned. Rejects on a station that cannot be reached, on a
|
|
27116
|
+
* record stating no `admin_user_id`, and on the reply not arriving whole in time. Assembling that
|
|
27117
|
+
* reply is `P2PSession.readDatabase`'s; this resolves which station and which account id.
|
|
27118
|
+
*
|
|
27119
|
+
* `stranger<n>` names are the station's own placeholders for a face nobody has named, and are
|
|
27120
|
+
* returned as they arrive; a caller that wants only named people filters them.
|
|
27121
|
+
*/
|
|
27122
|
+
async getStationFaces(stationSn, opts = {}) {
|
|
27123
|
+
if (!this.registry.list().length)
|
|
27124
|
+
await this.getDevices();
|
|
27125
|
+
const station = this.p2p.stationKeyOf(stationSn);
|
|
27126
|
+
const accountId = this.adminUserIdOf(station);
|
|
27127
|
+
if (!accountId)
|
|
27128
|
+
throw new Error(`getStationFaces: no admin_user_id on record for ${station}`);
|
|
27129
|
+
await this.p2p.ensureStation(station, opts.signal);
|
|
27130
|
+
const session = this.p2p.getSessions().get(station);
|
|
27131
|
+
if (!session)
|
|
27132
|
+
throw new Error(`getStationFaces: no P2P session for ${station}`);
|
|
27133
|
+
const rows = await session.readDatabase("person_basic_info", { accountId, ...opts });
|
|
27134
|
+
return rows.filter((row) => typeof row === "object" && row !== null);
|
|
27135
|
+
}
|
|
27136
|
+
/** The station's own `admin_user_id`, which its database refuses the query without. */
|
|
27137
|
+
adminUserIdOf(stationSn) {
|
|
27138
|
+
return adminUserIdFrom(this.registry.list().find((d) => d.sn === stationSn)?.raw);
|
|
27139
|
+
}
|
|
26617
27140
|
/**
|
|
26618
27141
|
* The liveness facts for one device — see {@link DeviceState}. Facts, not an `online` verdict: "how
|
|
26619
27142
|
* long is too long" is a threshold that belongs to the caller, and it differs per device class.
|
|
@@ -26851,7 +27374,7 @@ var EufyMega = class extends EventEmitter9 {
|
|
|
26851
27374
|
if (ev.eventName === void 0 && ev.eventType != null)
|
|
26852
27375
|
ev.eventName = detectionName(ev.eventType);
|
|
26853
27376
|
if (ev.thumbnailCandidate)
|
|
26854
|
-
void this.observeStoredImage(ev.thumbnailCandidate);
|
|
27377
|
+
void this.observeStoredImage(ev.thumbnailCandidate, ev.deviceSn);
|
|
26855
27378
|
this.emit("push", ev);
|
|
26856
27379
|
const signal2 = {
|
|
26857
27380
|
source: "push",
|
|
@@ -26862,7 +27385,10 @@ var EufyMega = class extends EventEmitter9 {
|
|
|
26862
27385
|
thumbnailUrl: ev.thumbnailUrl,
|
|
26863
27386
|
payload: ev.payload
|
|
26864
27387
|
};
|
|
26865
|
-
|
|
27388
|
+
const caps = this.capsForEvent(ev.deviceSn);
|
|
27389
|
+
const decoded = decodeEvent(signal2, caps);
|
|
27390
|
+
this.opts.logger?.debug(`[push] in: eventType=${ev.eventType} device=${caps ? "known" : ev.deviceSn ? "unknown" : "none"} events=${decoded.length}`);
|
|
27391
|
+
for (const out of decoded) {
|
|
26866
27392
|
this.emitSemantic(out.event, out.payload, { edge: true, refresh: out.refresh });
|
|
26867
27393
|
const dsn = out.payload.deviceSn ?? ev.deviceSn;
|
|
26868
27394
|
if (dsn)
|
|
@@ -26900,9 +27426,16 @@ var EufyMega = class extends EventEmitter9 {
|
|
|
26900
27426
|
client.on("error", (e) => this.reportError(e));
|
|
26901
27427
|
return client;
|
|
26902
27428
|
}
|
|
26903
|
-
/**
|
|
26904
|
-
|
|
26905
|
-
|
|
27429
|
+
/**
|
|
27430
|
+
* Admit only exact, account-known devices with resolved snapshot evidence into the passive store; a
|
|
27431
|
+
* station-only candidate is admitted under the event's device serial.
|
|
27432
|
+
*/
|
|
27433
|
+
async observeStoredImage(candidate, eventDeviceSn) {
|
|
27434
|
+
if (!this.storedImages)
|
|
27435
|
+
return;
|
|
27436
|
+
const { attribution } = candidate;
|
|
27437
|
+
const deviceSn = attribution.kind === "device" ? attribution.deviceSn : attribution.kind === "station" ? eventDeviceSn : void 0;
|
|
27438
|
+
if (!deviceSn)
|
|
26906
27439
|
return;
|
|
26907
27440
|
const account = this.mega.auth?.userId;
|
|
26908
27441
|
if (!account)
|
|
@@ -26912,9 +27445,9 @@ var EufyMega = class extends EventEmitter9 {
|
|
|
26912
27445
|
await this.registry.getDevices();
|
|
26913
27446
|
if (this.mega.auth?.userId !== account)
|
|
26914
27447
|
return;
|
|
26915
|
-
const caps = this.registry.capabilitiesForDevice(
|
|
27448
|
+
const caps = this.registry.capabilitiesForDevice(deviceSn);
|
|
26916
27449
|
if (caps && hasProvidedAction(caps, "snapshotStored")) {
|
|
26917
|
-
this.storedImages.observe(
|
|
27450
|
+
this.storedImages.observe(deviceSn, candidate.url);
|
|
26918
27451
|
}
|
|
26919
27452
|
} catch (e) {
|
|
26920
27453
|
this.reportError(e);
|
|
@@ -27139,26 +27672,16 @@ var SolixClient = class _SolixClient {
|
|
|
27139
27672
|
}
|
|
27140
27673
|
/** Build the encrypted, signed `/passport/login` request body + headers for the negotiated key. */
|
|
27141
27674
|
async postLogin(kx, verifyCode, limitedToken) {
|
|
27142
|
-
const { clientPublicKeyHex, encryptedPassword } = encryptLoginPassword(this.password);
|
|
27143
27675
|
const bodyObj = {
|
|
27144
|
-
email
|
|
27145
|
-
password: encryptedPassword,
|
|
27146
|
-
ab: this.country,
|
|
27147
|
-
client_secret_info: { public_key: clientPublicKeyHex },
|
|
27676
|
+
...loginCredentials(this.email, this.password, this.country),
|
|
27148
27677
|
answer: "",
|
|
27149
27678
|
captcha_id: "",
|
|
27150
27679
|
verify_code: verifyCode ?? "",
|
|
27151
27680
|
login_id: ""
|
|
27152
27681
|
};
|
|
27153
27682
|
const encBody = encryptBody(JSON.stringify(bodyObj), kx.shareKey);
|
|
27154
|
-
const ts = nowSec();
|
|
27155
|
-
const once = genId();
|
|
27156
27683
|
return this.post(this.apiHost, SOLIX_ENDPOINTS.login, encBody, this.authHeaders({
|
|
27157
|
-
|
|
27158
|
-
"x-key-ident": kx.keyIdent,
|
|
27159
|
-
"x-request-ts": ts,
|
|
27160
|
-
"x-request-once": once,
|
|
27161
|
-
"x-signature": signRequest(kx.shareKey, ts, once, encBody),
|
|
27684
|
+
...signedHeaders(kx, encBody),
|
|
27162
27685
|
...limitedToken ? { "x-auth-token": limitedToken } : {}
|
|
27163
27686
|
}));
|
|
27164
27687
|
}
|
|
@@ -27173,13 +27696,12 @@ var SolixClient = class _SolixClient {
|
|
|
27173
27696
|
* observed refusing the header, which is consistent with the two ids agreeing on the accounts seen.
|
|
27174
27697
|
*/
|
|
27175
27698
|
classifyLogin(data, isVerify) {
|
|
27176
|
-
const
|
|
27177
|
-
|
|
27178
|
-
if (!userId || !authToken)
|
|
27699
|
+
const reply = readLoginReply(data);
|
|
27700
|
+
if (!reply)
|
|
27179
27701
|
throw new Error(`Solix login returned no session: ${JSON.stringify(data).slice(0, 160)}`);
|
|
27180
|
-
const
|
|
27181
|
-
if (!isVerify &&
|
|
27182
|
-
this.pending2fa = { limitedToken: authToken, userId, geoKey:
|
|
27702
|
+
const { userId, authToken } = reply;
|
|
27703
|
+
if (!isVerify && reply.twoFactorPending) {
|
|
27704
|
+
this.pending2fa = { limitedToken: authToken, userId, geoKey: reply.geoKey };
|
|
27183
27705
|
return { status: "2fa", method: "code sent by the passport" };
|
|
27184
27706
|
}
|
|
27185
27707
|
this.pending2fa = void 0;
|
|
@@ -27188,7 +27710,7 @@ var SolixClient = class _SolixClient {
|
|
|
27188
27710
|
userId,
|
|
27189
27711
|
gtoken: gtoken(userId),
|
|
27190
27712
|
apiHost: this.apiHost,
|
|
27191
|
-
tokenExpiresAt:
|
|
27713
|
+
tokenExpiresAt: reply.tokenExpiresAt
|
|
27192
27714
|
};
|
|
27193
27715
|
this.persist();
|
|
27194
27716
|
return { status: "ok", session: this.session_ };
|
|
@@ -27343,14 +27865,8 @@ var SolixClient = class _SolixClient {
|
|
|
27343
27865
|
throw new Error("not authenticated \u2014 call login() first");
|
|
27344
27866
|
const kx = await this.keyExchange();
|
|
27345
27867
|
const encBody = encryptBody(JSON.stringify(payload), kx.shareKey);
|
|
27346
|
-
const ts = nowSec();
|
|
27347
|
-
const once = genId();
|
|
27348
27868
|
const env = await this.post(this.apiHost, path, encBody, this.baseHeaders({
|
|
27349
|
-
|
|
27350
|
-
"x-key-ident": kx.keyIdent,
|
|
27351
|
-
"x-request-ts": ts,
|
|
27352
|
-
"x-request-once": once,
|
|
27353
|
-
"x-signature": signRequest(kx.shareKey, ts, once, encBody),
|
|
27869
|
+
...signedHeaders(kx, encBody),
|
|
27354
27870
|
"x-auth-token": this.session_.authToken,
|
|
27355
27871
|
gtoken: this.session_.gtoken
|
|
27356
27872
|
}));
|
|
@@ -28118,6 +28634,7 @@ export {
|
|
|
28118
28634
|
ROOM_SUCTIONS,
|
|
28119
28635
|
RTSP_MEMBERS,
|
|
28120
28636
|
RawDpWriter,
|
|
28637
|
+
RecordingDownloadError,
|
|
28121
28638
|
RecordingQuality,
|
|
28122
28639
|
RtspRecordingMode,
|
|
28123
28640
|
SCENE_INVALID_REASONS,
|
|
@@ -28161,6 +28678,7 @@ export {
|
|
|
28161
28678
|
VACUUM_DOCK_MEMBERS,
|
|
28162
28679
|
VACUUM_SCHEDULE_ACTIONS,
|
|
28163
28680
|
VACUUM_SCHEDULE_WEEKDAYS,
|
|
28681
|
+
VIDEO_GCM_AAD,
|
|
28164
28682
|
VacuumMapStore,
|
|
28165
28683
|
VideoFrameDecoder,
|
|
28166
28684
|
WORKING_MODE_MAPS,
|
|
@@ -28180,6 +28698,7 @@ export {
|
|
|
28180
28698
|
buildEventIndex,
|
|
28181
28699
|
buildModelIndex,
|
|
28182
28700
|
buildRealtimeInit,
|
|
28701
|
+
cameraPowerTier,
|
|
28183
28702
|
captureSnapshotFromShared,
|
|
28184
28703
|
cellAtPoint,
|
|
28185
28704
|
claimedParams,
|