@mega-yfue/eufy-sdk 0.4.0-beta.2 → 0.4.0-beta.21
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 +1204 -681
- 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/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/adts.d.ts +6 -0
- package/dist/transport/p2p/codec.d.ts +13 -0
- package/dist/transport/p2p/command-router.d.ts +29 -0
- package/dist/transport/p2p/p2p-session.d.ts +60 -13
- package/dist/transport/p2p/recording-download.d.ts +54 -0
- package/dist/transport/p2p/shared-live-source.d.ts +1 -1
- package/dist/transport/p2p/video.d.ts +2 -0
- package/dist/transport/stored-image-cache.d.ts +2 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1070,6 +1070,409 @@ function normalizePushImage(data, p2pDid) {
|
|
|
1070
1070
|
return decodeImageV1(data, p2pDid) ?? data;
|
|
1071
1071
|
}
|
|
1072
1072
|
|
|
1073
|
+
// dist/core/types.js
|
|
1074
|
+
var PRINTER_CATEGORY_RE = /ankermake|eufymake|fdm|3d_?print/i;
|
|
1075
|
+
function classifyDevice(raw) {
|
|
1076
|
+
const category = raw.category ?? "";
|
|
1077
|
+
const hasP2p = typeof raw.p2p_did === "string" && raw.p2p_did.length > 0;
|
|
1078
|
+
if (category === "eufy_security" || hasP2p)
|
|
1079
|
+
return { category, api: "mega", realtime: "p2p" };
|
|
1080
|
+
if (PRINTER_CATEGORY_RE.test(category))
|
|
1081
|
+
return { category, api: "ankermake", realtime: "ankermake-mqtt" };
|
|
1082
|
+
return { category, api: "mega", realtime: "smqtt" };
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// dist/core/contracts.js
|
|
1086
|
+
var StoredSnapshotUnavailableError = class extends Error {
|
|
1087
|
+
reason;
|
|
1088
|
+
constructor(reason, message, options) {
|
|
1089
|
+
super(message, options);
|
|
1090
|
+
this.reason = reason;
|
|
1091
|
+
this.name = "StoredSnapshotUnavailableError";
|
|
1092
|
+
}
|
|
1093
|
+
};
|
|
1094
|
+
var RETRYABLE_LIVE_SNAPSHOT_REASONS = [
|
|
1095
|
+
"no-keyframe",
|
|
1096
|
+
"source-failed",
|
|
1097
|
+
"undecodable-burst"
|
|
1098
|
+
];
|
|
1099
|
+
var LiveSnapshotUnavailableError = class extends Error {
|
|
1100
|
+
reason;
|
|
1101
|
+
/** Whether another attempt could plausibly succeed without the host changing anything. */
|
|
1102
|
+
retryable;
|
|
1103
|
+
constructor(reason, message, options) {
|
|
1104
|
+
super(message, options);
|
|
1105
|
+
this.reason = reason;
|
|
1106
|
+
this.name = "LiveSnapshotUnavailableError";
|
|
1107
|
+
this.retryable = RETRYABLE_LIVE_SNAPSHOT_REASONS.includes(reason);
|
|
1108
|
+
}
|
|
1109
|
+
};
|
|
1110
|
+
var CameraDisabledError = class extends Error {
|
|
1111
|
+
camera;
|
|
1112
|
+
constructor(camera, options) {
|
|
1113
|
+
super(`camera${camera ? ` ${camera}` : ""} is disabled \u2014 no live stream possible`, options);
|
|
1114
|
+
this.camera = camera;
|
|
1115
|
+
this.name = "CameraDisabledError";
|
|
1116
|
+
}
|
|
1117
|
+
};
|
|
1118
|
+
var StationKeyUnavailableError = class extends Error {
|
|
1119
|
+
stationSn;
|
|
1120
|
+
/** Always true: the negotiation is per connection, so a later one may still produce a key. */
|
|
1121
|
+
retryable = true;
|
|
1122
|
+
constructor(stationSn, options) {
|
|
1123
|
+
super(`station ${stationSn} did not provide its session key, so nothing that requires one could be sent`, options);
|
|
1124
|
+
this.stationSn = stationSn;
|
|
1125
|
+
this.name = "StationKeyUnavailableError";
|
|
1126
|
+
}
|
|
1127
|
+
};
|
|
1128
|
+
var StationUnreachableError = class extends Error {
|
|
1129
|
+
stationSn;
|
|
1130
|
+
waitedMs;
|
|
1131
|
+
/** Always true: a station unreachable now may answer on a later attempt. */
|
|
1132
|
+
retryable = true;
|
|
1133
|
+
constructor(stationSn, waitedMs, options) {
|
|
1134
|
+
super(`station ${stationSn}'s P2P session did not connect within ${waitedMs}ms, so nothing could be sent to it`, options);
|
|
1135
|
+
this.stationSn = stationSn;
|
|
1136
|
+
this.waitedMs = waitedMs;
|
|
1137
|
+
this.name = "StationUnreachableError";
|
|
1138
|
+
}
|
|
1139
|
+
};
|
|
1140
|
+
var DeviceChannelUnresolvedError = class extends Error {
|
|
1141
|
+
sn;
|
|
1142
|
+
stationSn;
|
|
1143
|
+
constructor(sn, stationSn, options) {
|
|
1144
|
+
super(`${sn} has no usable channel on station ${stationSn}, so nothing was sent to it`, options);
|
|
1145
|
+
this.sn = sn;
|
|
1146
|
+
this.stationSn = stationSn;
|
|
1147
|
+
this.name = "DeviceChannelUnresolvedError";
|
|
1148
|
+
}
|
|
1149
|
+
};
|
|
1150
|
+
var RecordingDownloadError = class extends Error {
|
|
1151
|
+
reason;
|
|
1152
|
+
constructor(reason, message, options) {
|
|
1153
|
+
super(message, options);
|
|
1154
|
+
this.reason = reason;
|
|
1155
|
+
this.name = "RecordingDownloadError";
|
|
1156
|
+
}
|
|
1157
|
+
};
|
|
1158
|
+
var LiveStreamStartError = class extends Error {
|
|
1159
|
+
reason;
|
|
1160
|
+
stage;
|
|
1161
|
+
timeoutMs;
|
|
1162
|
+
attempts;
|
|
1163
|
+
constructor(failure) {
|
|
1164
|
+
super(`live stream failed to start (${failure.reason} at ${failure.stage} after ${failure.attempts} attempts; ${failure.timeoutMs}ms deadline)`, failure.cause === void 0 ? void 0 : { cause: failure.cause });
|
|
1165
|
+
this.name = "LiveStreamStartError";
|
|
1166
|
+
this.reason = failure.reason;
|
|
1167
|
+
this.stage = failure.stage;
|
|
1168
|
+
this.timeoutMs = failure.timeoutMs;
|
|
1169
|
+
this.attempts = failure.attempts;
|
|
1170
|
+
}
|
|
1171
|
+
};
|
|
1172
|
+
var StateConvergenceError = class extends Error {
|
|
1173
|
+
sn;
|
|
1174
|
+
property;
|
|
1175
|
+
param;
|
|
1176
|
+
expected;
|
|
1177
|
+
observed;
|
|
1178
|
+
timeoutMs;
|
|
1179
|
+
constructor(failure) {
|
|
1180
|
+
super(`device ${failure.sn} did not report ${failure.property} as ${String(failure.expected)} within ${failure.timeoutMs}ms (param ${failure.param} read ${String(failure.observed)})`);
|
|
1181
|
+
this.name = "StateConvergenceError";
|
|
1182
|
+
this.sn = failure.sn;
|
|
1183
|
+
this.property = failure.property;
|
|
1184
|
+
this.param = failure.param;
|
|
1185
|
+
this.expected = failure.expected;
|
|
1186
|
+
this.observed = failure.observed;
|
|
1187
|
+
this.timeoutMs = failure.timeoutMs;
|
|
1188
|
+
}
|
|
1189
|
+
};
|
|
1190
|
+
var COMMAND_OBSERVATION = /* @__PURE__ */ Symbol("command-observation");
|
|
1191
|
+
function observeCommand(command, observation) {
|
|
1192
|
+
return Object.defineProperty(command, COMMAND_OBSERVATION, { configurable: true, value: observation });
|
|
1193
|
+
}
|
|
1194
|
+
function commandObservation(command) {
|
|
1195
|
+
return command[COMMAND_OBSERVATION];
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// dist/core/raw-dp-writer.js
|
|
1199
|
+
var WIRE = { VARINT: 0, LENGTH_DELIMITED: 2 };
|
|
1200
|
+
function encodeVarint(value) {
|
|
1201
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
1202
|
+
throw new Error(`varint: ${value} is not a non-negative safe integer`);
|
|
1203
|
+
}
|
|
1204
|
+
const out = [];
|
|
1205
|
+
let v = value;
|
|
1206
|
+
while (v > 127) {
|
|
1207
|
+
out.push(v % 128 + 128);
|
|
1208
|
+
v = Math.floor(v / 128);
|
|
1209
|
+
}
|
|
1210
|
+
out.push(v);
|
|
1211
|
+
return out;
|
|
1212
|
+
}
|
|
1213
|
+
function zigzag(value) {
|
|
1214
|
+
if (!Number.isSafeInteger(value))
|
|
1215
|
+
throw new Error(`zigzag: ${value} is not a safe integer`);
|
|
1216
|
+
return value < 0 ? -2 * value - 1 : 2 * value;
|
|
1217
|
+
}
|
|
1218
|
+
var RawDpWriter = class _RawDpWriter {
|
|
1219
|
+
body = [];
|
|
1220
|
+
/** A varint-valued field (`int32`, `uint32`, `bool`, an enum member). */
|
|
1221
|
+
int(field, value) {
|
|
1222
|
+
this.body.push(...encodeVarint(tag(field, WIRE.VARINT)), ...encodeVarint(value));
|
|
1223
|
+
return this;
|
|
1224
|
+
}
|
|
1225
|
+
/** A signed varint field (`sint32`, `sint64`), ZigZag-encoded — see {@link zigzag}. */
|
|
1226
|
+
sint(field, value) {
|
|
1227
|
+
return this.int(field, zigzag(value));
|
|
1228
|
+
}
|
|
1229
|
+
/** A boolean field, on protobuf's own terms: `1` or `0` in a varint. */
|
|
1230
|
+
bool(field, value) {
|
|
1231
|
+
return this.int(field, value ? 1 : 0);
|
|
1232
|
+
}
|
|
1233
|
+
/** A length-delimited field carrying raw bytes — a string, a blob, or a message built elsewhere. */
|
|
1234
|
+
bytes(field, value) {
|
|
1235
|
+
const bytes2 = value instanceof Uint8Array ? Array.from(value) : [...value];
|
|
1236
|
+
this.body.push(...encodeVarint(tag(field, WIRE.LENGTH_DELIMITED)), ...encodeVarint(bytes2.length), ...bytes2);
|
|
1237
|
+
return this;
|
|
1238
|
+
}
|
|
1239
|
+
/**
|
|
1240
|
+
* A nested sub-message, built by `build` into a writer of its own.
|
|
1241
|
+
*
|
|
1242
|
+
* The sub-message is emitted even when `build` writes nothing into it. That is not an oversight: an
|
|
1243
|
+
* EMPTY sub-message is how these protocols say "this subsystem exists and is in its zero state", as
|
|
1244
|
+
* opposed to saying nothing about it, and the readers on the other side of this file depend on the
|
|
1245
|
+
* distinction. Omitting the call is what says nothing about it.
|
|
1246
|
+
*/
|
|
1247
|
+
sub(field, build) {
|
|
1248
|
+
const inner = new _RawDpWriter();
|
|
1249
|
+
build(inner);
|
|
1250
|
+
return this.bytes(field, inner.toBytes());
|
|
1251
|
+
}
|
|
1252
|
+
/** The message body on its own, unframed — for nesting into another writer. */
|
|
1253
|
+
toBytes() {
|
|
1254
|
+
return [...this.body];
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1257
|
+
* The finished DP value: `varint(bodyLength) ++ body`, base64-encoded.
|
|
1258
|
+
*
|
|
1259
|
+
* This is the envelope `RawDpCodec.decode` unwraps, so a value produced here and read back through the
|
|
1260
|
+
* codec is a genuine round-trip rather than two implementations of the same guess.
|
|
1261
|
+
*/
|
|
1262
|
+
finish() {
|
|
1263
|
+
return Buffer.from([...encodeVarint(this.body.length), ...this.body]).toString("base64");
|
|
1264
|
+
}
|
|
1265
|
+
};
|
|
1266
|
+
function tag(field, wire) {
|
|
1267
|
+
if (!Number.isSafeInteger(field) || field < 1)
|
|
1268
|
+
throw new Error(`field number: ${field} is not a positive integer`);
|
|
1269
|
+
return field * 8 + wire;
|
|
1270
|
+
}
|
|
1271
|
+
function rawDp(build) {
|
|
1272
|
+
const w = new RawDpWriter();
|
|
1273
|
+
build(w);
|
|
1274
|
+
return w.finish();
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
// dist/core/raw-dp-hex.js
|
|
1278
|
+
function hexToRawDp(hex) {
|
|
1279
|
+
if (hex.length === 0 || hex.length % 2 !== 0 || !/^[0-9a-fA-F]+$/.test(hex))
|
|
1280
|
+
return void 0;
|
|
1281
|
+
return Buffer.from(hex, "hex").toString("base64");
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
// dist/core/util.js
|
|
1285
|
+
var Timer = class {
|
|
1286
|
+
handle;
|
|
1287
|
+
/** (Re)arm to run `fn` after `ms`, cancelling any already-pending fire. */
|
|
1288
|
+
arm(ms, fn) {
|
|
1289
|
+
this.cancel();
|
|
1290
|
+
this.handle = setTimeout(() => {
|
|
1291
|
+
this.handle = void 0;
|
|
1292
|
+
fn();
|
|
1293
|
+
}, ms);
|
|
1294
|
+
this.handle.unref?.();
|
|
1295
|
+
}
|
|
1296
|
+
/** Cancel a pending fire. No-op if not armed. */
|
|
1297
|
+
cancel() {
|
|
1298
|
+
if (this.handle) {
|
|
1299
|
+
clearTimeout(this.handle);
|
|
1300
|
+
this.handle = void 0;
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
/** Whether a fire is currently scheduled. */
|
|
1304
|
+
get pending() {
|
|
1305
|
+
return this.handle !== void 0;
|
|
1306
|
+
}
|
|
1307
|
+
};
|
|
1308
|
+
function assertNever(value) {
|
|
1309
|
+
throw new Error(`unhandled variant: ${JSON.stringify(value)}`);
|
|
1310
|
+
}
|
|
1311
|
+
function asBool(value) {
|
|
1312
|
+
if (typeof value === "string")
|
|
1313
|
+
return value === "1" || value.toLowerCase() === "true";
|
|
1314
|
+
return value === true || value === 1;
|
|
1315
|
+
}
|
|
1316
|
+
function clamp(value, min, max) {
|
|
1317
|
+
if (Number.isNaN(value))
|
|
1318
|
+
return min;
|
|
1319
|
+
return Math.max(min, Math.min(max, Math.round(value)));
|
|
1320
|
+
}
|
|
1321
|
+
function structuralEqual(a, b) {
|
|
1322
|
+
if (Object.is(a, b))
|
|
1323
|
+
return true;
|
|
1324
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null)
|
|
1325
|
+
return false;
|
|
1326
|
+
const aArr = Array.isArray(a);
|
|
1327
|
+
const bArr = Array.isArray(b);
|
|
1328
|
+
if (aArr !== bArr)
|
|
1329
|
+
return false;
|
|
1330
|
+
if (aArr && bArr) {
|
|
1331
|
+
if (a.length !== b.length)
|
|
1332
|
+
return false;
|
|
1333
|
+
for (let i = 0; i < a.length; i++)
|
|
1334
|
+
if (!structuralEqual(a[i], b[i]))
|
|
1335
|
+
return false;
|
|
1336
|
+
return true;
|
|
1337
|
+
}
|
|
1338
|
+
const aObj = a;
|
|
1339
|
+
const bObj = b;
|
|
1340
|
+
const aKeys = Object.keys(aObj);
|
|
1341
|
+
const bKeys = Object.keys(bObj);
|
|
1342
|
+
if (aKeys.length !== bKeys.length)
|
|
1343
|
+
return false;
|
|
1344
|
+
for (const k of aKeys) {
|
|
1345
|
+
if (!Object.prototype.hasOwnProperty.call(bObj, k))
|
|
1346
|
+
return false;
|
|
1347
|
+
if (!structuralEqual(aObj[k], bObj[k]))
|
|
1348
|
+
return false;
|
|
1349
|
+
}
|
|
1350
|
+
return true;
|
|
1351
|
+
}
|
|
1352
|
+
function coerceEnumValue(enumObj, value) {
|
|
1353
|
+
const n = Number(value);
|
|
1354
|
+
return Number.isInteger(n) && Object.values(enumObj).includes(n) ? n : void 0;
|
|
1355
|
+
}
|
|
1356
|
+
function enumValues(enumObj) {
|
|
1357
|
+
return Object.values(enumObj).join("/");
|
|
1358
|
+
}
|
|
1359
|
+
function enumLabels(enumObj) {
|
|
1360
|
+
return Object.fromEntries(Object.entries(enumObj).map(([name, value]) => [value, name]));
|
|
1361
|
+
}
|
|
1362
|
+
function jsonObject(text2) {
|
|
1363
|
+
if (typeof text2 !== "string")
|
|
1364
|
+
return void 0;
|
|
1365
|
+
try {
|
|
1366
|
+
const v = JSON.parse(text2);
|
|
1367
|
+
return v && typeof v === "object" && !Array.isArray(v) ? v : void 0;
|
|
1368
|
+
} catch {
|
|
1369
|
+
return void 0;
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
function u16le(n) {
|
|
1373
|
+
const b = Buffer.alloc(2);
|
|
1374
|
+
b.writeUInt16LE(n & 65535);
|
|
1375
|
+
return b;
|
|
1376
|
+
}
|
|
1377
|
+
function u16be(n) {
|
|
1378
|
+
const b = Buffer.alloc(2);
|
|
1379
|
+
b.writeUInt16BE(n & 65535);
|
|
1380
|
+
return b;
|
|
1381
|
+
}
|
|
1382
|
+
function u32le(n) {
|
|
1383
|
+
const b = Buffer.alloc(4);
|
|
1384
|
+
b.writeUInt32LE(n >>> 0);
|
|
1385
|
+
return b;
|
|
1386
|
+
}
|
|
1387
|
+
function u32be(n) {
|
|
1388
|
+
const b = Buffer.alloc(4);
|
|
1389
|
+
b.writeUInt32BE(n >>> 0);
|
|
1390
|
+
return b;
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
// dist/core/lz4-block.js
|
|
1394
|
+
var MAX_OUTPUT = 64 * 1024 * 1024;
|
|
1395
|
+
function lz4BlockDecompress(block, size) {
|
|
1396
|
+
if (!Number.isSafeInteger(size) || size < 0 || size > MAX_OUTPUT)
|
|
1397
|
+
return void 0;
|
|
1398
|
+
const out = Buffer.alloc(size);
|
|
1399
|
+
const end = block.length;
|
|
1400
|
+
let pos = 0;
|
|
1401
|
+
let written = 0;
|
|
1402
|
+
const extend2 = (base) => {
|
|
1403
|
+
let len = base;
|
|
1404
|
+
if (base !== 15)
|
|
1405
|
+
return len;
|
|
1406
|
+
for (; ; ) {
|
|
1407
|
+
if (pos >= end)
|
|
1408
|
+
return void 0;
|
|
1409
|
+
const extra = block[pos++];
|
|
1410
|
+
len += extra;
|
|
1411
|
+
if (extra !== 255)
|
|
1412
|
+
return len;
|
|
1413
|
+
}
|
|
1414
|
+
};
|
|
1415
|
+
while (pos < end) {
|
|
1416
|
+
const token = block[pos++];
|
|
1417
|
+
const literals = extend2(token >> 4);
|
|
1418
|
+
if (literals === void 0)
|
|
1419
|
+
return void 0;
|
|
1420
|
+
if (pos + literals > end || written + literals > size)
|
|
1421
|
+
return void 0;
|
|
1422
|
+
for (let i = 0; i < literals; i++)
|
|
1423
|
+
out[written++] = block[pos++];
|
|
1424
|
+
if (pos === end)
|
|
1425
|
+
break;
|
|
1426
|
+
if (pos + 2 > end)
|
|
1427
|
+
return void 0;
|
|
1428
|
+
const offset = block[pos] | block[pos + 1] << 8;
|
|
1429
|
+
pos += 2;
|
|
1430
|
+
if (offset === 0 || offset > written)
|
|
1431
|
+
return void 0;
|
|
1432
|
+
const match = extend2(token & 15);
|
|
1433
|
+
if (match === void 0)
|
|
1434
|
+
return void 0;
|
|
1435
|
+
const length = match + 4;
|
|
1436
|
+
if (written + length > size)
|
|
1437
|
+
return void 0;
|
|
1438
|
+
let from = written - offset;
|
|
1439
|
+
for (let i = 0; i < length; i++)
|
|
1440
|
+
out[written++] = out[from++];
|
|
1441
|
+
}
|
|
1442
|
+
return written === size ? out : void 0;
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
// dist/transport/http/passport.js
|
|
1446
|
+
function loginCredentials(email, password, country) {
|
|
1447
|
+
const { clientPublicKeyHex, encryptedPassword } = encryptLoginPassword(password);
|
|
1448
|
+
return { email, password: encryptedPassword, ab: country, client_secret_info: { public_key: clientPublicKeyHex } };
|
|
1449
|
+
}
|
|
1450
|
+
function readLoginReply(data) {
|
|
1451
|
+
const userId = data.ap_cloud_user_id ?? data.user_id ?? data.userId;
|
|
1452
|
+
const authToken = data.auth_token ?? data.token;
|
|
1453
|
+
if (!userId || !authToken)
|
|
1454
|
+
return void 0;
|
|
1455
|
+
return {
|
|
1456
|
+
userId,
|
|
1457
|
+
accountUserId: data.user_id ?? data.userId,
|
|
1458
|
+
authToken,
|
|
1459
|
+
geoKey: data.geo_key,
|
|
1460
|
+
tokenExpiresAt: Number(data.token_expires_at ?? 0) || 0,
|
|
1461
|
+
twoFactorPending: !!(data.fa_info ?? {}).info
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
function signedHeaders(entry, encBody) {
|
|
1465
|
+
const ts = nowSec();
|
|
1466
|
+
const once = genId();
|
|
1467
|
+
return {
|
|
1468
|
+
"x-encryption-info": "algo_ecdh",
|
|
1469
|
+
"x-key-ident": entry.keyIdent,
|
|
1470
|
+
"x-request-ts": ts,
|
|
1471
|
+
"x-request-once": once,
|
|
1472
|
+
"x-signature": signRequest(entry.shareKey, ts, once, encBody)
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1073
1476
|
// dist/transport/http/mega-client.js
|
|
1074
1477
|
function parseMaybeJson(text2) {
|
|
1075
1478
|
if (text2.length === 0)
|
|
@@ -1437,17 +1840,11 @@ ${this.auth_?.authToken ?? ""}`;
|
|
|
1437
1840
|
const order = preferred === "text/plain" ? ["text/plain", "application/json"] : ["application/json", "text/plain"];
|
|
1438
1841
|
let last;
|
|
1439
1842
|
for (const contentType of order) {
|
|
1440
|
-
const ts = nowSec();
|
|
1441
|
-
const once = genId();
|
|
1442
1843
|
const headers = {
|
|
1443
1844
|
...this.baseHeaders(),
|
|
1444
1845
|
"content-type": contentType,
|
|
1445
|
-
|
|
1846
|
+
...signedHeaders(entry, encBody),
|
|
1446
1847
|
"x-replay-info": "replay",
|
|
1447
|
-
"x-key-ident": entry.keyIdent,
|
|
1448
|
-
"x-request-ts": ts,
|
|
1449
|
-
"x-request-once": once,
|
|
1450
|
-
"x-signature": signRequest(entry.shareKey, ts, once, encBody),
|
|
1451
1848
|
// Per-call header overrides (e.g. app-name=eufy_security to request a security-scoped
|
|
1452
1849
|
// MQTT cert on an existing eufy_mega session — see EufyMega.getUserMqttInfo).
|
|
1453
1850
|
...headerOverrides
|
|
@@ -1746,13 +2143,7 @@ ${this.auth_?.authToken ?? ""}`;
|
|
|
1746
2143
|
}
|
|
1747
2144
|
/** Build the /passport/login body (verify_code empty unless 2FA). */
|
|
1748
2145
|
loginBody(opts) {
|
|
1749
|
-
const
|
|
1750
|
-
const base = {
|
|
1751
|
-
email: this.cfg.email,
|
|
1752
|
-
password: encryptedPassword,
|
|
1753
|
-
ab: this.cfg.countryCode,
|
|
1754
|
-
client_secret_info: { public_key: clientPublicKeyHex }
|
|
1755
|
-
};
|
|
2146
|
+
const base = loginCredentials(this.cfg.email, this.cfg.password, this.cfg.countryCode);
|
|
1756
2147
|
if (opts.verifyCode)
|
|
1757
2148
|
return { ...base, verify_code: opts.verifyCode, login_id: "" };
|
|
1758
2149
|
if (opts.captchaId)
|
|
@@ -1990,15 +2381,12 @@ ${this.auth_?.authToken ?? ""}`;
|
|
|
1990
2381
|
if (cap.length)
|
|
1991
2382
|
this.logger.debug("[mega] captcha/fa:", JSON.stringify(Object.fromEntries(cap.map((k) => [k, res[k]]))));
|
|
1992
2383
|
}
|
|
1993
|
-
const
|
|
1994
|
-
|
|
1995
|
-
const authToken = res.auth_token ?? res.token;
|
|
1996
|
-
if (!userId || !authToken)
|
|
2384
|
+
const reply = readLoginReply(res);
|
|
2385
|
+
if (!reply)
|
|
1997
2386
|
throw new Error(`login returned no session: ${JSON.stringify(res).slice(0, 200)}`);
|
|
1998
|
-
const
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
this.auth_ = { userId, accountUserId, authToken, geoKey: res.geo_key };
|
|
2387
|
+
const { userId, accountUserId, authToken, geoKey } = reply;
|
|
2388
|
+
if (!isVerify && reply.twoFactorPending) {
|
|
2389
|
+
this.auth_ = { userId, accountUserId, authToken, geoKey };
|
|
2002
2390
|
this.pendingCaptchaId = void 0;
|
|
2003
2391
|
this.pending2fa = true;
|
|
2004
2392
|
await this.sendVerifyCode(messageType);
|
|
@@ -2006,8 +2394,8 @@ ${this.auth_?.authToken ?? ""}`;
|
|
|
2006
2394
|
}
|
|
2007
2395
|
this.pendingCaptchaId = void 0;
|
|
2008
2396
|
this.pending2fa = false;
|
|
2009
|
-
this.auth_ = { userId, accountUserId, authToken, geoKey
|
|
2010
|
-
this.tokenExpiresAt =
|
|
2397
|
+
this.auth_ = { userId, accountUserId, authToken, geoKey };
|
|
2398
|
+
this.tokenExpiresAt = reply.tokenExpiresAt;
|
|
2011
2399
|
this.sessionKey = void 0;
|
|
2012
2400
|
await this.ensureSessionKey();
|
|
2013
2401
|
this.persist();
|
|
@@ -2125,6 +2513,10 @@ function solixUserTopics(appName, userId) {
|
|
|
2125
2513
|
|
|
2126
2514
|
// dist/transport/mqtt/secure-mqtt.js
|
|
2127
2515
|
var SUBACK_FAILURE = 128;
|
|
2516
|
+
function isRefusal(err) {
|
|
2517
|
+
const granted = err?.packet?.granted;
|
|
2518
|
+
return Array.isArray(granted) && granted.some((g) => typeof g === "number" && (g & SUBACK_FAILURE) !== 0);
|
|
2519
|
+
}
|
|
2128
2520
|
function isNotAuthorized(err) {
|
|
2129
2521
|
return err instanceof Error && /connection refused/i.test(err.message) && /not authori[sz]ed/i.test(err.message);
|
|
2130
2522
|
}
|
|
@@ -2227,19 +2619,16 @@ var SecureMqtt = class extends EventEmitter {
|
|
|
2227
2619
|
}
|
|
2228
2620
|
/**
|
|
2229
2621
|
* Subscribe every inbound leg this device's line uses (see `topics.ts` — one `/res` for most lines,
|
|
2230
|
-
* four topics for `eufy_life`).
|
|
2622
|
+
* four topics for `eufy_life` and the clean line).
|
|
2231
2623
|
*
|
|
2232
|
-
* The grants are INSPECTED, not assumed: AWS IoT
|
|
2233
|
-
* SUBACK_FAILURE
|
|
2234
|
-
*
|
|
2235
|
-
*
|
|
2236
|
-
* line that grants its state channel but refuses (say) the OTA leg still works.
|
|
2624
|
+
* The grants are INSPECTED, not assumed: AWS IoT refuses a policy-denied filter with a
|
|
2625
|
+
* {@link SUBACK_FAILURE} grant rather than failing the connection. A denied topic is reported via
|
|
2626
|
+
* `error` naming the credential scope; only an all-denied device throws, so a line that grants its
|
|
2627
|
+
* state channel but refuses (say) a business leg still works.
|
|
2237
2628
|
*/
|
|
2238
2629
|
async subscribeDevice(device) {
|
|
2239
|
-
if (!this.client)
|
|
2240
|
-
throw new Error("SecureMqtt not connected");
|
|
2241
2630
|
const topics = [...subscribeTopics(device)];
|
|
2242
|
-
const { denied } =
|
|
2631
|
+
const { denied } = await this.subscribeEach(topics);
|
|
2243
2632
|
const scope = this.o.credentials.app_name ?? "default";
|
|
2244
2633
|
if (denied.length === topics.length) {
|
|
2245
2634
|
throw new Error(`subscribe ${device.sn}: every topic denied on credential scope "${scope}" \u2014 this line's topics are granted to a different scope (${denied.join(", ")})`);
|
|
@@ -2250,25 +2639,43 @@ var SecureMqtt = class extends EventEmitter {
|
|
|
2250
2639
|
}
|
|
2251
2640
|
/**
|
|
2252
2641
|
* Subscribe to explicit topic filters, returning the topics that were granted. A scope-denied filter
|
|
2253
|
-
*
|
|
2254
|
-
*
|
|
2255
|
-
*
|
|
2642
|
+
* is dropped from the result instead of throwing — callers that need every leg check the returned
|
|
2643
|
+
* list. Used by lines whose topic vocabulary isn't the eufy `subscribeTopics` shape (e.g. Anker Solix
|
|
2644
|
+
* `dt/{app}/{pn}/{sn}`).
|
|
2256
2645
|
*/
|
|
2257
2646
|
async subscribe(topics) {
|
|
2258
|
-
|
|
2259
|
-
throw new Error("SecureMqtt not connected");
|
|
2260
|
-
return this.partitionGrants(await this.client.subscribeAsync(topics, { qos: 1 })).granted;
|
|
2647
|
+
return (await this.subscribeEach(topics)).granted;
|
|
2261
2648
|
}
|
|
2262
2649
|
/**
|
|
2263
|
-
*
|
|
2264
|
-
*
|
|
2265
|
-
*
|
|
2650
|
+
* Subscribe each filter in a SUBSCRIBE of its own and split the outcome into granted vs refused.
|
|
2651
|
+
*
|
|
2652
|
+
* One request per filter because the MQTT engine treats a SUBACK as all-or-nothing: one refused grant
|
|
2653
|
+
* rejects the whole request, and it forgets EVERY filter of that request for resubscription after a
|
|
2654
|
+
* reconnect — so a batch holding one denied leg would lose the granted legs on the next drop. Alone,
|
|
2655
|
+
* a refused filter costs only itself. A rejection that carries no SUBACK is a transport failure and
|
|
2656
|
+
* is thrown.
|
|
2657
|
+
*
|
|
2658
|
+
* The cost is one SUBSCRIBE and one SUBACK per filter instead of one per device — four for a
|
|
2659
|
+
* four-leg line. They are sent concurrently, so the wall-clock cost is one round trip. Folding them
|
|
2660
|
+
* back into one request brings back the lost resubscription.
|
|
2266
2661
|
*/
|
|
2267
|
-
|
|
2662
|
+
async subscribeEach(topics) {
|
|
2663
|
+
const client = this.client;
|
|
2664
|
+
if (!client)
|
|
2665
|
+
throw new Error("SecureMqtt not connected");
|
|
2666
|
+
const outcomes = await Promise.allSettled(topics.map((topic) => client.subscribeAsync(topic, { qos: 1 })));
|
|
2268
2667
|
const granted = [];
|
|
2269
2668
|
const denied = [];
|
|
2270
|
-
|
|
2271
|
-
|
|
2669
|
+
outcomes.forEach((outcome, i) => {
|
|
2670
|
+
const topic = topics[i];
|
|
2671
|
+
if (outcome.status === "fulfilled") {
|
|
2672
|
+
granted.push(topic);
|
|
2673
|
+
return;
|
|
2674
|
+
}
|
|
2675
|
+
if (!isRefusal(outcome.reason))
|
|
2676
|
+
throw outcome.reason;
|
|
2677
|
+
denied.push(topic);
|
|
2678
|
+
});
|
|
2272
2679
|
return { granted, denied };
|
|
2273
2680
|
}
|
|
2274
2681
|
/**
|
|
@@ -2292,125 +2699,16 @@ var SecureMqtt = class extends EventEmitter {
|
|
|
2292
2699
|
await this.client?.endAsync(true);
|
|
2293
2700
|
this.client = void 0;
|
|
2294
2701
|
}
|
|
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
|
+
};
|
|
2703
|
+
|
|
2704
|
+
// dist/transport/mqtt/app-client-id.js
|
|
2705
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
2706
|
+
function buildAppShapedClientId(input) {
|
|
2707
|
+
const ts = input.timestamp ?? Math.floor(Date.now() / 1e3);
|
|
2708
|
+
return `android-${input.appName}-${input.uid}-${input.mqttUuid}-${ts}`;
|
|
2409
2709
|
}
|
|
2410
|
-
function
|
|
2411
|
-
|
|
2412
|
-
b.writeUInt32BE(n >>> 0);
|
|
2413
|
-
return b;
|
|
2710
|
+
function mqttUuidFrom(installId) {
|
|
2711
|
+
return createHash4("sha256").update(installId).digest("hex").slice(0, 16);
|
|
2414
2712
|
}
|
|
2415
2713
|
|
|
2416
2714
|
// dist/transport/mqtt/availability.js
|
|
@@ -2537,13 +2835,6 @@ function parseAiotDpReport(raw) {
|
|
|
2537
2835
|
return Object.keys(out).length ? out : void 0;
|
|
2538
2836
|
}
|
|
2539
2837
|
|
|
2540
|
-
// dist/core/raw-dp-hex.js
|
|
2541
|
-
function hexToRawDp(hex) {
|
|
2542
|
-
if (hex.length === 0 || hex.length % 2 !== 0 || !/^[0-9a-fA-F]+$/.test(hex))
|
|
2543
|
-
return void 0;
|
|
2544
|
-
return Buffer.from(hex, "hex").toString("base64");
|
|
2545
|
-
}
|
|
2546
|
-
|
|
2547
2838
|
// dist/transport/mqtt/biz-stream.js
|
|
2548
2839
|
var BIZ_PROTOCOL = {
|
|
2549
2840
|
/** Map-stream data: the frames this module parses. */
|
|
@@ -2727,18 +3018,6 @@ var DeviceType = {
|
|
|
2727
3018
|
CAMERA_C35: 10035
|
|
2728
3019
|
};
|
|
2729
3020
|
|
|
2730
|
-
// dist/core/types.js
|
|
2731
|
-
var PRINTER_CATEGORY_RE = /ankermake|eufymake|fdm|3d_?print/i;
|
|
2732
|
-
function classifyDevice(raw) {
|
|
2733
|
-
const category = raw.category ?? "";
|
|
2734
|
-
const hasP2p = typeof raw.p2p_did === "string" && raw.p2p_did.length > 0;
|
|
2735
|
-
if (category === "eufy_security" || hasP2p)
|
|
2736
|
-
return { category, api: "mega", realtime: "p2p" };
|
|
2737
|
-
if (PRINTER_CATEGORY_RE.test(category))
|
|
2738
|
-
return { category, api: "ankermake", realtime: "ankermake-mqtt" };
|
|
2739
|
-
return { category, api: "mega", realtime: "smqtt" };
|
|
2740
|
-
}
|
|
2741
|
-
|
|
2742
3021
|
// dist/model/classify.js
|
|
2743
3022
|
var STATION_TYPES = /* @__PURE__ */ new Set([
|
|
2744
3023
|
DeviceType.STATION,
|
|
@@ -2877,111 +3156,6 @@ function inferName(rec) {
|
|
|
2877
3156
|
return void 0;
|
|
2878
3157
|
}
|
|
2879
3158
|
|
|
2880
|
-
// dist/core/contracts.js
|
|
2881
|
-
var StoredSnapshotUnavailableError = class extends Error {
|
|
2882
|
-
reason;
|
|
2883
|
-
constructor(reason, message, options) {
|
|
2884
|
-
super(message, options);
|
|
2885
|
-
this.reason = reason;
|
|
2886
|
-
this.name = "StoredSnapshotUnavailableError";
|
|
2887
|
-
}
|
|
2888
|
-
};
|
|
2889
|
-
var RETRYABLE_LIVE_SNAPSHOT_REASONS = [
|
|
2890
|
-
"no-keyframe",
|
|
2891
|
-
"source-failed",
|
|
2892
|
-
"undecodable-burst"
|
|
2893
|
-
];
|
|
2894
|
-
var LiveSnapshotUnavailableError = class extends Error {
|
|
2895
|
-
reason;
|
|
2896
|
-
/** Whether another attempt could plausibly succeed without the host changing anything. */
|
|
2897
|
-
retryable;
|
|
2898
|
-
constructor(reason, message, options) {
|
|
2899
|
-
super(message, options);
|
|
2900
|
-
this.reason = reason;
|
|
2901
|
-
this.name = "LiveSnapshotUnavailableError";
|
|
2902
|
-
this.retryable = RETRYABLE_LIVE_SNAPSHOT_REASONS.includes(reason);
|
|
2903
|
-
}
|
|
2904
|
-
};
|
|
2905
|
-
var CameraDisabledError = class extends Error {
|
|
2906
|
-
camera;
|
|
2907
|
-
constructor(camera, options) {
|
|
2908
|
-
super(`camera${camera ? ` ${camera}` : ""} is disabled \u2014 no live stream possible`, options);
|
|
2909
|
-
this.camera = camera;
|
|
2910
|
-
this.name = "CameraDisabledError";
|
|
2911
|
-
}
|
|
2912
|
-
};
|
|
2913
|
-
var StationKeyUnavailableError = class extends Error {
|
|
2914
|
-
stationSn;
|
|
2915
|
-
/** Always true: the negotiation is per connection, so a later one may still produce a key. */
|
|
2916
|
-
retryable = true;
|
|
2917
|
-
constructor(stationSn, options) {
|
|
2918
|
-
super(`station ${stationSn} did not provide its session key, so nothing that requires one could be sent`, options);
|
|
2919
|
-
this.stationSn = stationSn;
|
|
2920
|
-
this.name = "StationKeyUnavailableError";
|
|
2921
|
-
}
|
|
2922
|
-
};
|
|
2923
|
-
var StationUnreachableError = class extends Error {
|
|
2924
|
-
stationSn;
|
|
2925
|
-
waitedMs;
|
|
2926
|
-
/** Always true: a station unreachable now may answer on a later attempt. */
|
|
2927
|
-
retryable = true;
|
|
2928
|
-
constructor(stationSn, waitedMs, options) {
|
|
2929
|
-
super(`station ${stationSn}'s P2P session did not connect within ${waitedMs}ms, so nothing could be sent to it`, options);
|
|
2930
|
-
this.stationSn = stationSn;
|
|
2931
|
-
this.waitedMs = waitedMs;
|
|
2932
|
-
this.name = "StationUnreachableError";
|
|
2933
|
-
}
|
|
2934
|
-
};
|
|
2935
|
-
var DeviceChannelUnresolvedError = class extends Error {
|
|
2936
|
-
sn;
|
|
2937
|
-
stationSn;
|
|
2938
|
-
constructor(sn, stationSn, options) {
|
|
2939
|
-
super(`${sn} has no usable channel on station ${stationSn}, so nothing was sent to it`, options);
|
|
2940
|
-
this.sn = sn;
|
|
2941
|
-
this.stationSn = stationSn;
|
|
2942
|
-
this.name = "DeviceChannelUnresolvedError";
|
|
2943
|
-
}
|
|
2944
|
-
};
|
|
2945
|
-
var LiveStreamStartError = class extends Error {
|
|
2946
|
-
reason;
|
|
2947
|
-
stage;
|
|
2948
|
-
timeoutMs;
|
|
2949
|
-
attempts;
|
|
2950
|
-
constructor(failure) {
|
|
2951
|
-
super(`live stream failed to start (${failure.reason} at ${failure.stage} after ${failure.attempts} attempts; ${failure.timeoutMs}ms deadline)`, failure.cause === void 0 ? void 0 : { cause: failure.cause });
|
|
2952
|
-
this.name = "LiveStreamStartError";
|
|
2953
|
-
this.reason = failure.reason;
|
|
2954
|
-
this.stage = failure.stage;
|
|
2955
|
-
this.timeoutMs = failure.timeoutMs;
|
|
2956
|
-
this.attempts = failure.attempts;
|
|
2957
|
-
}
|
|
2958
|
-
};
|
|
2959
|
-
var StateConvergenceError = class extends Error {
|
|
2960
|
-
sn;
|
|
2961
|
-
property;
|
|
2962
|
-
param;
|
|
2963
|
-
expected;
|
|
2964
|
-
observed;
|
|
2965
|
-
timeoutMs;
|
|
2966
|
-
constructor(failure) {
|
|
2967
|
-
super(`device ${failure.sn} did not report ${failure.property} as ${String(failure.expected)} within ${failure.timeoutMs}ms (param ${failure.param} read ${String(failure.observed)})`);
|
|
2968
|
-
this.name = "StateConvergenceError";
|
|
2969
|
-
this.sn = failure.sn;
|
|
2970
|
-
this.property = failure.property;
|
|
2971
|
-
this.param = failure.param;
|
|
2972
|
-
this.expected = failure.expected;
|
|
2973
|
-
this.observed = failure.observed;
|
|
2974
|
-
this.timeoutMs = failure.timeoutMs;
|
|
2975
|
-
}
|
|
2976
|
-
};
|
|
2977
|
-
var COMMAND_OBSERVATION = /* @__PURE__ */ Symbol("command-observation");
|
|
2978
|
-
function observeCommand(command, observation) {
|
|
2979
|
-
return Object.defineProperty(command, COMMAND_OBSERVATION, { configurable: true, value: observation });
|
|
2980
|
-
}
|
|
2981
|
-
function commandObservation(command) {
|
|
2982
|
-
return command[COMMAND_OBSERVATION];
|
|
2983
|
-
}
|
|
2984
|
-
|
|
2985
3159
|
// dist/model/capabilities/access.js
|
|
2986
3160
|
function setScalar(param, value, ctx, form = "auto") {
|
|
2987
3161
|
return { kind: "set-param", param, value, form, channel: ctx.channel };
|
|
@@ -3254,11 +3428,17 @@ var unreflectedMembers = (surface) => statement(surface, UNREFLECTED);
|
|
|
3254
3428
|
// dist/model/capabilities/manifest.js
|
|
3255
3429
|
function describeBound(modules, bound, ctx) {
|
|
3256
3430
|
const out = [];
|
|
3431
|
+
const anyBound = Object.values(bound).some((obj) => obj && typeof obj === "object");
|
|
3257
3432
|
for (const m of modules) {
|
|
3258
3433
|
const accessor = camelCase(m.capability);
|
|
3259
3434
|
const obj = bound[accessor];
|
|
3260
|
-
if (!obj || typeof obj !== "object")
|
|
3435
|
+
if (!obj || typeof obj !== "object") {
|
|
3436
|
+
const events = anyBound ? emitsOf(m, [], ctx) : [];
|
|
3437
|
+
if (events.length && ctx?.capabilities?.has(m.capability)) {
|
|
3438
|
+
out.push({ capability: m.capability, reads: [], actions: [], undescribedActions: [], events });
|
|
3439
|
+
}
|
|
3261
3440
|
continue;
|
|
3441
|
+
}
|
|
3262
3442
|
const descriptors = Object.getOwnPropertyDescriptors(obj);
|
|
3263
3443
|
const reads2 = [];
|
|
3264
3444
|
const actions = [];
|
|
@@ -3781,7 +3961,6 @@ var MOTION_CMD = {
|
|
|
3781
3961
|
* payload key AND pass mChannel 0 explicitly; the two are separate choices, not linked.)
|
|
3782
3962
|
*
|
|
3783
3963
|
* ⚠️ Replay + readback confirmed on a HomeBase-attached T8425 (1719 `0`→`1`→`0`), NOT byte-captured.
|
|
3784
|
-
* Provenance is `apk`, not `verified`: a divergent-but-also-accepted frame can't be ruled out.
|
|
3785
3964
|
*/
|
|
3786
3965
|
HUMAN_ONLY_AT_NIGHT: 1719,
|
|
3787
3966
|
/**
|
|
@@ -3795,7 +3974,7 @@ var MOTION_CMD = {
|
|
|
3795
3974
|
* form, so a feature that is on reads as off.
|
|
3796
3975
|
* Decoded by {@link decodeRadarWdSwitch} to match the app.
|
|
3797
3976
|
*
|
|
3798
|
-
* ⚠️ Replay + readback confirmed on a T8214 (2706 `0`→`1`→`0`), NOT byte-captured.
|
|
3977
|
+
* ⚠️ Replay + readback confirmed on a T8214 (2706 `0`→`1`→`0`), NOT byte-captured.
|
|
3799
3978
|
*/
|
|
3800
3979
|
LOITERING_DETECTION: 2706,
|
|
3801
3980
|
/**
|
|
@@ -4119,7 +4298,7 @@ var MOTION_MEMBERS = {
|
|
|
4119
4298
|
param: MOTION_CMD.HUMAN_ONLY_AT_NIGHT,
|
|
4120
4299
|
type: "bool",
|
|
4121
4300
|
kind: "boolean",
|
|
4122
|
-
provenance: "
|
|
4301
|
+
provenance: "verified",
|
|
4123
4302
|
description: "Restrict AI classification to night-time only (1719). \u26A0\uFE0F Replay + readback confirmed on a HomeBase-attached T8425, not byte-captured.",
|
|
4124
4303
|
requires: [MOTION_CMD.HUMAN_ONLY_AT_NIGHT],
|
|
4125
4304
|
write: (v, ctx) => {
|
|
@@ -4135,7 +4314,7 @@ var MOTION_MEMBERS = {
|
|
|
4135
4314
|
param: MOTION_CMD.LOITERING_DETECTION,
|
|
4136
4315
|
type: "bool",
|
|
4137
4316
|
kind: "boolean",
|
|
4138
|
-
provenance: "
|
|
4317
|
+
provenance: "verified",
|
|
4139
4318
|
coerce: (raw) => decodeRadarWdSwitch(raw) ?? false,
|
|
4140
4319
|
description: "Loitering detection \u2014 alert on lingering rather than passing (2706). Observed on the T8214 doorbell only; the read is object-OR-scalar, matching the app's own decode.",
|
|
4141
4320
|
requires: [MOTION_CMD.LOITERING_DETECTION],
|
|
@@ -4241,6 +4420,7 @@ var MOTION = {
|
|
|
4241
4420
|
};
|
|
4242
4421
|
|
|
4243
4422
|
// dist/model/capabilities/person-detection.js
|
|
4423
|
+
var IDENTIFIED_PERSON_CLAIM = { homeBaseAttached: true };
|
|
4244
4424
|
var PERSON_DETECTION = {
|
|
4245
4425
|
capability: "person_detection",
|
|
4246
4426
|
description: "AI human detection enable switch and live detected state.",
|
|
@@ -4257,8 +4437,18 @@ var PERSON_DETECTION = {
|
|
|
4257
4437
|
*/
|
|
4258
4438
|
events: [
|
|
4259
4439
|
{ source: "push", match: DoorbellPushEvent.FACE_DETECTION, emit: "personDetected" },
|
|
4260
|
-
{
|
|
4261
|
-
|
|
4440
|
+
{
|
|
4441
|
+
source: "push",
|
|
4442
|
+
match: HB3PairedDevicePushEvent.IDENTITY_PERSON_DETECTION,
|
|
4443
|
+
emit: "personDetected",
|
|
4444
|
+
claim: IDENTIFIED_PERSON_CLAIM
|
|
4445
|
+
},
|
|
4446
|
+
{
|
|
4447
|
+
source: "push",
|
|
4448
|
+
match: HB3PairedDevicePushEvent.STRANGER_PERSON_DETECTION,
|
|
4449
|
+
emit: "strangerDetected",
|
|
4450
|
+
claim: IDENTIFIED_PERSON_CLAIM
|
|
4451
|
+
}
|
|
4262
4452
|
]
|
|
4263
4453
|
};
|
|
4264
4454
|
|
|
@@ -4326,7 +4516,7 @@ function decodePowerSource(raw) {
|
|
|
4326
4516
|
function recordSetting(param, value, ctx) {
|
|
4327
4517
|
return setStationScalar(param, value, ctx.channel);
|
|
4328
4518
|
}
|
|
4329
|
-
var MAINS_CAMERA_MODELS = ["T8425", "T8419", "T8410", "T84A1"];
|
|
4519
|
+
var MAINS_CAMERA_MODELS = ["T8425", "T8419", "T8410", "T84A1", "T8423"];
|
|
4330
4520
|
function cameraPowerTier(model, capabilities) {
|
|
4331
4521
|
return capabilities.has("battery") && notMainsCamera({ model }) ? "battery" : "wired";
|
|
4332
4522
|
}
|
|
@@ -5536,6 +5726,7 @@ var CAMERA_MEMBERS = {
|
|
|
5536
5726
|
refuseWhenDisabled(ctx, read);
|
|
5537
5727
|
return m.recordFragments({ powered: poweredOf(ctx), ...opts });
|
|
5538
5728
|
}), "Continuous fragmented-MP4 (CMAF) recording."),
|
|
5729
|
+
downloadRecording: provided("media", (m) => m.downloadRecording && ((opts) => m.downloadRecording(opts)), "Download a recording the station holds \u2192 H.264 and AAC elementary streams."),
|
|
5539
5730
|
/**
|
|
5540
5731
|
* Push audio from the host to this camera's speaker. Gated on the **speaker** param specifically —
|
|
5541
5732
|
* not the `audio` capability, which resolves on a microphone alone and would advertise a speaker the
|
|
@@ -5577,7 +5768,7 @@ var SIREN_CMD = {
|
|
|
5577
5768
|
ALARM_TEST: 1826,
|
|
5578
5769
|
/** Manually stop a sounding alarm (app `APP_CMD_SIREN_SENSOR_MANUAL_STOP_ALARM`). */
|
|
5579
5770
|
MANUAL_STOP: 1871,
|
|
5580
|
-
/** Station-family HomeBase duration alarm, verified live on T8010 (app `SET_TONE_FILE`). */
|
|
5771
|
+
/** Station-family HomeBase duration alarm, verified live on T8010 and T8030 (app `SET_TONE_FILE`). */
|
|
5581
5772
|
HOMEBASE_TONE: 1201,
|
|
5582
5773
|
/** HomeBase alarm volume percentage (app `CMD_SET_HUB_SPK_VOLUME`). */
|
|
5583
5774
|
HUB_SPK_VOLUME: 1235,
|
|
@@ -5592,7 +5783,7 @@ var SirenAlarmDuration = { Min1: 60, Min5: 300, Min10: 600, Min15: 900 };
|
|
|
5592
5783
|
var HubAlarmTone = { Tone1: 1, Tone2: 2 };
|
|
5593
5784
|
var HUB_ALARM_EVIDENCE = [SIREN_CMD.HUB_ALARM_TONE, 1282];
|
|
5594
5785
|
function isStationAlarmOutput(ctx) {
|
|
5595
|
-
return ctx.deviceType === DeviceType.STATION;
|
|
5786
|
+
return ctx.deviceType === DeviceType.STATION || ctx.deviceType === DeviceType.HB3;
|
|
5596
5787
|
}
|
|
5597
5788
|
function isEvidencedStationAlarm(ctx) {
|
|
5598
5789
|
return isStationAlarmOutput(ctx) && HUB_ALARM_EVIDENCE.some((param) => ctx.paramIds.has(param));
|
|
@@ -6714,9 +6905,9 @@ function parseQuickResponses(voiceList) {
|
|
|
6714
6905
|
var DOORBELL_MEMBERS = {
|
|
6715
6906
|
/**
|
|
6716
6907
|
* The HOMEBASE as the doorbell's chime — the hub plays the ring, not the wired chime box
|
|
6717
|
-
* `mechanicalChimeSwitch` drives. `provenance` is "verified" on our own decrypt of the app's frame
|
|
6718
|
-
*
|
|
6719
|
-
*
|
|
6908
|
+
* `mechanicalChimeSwitch` drives. `provenance` is "verified" on our own decrypt of the app's frame:
|
|
6909
|
+
* direct-binary `[ch][value][acct]`, 1=on/0=off, the same shape as its 1703 sibling — captured, not
|
|
6910
|
+
* inferred from the shared param range.
|
|
6720
6911
|
*/
|
|
6721
6912
|
chimeSwitch: {
|
|
6722
6913
|
param: DOORBELL_CMD.CHIME_SWITCH,
|
|
@@ -6727,9 +6918,8 @@ var DOORBELL_MEMBERS = {
|
|
|
6727
6918
|
write: (v, ctx) => setScalar(DOORBELL_CMD.CHIME_SWITCH, asBool(v) ? 1 : 0, ctx, "direct-binary")
|
|
6728
6919
|
},
|
|
6729
6920
|
/**
|
|
6730
|
-
* `provenance` is "verified"
|
|
6731
|
-
*
|
|
6732
|
-
* `[ch][value][acct]`, 1=on/0=off — verified live on a T8214 (ON then OFF).
|
|
6921
|
+
* `provenance` is "verified" on our own P2P decrypt of the write wire: direct-binary
|
|
6922
|
+
* `[ch][value][acct]`, 1=on/0=off, verified live on a T8214 (ON then OFF).
|
|
6733
6923
|
*/
|
|
6734
6924
|
mechanicalChimeSwitch: {
|
|
6735
6925
|
param: DOORBELL_CMD.MECHANICAL_CHIME_SWITCH,
|
|
@@ -6760,7 +6950,7 @@ var DOORBELL_MEMBERS = {
|
|
|
6760
6950
|
type: "number",
|
|
6761
6951
|
unit: "%",
|
|
6762
6952
|
kind: "percent",
|
|
6763
|
-
provenance: "
|
|
6953
|
+
provenance: "verified",
|
|
6764
6954
|
writtenElsewhere: true,
|
|
6765
6955
|
description: "Ringtone volume (1708; confirmed on T8214, observed value 80). The WRITE is `audio`'s setRingtoneVolume \u2014 that capability owns every volume wire."
|
|
6766
6956
|
},
|
|
@@ -6865,7 +7055,7 @@ var DOORBELL_MEMBERS = {
|
|
|
6865
7055
|
param: 1710,
|
|
6866
7056
|
type: "string",
|
|
6867
7057
|
kind: "text",
|
|
6868
|
-
provenance: "
|
|
7058
|
+
provenance: "verified",
|
|
6869
7059
|
description: "Notification config JSON {notification_motion_onoff,notification_ring_onoff,notification_style} (1710; confirmed on T8214)."
|
|
6870
7060
|
},
|
|
6871
7061
|
/**
|
|
@@ -7534,112 +7724,33 @@ var ARMING = {
|
|
|
7534
7724
|
ownedByStation: true,
|
|
7535
7725
|
/**
|
|
7536
7726
|
* 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
|
-
}
|
|
7623
|
-
/**
|
|
7624
|
-
* The finished DP value: `varint(bodyLength) ++ body`, base64-encoded.
|
|
7727
|
+
* generic `CusPushEvent` channel, so these ids are shared with other families and resolve by
|
|
7728
|
+
* capability (see the barrel's event index).
|
|
7625
7729
|
*
|
|
7626
|
-
*
|
|
7627
|
-
*
|
|
7730
|
+
* The alarm ids carry a static `phase` so one event name covers the whole lifecycle, the same shape
|
|
7731
|
+
* `battery` uses for its threshold pushes. The mode-switch push is known to identify WHICH mode and
|
|
7732
|
+
* what triggered the change, but neither has been observed on the wire here, so this emits the bare
|
|
7733
|
+
* transition and refreshes the `mode` read rather than trusting a decoded field.
|
|
7628
7734
|
*/
|
|
7629
|
-
|
|
7630
|
-
|
|
7631
|
-
|
|
7735
|
+
events: [
|
|
7736
|
+
{
|
|
7737
|
+
source: "push",
|
|
7738
|
+
match: CusPushEvent.MODE_SWITCH,
|
|
7739
|
+
emit: "armingModeChanged",
|
|
7740
|
+
refresh: { member: "mode" }
|
|
7741
|
+
},
|
|
7742
|
+
{ source: "push", match: CusPushEvent.ALARM, emit: "alarm", payload: { phase: "triggered" } },
|
|
7743
|
+
{ source: "push", match: CusPushEvent.ALARM_DELAY, emit: "alarm", payload: { phase: "delayed" } }
|
|
7744
|
+
]
|
|
7745
|
+
};
|
|
7746
|
+
|
|
7747
|
+
// dist/model/capabilities/storage.js
|
|
7748
|
+
var STORAGE = {
|
|
7749
|
+
capability: "storage",
|
|
7750
|
+
description: "Local storage (SD card / eMMC / HDD). Behaviour-only until a capacity param is captured.",
|
|
7751
|
+
properties: [],
|
|
7752
|
+
detection: { codecs: ["station"] }
|
|
7632
7753
|
};
|
|
7633
|
-
function tag(field, wire) {
|
|
7634
|
-
if (!Number.isSafeInteger(field) || field < 1)
|
|
7635
|
-
throw new Error(`field number: ${field} is not a positive integer`);
|
|
7636
|
-
return field * 8 + wire;
|
|
7637
|
-
}
|
|
7638
|
-
function rawDp(build) {
|
|
7639
|
-
const w = new RawDpWriter();
|
|
7640
|
-
build(w);
|
|
7641
|
-
return w.finish();
|
|
7642
|
-
}
|
|
7643
7754
|
|
|
7644
7755
|
// dist/model/capabilities/vacuum-dock.js
|
|
7645
7756
|
var VACUUM_DOCK_DP = 173;
|
|
@@ -13345,58 +13456,6 @@ function parseCleanRecordDetail(blob, codec) {
|
|
|
13345
13456
|
};
|
|
13346
13457
|
}
|
|
13347
13458
|
|
|
13348
|
-
// dist/core/lz4-block.js
|
|
13349
|
-
var MAX_OUTPUT = 64 * 1024 * 1024;
|
|
13350
|
-
function lz4BlockDecompress(block, size) {
|
|
13351
|
-
if (!Number.isSafeInteger(size) || size < 0 || size > MAX_OUTPUT)
|
|
13352
|
-
return void 0;
|
|
13353
|
-
const out = Buffer.alloc(size);
|
|
13354
|
-
const end = block.length;
|
|
13355
|
-
let pos = 0;
|
|
13356
|
-
let written = 0;
|
|
13357
|
-
const extend2 = (base) => {
|
|
13358
|
-
let len = base;
|
|
13359
|
-
if (base !== 15)
|
|
13360
|
-
return len;
|
|
13361
|
-
for (; ; ) {
|
|
13362
|
-
if (pos >= end)
|
|
13363
|
-
return void 0;
|
|
13364
|
-
const extra = block[pos++];
|
|
13365
|
-
len += extra;
|
|
13366
|
-
if (extra !== 255)
|
|
13367
|
-
return len;
|
|
13368
|
-
}
|
|
13369
|
-
};
|
|
13370
|
-
while (pos < end) {
|
|
13371
|
-
const token = block[pos++];
|
|
13372
|
-
const literals = extend2(token >> 4);
|
|
13373
|
-
if (literals === void 0)
|
|
13374
|
-
return void 0;
|
|
13375
|
-
if (pos + literals > end || written + literals > size)
|
|
13376
|
-
return void 0;
|
|
13377
|
-
for (let i = 0; i < literals; i++)
|
|
13378
|
-
out[written++] = block[pos++];
|
|
13379
|
-
if (pos === end)
|
|
13380
|
-
break;
|
|
13381
|
-
if (pos + 2 > end)
|
|
13382
|
-
return void 0;
|
|
13383
|
-
const offset = block[pos] | block[pos + 1] << 8;
|
|
13384
|
-
pos += 2;
|
|
13385
|
-
if (offset === 0 || offset > written)
|
|
13386
|
-
return void 0;
|
|
13387
|
-
const match = extend2(token & 15);
|
|
13388
|
-
if (match === void 0)
|
|
13389
|
-
return void 0;
|
|
13390
|
-
const length = match + 4;
|
|
13391
|
-
if (written + length > size)
|
|
13392
|
-
return void 0;
|
|
13393
|
-
let from = written - offset;
|
|
13394
|
-
for (let i = 0; i < length; i++)
|
|
13395
|
-
out[written++] = out[from++];
|
|
13396
|
-
}
|
|
13397
|
-
return written === size ? out : void 0;
|
|
13398
|
-
}
|
|
13399
|
-
|
|
13400
13459
|
// dist/model/vacuum-map.js
|
|
13401
13460
|
var DOCK_KINDS = ["charger", "station"];
|
|
13402
13461
|
var MAP_QUALITIES = ["incomplete", "rough", "effective", "listFull"];
|
|
@@ -14307,6 +14366,7 @@ __export(codec_exports, {
|
|
|
14307
14366
|
buildLookupWithKeyPayload2: () => buildLookupWithKeyPayload2,
|
|
14308
14367
|
buildRawCommandPayload: () => buildRawCommandPayload,
|
|
14309
14368
|
buildStringCommandPayload: () => buildStringCommandPayload,
|
|
14369
|
+
buildStringPairCommandPayload: () => buildStringPairCommandPayload,
|
|
14310
14370
|
buildVoidCommandPayload: () => buildVoidCommandPayload,
|
|
14311
14371
|
decodeP2PCloudIPs: () => decodeP2PCloudIPs,
|
|
14312
14372
|
decryptP2PData: () => decryptP2PData,
|
|
@@ -14343,6 +14403,11 @@ var ResponseMessageType = {
|
|
|
14343
14403
|
LOOKUP_ADDR: Buffer.from([241, 64]),
|
|
14344
14404
|
LOOKUP_ADDR2: Buffer.from([241, 130]),
|
|
14345
14405
|
CAM_ID: Buffer.from([241, 66]),
|
|
14406
|
+
/**
|
|
14407
|
+
* The device's own address record, sent in answer to CHECK_CAM ahead of CAM_ID: its 20-byte device id, one
|
|
14408
|
+
* record in `encodeSelfAddress`'s shape naming the address it answers from, then 8 zero bytes (44-byte payload).
|
|
14409
|
+
*/
|
|
14410
|
+
CAM_ADDR: Buffer.from([241, 67]),
|
|
14346
14411
|
TURN_SERVER_CAM_ID: Buffer.from([241, 132]),
|
|
14347
14412
|
PING: Buffer.from([241, 224]),
|
|
14348
14413
|
PONG: Buffer.from([241, 225]),
|
|
@@ -14554,6 +14619,12 @@ function buildIntStringCommandPayload(value, valueSub, strValue, channel = 0, ke
|
|
|
14554
14619
|
data
|
|
14555
14620
|
]);
|
|
14556
14621
|
}
|
|
14622
|
+
function buildStringPairCommandPayload(strValue, strValueSub, channel = 0, key, encType = 1) {
|
|
14623
|
+
const encrypted = !!key && key.length === 16;
|
|
14624
|
+
const body = Buffer.concat([Buffer.alloc(5), stringWithLength(strValue), stringWithLength(strValueSub)]);
|
|
14625
|
+
const data = encrypted ? encryptP2PData(paddingP2PData(body), key) : body;
|
|
14626
|
+
return buildRawCommandPayload(data, channel, encrypted ? encType : 0);
|
|
14627
|
+
}
|
|
14557
14628
|
function buildRawCommandPayload(data, channel = 0, signCode = 0, magic = [1, 0], streamId = 0) {
|
|
14558
14629
|
const header = Buffer.allocUnsafe(2);
|
|
14559
14630
|
header.writeUInt16LE(data.length, 0);
|
|
@@ -15159,6 +15230,7 @@ var LOCAL_LOOKUP_PORT = 32108;
|
|
|
15159
15230
|
var HEARTBEAT_MS = 5e3;
|
|
15160
15231
|
var PATH_SILENCE_MS = HEARTBEAT_MS * 3;
|
|
15161
15232
|
var LOOKUP_RETRY_MS = 1e3;
|
|
15233
|
+
var RECEIVE_BUFFER_BYTES = 4 * 1024 * 1024;
|
|
15162
15234
|
var CONNECT_TIMEOUT_MS = 15e3;
|
|
15163
15235
|
var STATION_CHANNEL3 = 255;
|
|
15164
15236
|
var CMD_GATEWAYINFO = 1100;
|
|
@@ -15194,6 +15266,7 @@ var CMD_DATABASE_IMAGE = 1308;
|
|
|
15194
15266
|
var CMD_DATABASE = 1306;
|
|
15195
15267
|
var DB_QUERY = { FULL_TABLE: 1e4, COMBINATION_WITH_AI: 10011 };
|
|
15196
15268
|
var GCM_AAD = Buffer.from("eufy security");
|
|
15269
|
+
var LEVEL2_SEQ_BASE = 16909056;
|
|
15197
15270
|
function paramReport(payload) {
|
|
15198
15271
|
const list = payload?.params;
|
|
15199
15272
|
if (!Array.isArray(list))
|
|
@@ -15208,12 +15281,57 @@ function paramReport(payload) {
|
|
|
15208
15281
|
return Object.keys(out).length ? out : void 0;
|
|
15209
15282
|
}
|
|
15210
15283
|
var traceSequence = 0;
|
|
15284
|
+
var DB_TABLE_TIMEOUT_MS = 15e3;
|
|
15285
|
+
function firstTableRows(text2) {
|
|
15286
|
+
const decoded = Buffer.from(text2, "latin1").toString("utf8");
|
|
15287
|
+
for (let start = decoded.indexOf("{"); start >= 0; start = decoded.indexOf("{", start + 1)) {
|
|
15288
|
+
const end = closingBrace(decoded, start);
|
|
15289
|
+
if (end < 0)
|
|
15290
|
+
return void 0;
|
|
15291
|
+
let parsed;
|
|
15292
|
+
try {
|
|
15293
|
+
parsed = JSON.parse(decoded.slice(start, end + 1));
|
|
15294
|
+
} catch {
|
|
15295
|
+
continue;
|
|
15296
|
+
}
|
|
15297
|
+
const data = parsed?.data;
|
|
15298
|
+
if (Array.isArray(data))
|
|
15299
|
+
return data;
|
|
15300
|
+
}
|
|
15301
|
+
return void 0;
|
|
15302
|
+
}
|
|
15303
|
+
function closingBrace(text2, start) {
|
|
15304
|
+
let depth = 0;
|
|
15305
|
+
let inString = false;
|
|
15306
|
+
let escaped = false;
|
|
15307
|
+
for (let i = start; i < text2.length; i++) {
|
|
15308
|
+
const c = text2[i];
|
|
15309
|
+
if (inString) {
|
|
15310
|
+
if (escaped)
|
|
15311
|
+
escaped = false;
|
|
15312
|
+
else if (c === "\\")
|
|
15313
|
+
escaped = true;
|
|
15314
|
+
else if (c === '"')
|
|
15315
|
+
inString = false;
|
|
15316
|
+
continue;
|
|
15317
|
+
}
|
|
15318
|
+
if (c === '"')
|
|
15319
|
+
inString = true;
|
|
15320
|
+
else if (c === "{")
|
|
15321
|
+
depth++;
|
|
15322
|
+
else if (c === "}" && --depth === 0)
|
|
15323
|
+
return i;
|
|
15324
|
+
}
|
|
15325
|
+
return -1;
|
|
15326
|
+
}
|
|
15211
15327
|
var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
15212
15328
|
cfg;
|
|
15213
15329
|
socket;
|
|
15214
15330
|
connected = false;
|
|
15215
15331
|
connecting = false;
|
|
15216
15332
|
closed = false;
|
|
15333
|
+
/** Whether a short receive buffer has been reported, so a reconnect does not repeat the same warning. */
|
|
15334
|
+
receiveBufferReported = false;
|
|
15217
15335
|
connectAddress;
|
|
15218
15336
|
seqNumber = 0;
|
|
15219
15337
|
/**
|
|
@@ -15240,8 +15358,8 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
15240
15358
|
audioStalled = false;
|
|
15241
15359
|
audioRetransmitTimer;
|
|
15242
15360
|
lastPongData;
|
|
15243
|
-
/** When
|
|
15244
|
-
|
|
15361
|
+
/** When the selected peer last answered outbound P2P traffic on this connection. */
|
|
15362
|
+
lastPeerAt;
|
|
15245
15363
|
/** Whether the silence has already been stated, so it is traced once per connection rather than per read. */
|
|
15246
15364
|
pathStaleTraced = false;
|
|
15247
15365
|
lookupTimer;
|
|
@@ -15253,7 +15371,10 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
15253
15371
|
cloudLookup;
|
|
15254
15372
|
/** Local outbound IPv4 reported inside LOOKUP_WITH_KEY requests. */
|
|
15255
15373
|
selfHost;
|
|
15256
|
-
/**
|
|
15374
|
+
/**
|
|
15375
|
+
* In-flight multi-datagram frame per data channel (see reassemble): the payload gathered so far under its
|
|
15376
|
+
* parsed header, or, without a header, the start of a frame header cut by the datagram boundary.
|
|
15377
|
+
*/
|
|
15257
15378
|
pendingByDataType = /* @__PURE__ */ new Map();
|
|
15258
15379
|
/** Last delivered datagram sequence number per data type. */
|
|
15259
15380
|
lastSeqByType = /* @__PURE__ */ new Map();
|
|
@@ -15307,17 +15428,17 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
15307
15428
|
/**
|
|
15308
15429
|
* How long this connection's path has been silent, or nothing where it has never answered.
|
|
15309
15430
|
*
|
|
15310
|
-
*
|
|
15311
|
-
*
|
|
15431
|
+
* PONG and ACK from the selected peer prove the path answers outbound traffic. `undefined` means no such reply
|
|
15432
|
+
* has arrived since connection, so silence alone does not establish a stale path.
|
|
15312
15433
|
*/
|
|
15313
15434
|
get pathSilentMs() {
|
|
15314
|
-
return this.
|
|
15435
|
+
return this.lastPeerAt === void 0 ? void 0 : Date.now() - this.lastPeerAt;
|
|
15315
15436
|
}
|
|
15316
15437
|
/**
|
|
15317
15438
|
* Whether this path can still be committed to, on the evidence the heartbeat gives.
|
|
15318
15439
|
*
|
|
15319
|
-
* False where
|
|
15320
|
-
* not known to be dead, so it answers true.
|
|
15440
|
+
* False where selected-peer replies arrived and then stopped for {@link PATH_SILENCE_MS}. A connection with
|
|
15441
|
+
* no post-connect reply is not known to be dead, so it answers true.
|
|
15321
15442
|
*
|
|
15322
15443
|
* Traces the silence once per connection, on the read that first observes it.
|
|
15323
15444
|
*/
|
|
@@ -15533,6 +15654,26 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
15533
15654
|
get isConnected() {
|
|
15534
15655
|
return this.connected;
|
|
15535
15656
|
}
|
|
15657
|
+
/**
|
|
15658
|
+
* Ask the OS for {@link RECEIVE_BUFFER_BYTES} on a bound socket, and warn once per session when it grants
|
|
15659
|
+
* less or refuses.
|
|
15660
|
+
*
|
|
15661
|
+
* The request is made here rather than through `createSocket`'s `recvBufferSize`: Node applies that option
|
|
15662
|
+
* inside the bind callback, where a refusal is thrown out of reach of this session and ends the process.
|
|
15663
|
+
*/
|
|
15664
|
+
requestReceiveBuffer(socket) {
|
|
15665
|
+
let granted;
|
|
15666
|
+
try {
|
|
15667
|
+
socket.setRecvBufferSize(RECEIVE_BUFFER_BYTES);
|
|
15668
|
+
granted = socket.getRecvBufferSize();
|
|
15669
|
+
} catch {
|
|
15670
|
+
granted = 0;
|
|
15671
|
+
}
|
|
15672
|
+
if (granted >= RECEIVE_BUFFER_BYTES || this.receiveBufferReported)
|
|
15673
|
+
return;
|
|
15674
|
+
this.receiveBufferReported = true;
|
|
15675
|
+
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}.`);
|
|
15676
|
+
}
|
|
15536
15677
|
/** Open the socket and start the lookup → hole-punch handshake. */
|
|
15537
15678
|
async connect() {
|
|
15538
15679
|
if (this.connecting || this.connected)
|
|
@@ -15556,6 +15697,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
15556
15697
|
socket.setBroadcast(true);
|
|
15557
15698
|
} catch {
|
|
15558
15699
|
}
|
|
15700
|
+
this.requestReceiveBuffer(socket);
|
|
15559
15701
|
resolve();
|
|
15560
15702
|
});
|
|
15561
15703
|
});
|
|
@@ -15674,10 +15816,18 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
15674
15816
|
* straddling a level-2 wait on a camera whose failure to deliver video had a separate cause, and did not
|
|
15675
15817
|
* recur across later probes of it. Correlate an unmodelled type against a WORKING session before reading it
|
|
15676
15818
|
* as a cause.
|
|
15819
|
+
*
|
|
15820
|
+
* CAM_ADDR is recognised and dropped: it names the address the device is already answering from, and the
|
|
15821
|
+
* CAM_ID that follows it is what completes the connect.
|
|
15677
15822
|
*/
|
|
15678
15823
|
onMessage(msg, rinfo, socket = this.socket) {
|
|
15679
15824
|
if (!socket)
|
|
15680
15825
|
return;
|
|
15826
|
+
const fromConnectedPeer = this.connected && this.connectAddress?.host === rinfo.address && this.connectAddress.port === rinfo.port;
|
|
15827
|
+
if (fromConnectedPeer && (hasHeader(msg, ResponseMessageType.PONG) || hasHeader(msg, ResponseMessageType.ACK))) {
|
|
15828
|
+
this.lastPeerAt = Date.now();
|
|
15829
|
+
this.pathStaleTraced = false;
|
|
15830
|
+
}
|
|
15681
15831
|
if (!hasHeader(msg, ResponseMessageType.DATA)) {
|
|
15682
15832
|
this.logger.debug(`[p2p] ${this.cfg.stationSn} <<< ${rinfo.address}:${rinfo.port} header=${msg.subarray(0, 2).toString("hex")} len=${msg.length}`);
|
|
15683
15833
|
}
|
|
@@ -15695,8 +15845,6 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
15695
15845
|
this.onConnected({ host: rinfo.address, port: rinfo.port }, socket);
|
|
15696
15846
|
} else if (hasHeader(msg, ResponseMessageType.PONG)) {
|
|
15697
15847
|
this.lastPongData = msg.length > 4 ? msg.subarray(4) : void 0;
|
|
15698
|
-
this.lastPongAt = Date.now();
|
|
15699
|
-
this.pathStaleTraced = false;
|
|
15700
15848
|
} else if (hasHeader(msg, ResponseMessageType.PING)) {
|
|
15701
15849
|
this.send({ host: rinfo.address, port: rinfo.port }, RequestMessageType.PONG, void 0, socket);
|
|
15702
15850
|
} else if (hasHeader(msg, ResponseMessageType.ACK)) {
|
|
@@ -15704,7 +15852,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
15704
15852
|
} else if (hasHeader(msg, ResponseMessageType.DATA)) {
|
|
15705
15853
|
if (this.connected)
|
|
15706
15854
|
this.onData(msg, { host: rinfo.address, port: rinfo.port });
|
|
15707
|
-
} else {
|
|
15855
|
+
} else if (!hasHeader(msg, ResponseMessageType.CAM_ADDR)) {
|
|
15708
15856
|
this.logger.debug(`[p2p] ${this.cfg.stationSn} UNHANDLED payload hex: ${msg.toString("hex")}`);
|
|
15709
15857
|
}
|
|
15710
15858
|
}
|
|
@@ -15995,6 +16143,17 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
15995
16143
|
this.seqNumber = this.seqNumber + 1 & 65535;
|
|
15996
16144
|
this.send(this.connectAddress, RequestMessageType.DATA, data);
|
|
15997
16145
|
}
|
|
16146
|
+
/** Send a level-1 {@link buildStringPairCommandPayload} command on `channel`. */
|
|
16147
|
+
sendStringPairCommand(commandType, strValue, strValueSub, channel) {
|
|
16148
|
+
if (!this.connectAddress)
|
|
16149
|
+
throw new Error(`P2P session ${this.cfg.stationSn} is not connected`);
|
|
16150
|
+
const data = Buffer.concat([
|
|
16151
|
+
buildCommandHeader(this.seqNumber, commandType),
|
|
16152
|
+
buildStringPairCommandPayload(strValue, strValueSub, channel, this.level1Key, 1)
|
|
16153
|
+
]);
|
|
16154
|
+
this.seqNumber = this.seqNumber + 1 & 65535;
|
|
16155
|
+
this.send(this.connectAddress, RequestMessageType.DATA, data);
|
|
16156
|
+
}
|
|
15998
16157
|
/**
|
|
15999
16158
|
* Send a **level-2 (AES-256-GCM, signCode 8) control payload** to a HomeBase-attached device. The
|
|
16000
16159
|
* target camera is selected by `channel` (= device_channel) + the `mChannel` envelope — the same
|
|
@@ -16272,11 +16431,6 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16272
16431
|
this.seqNumber = this.seqNumber + 1 & 65535;
|
|
16273
16432
|
this.send(this.connectAddress, RequestMessageType.DATA, data);
|
|
16274
16433
|
}
|
|
16275
|
-
/**
|
|
16276
|
-
* Encrypt a level-2 command body (signCode 8): `tag(16) ‖ nonce(12) ‖ [seq,03,02,01](4) ‖
|
|
16277
|
-
* ciphertext`, AES-256-GCM under the negotiated session key, AAD "eufy security". Inverse of
|
|
16278
|
-
* `decryptLevel2`. The 4-byte sub-header is cleartext (skipped on decrypt); `seq` is a counter.
|
|
16279
|
-
*/
|
|
16280
16434
|
/**
|
|
16281
16435
|
* Decode a `CMD_VIDEO_FRAME` (1300) payload into clean Annex-B H.264 (the 22-byte frame header
|
|
16282
16436
|
* stripped). Reversed from the V6 app + live H.264 captures: the 22-byte header is
|
|
@@ -16343,6 +16497,11 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16343
16497
|
}
|
|
16344
16498
|
return this.rsaModulusHex;
|
|
16345
16499
|
}
|
|
16500
|
+
/**
|
|
16501
|
+
* Encrypt a level-2 command body (signCode 8): `tag(16) ‖ nonce(12) ‖ seq(4) ‖ ciphertext`,
|
|
16502
|
+
* AES-256-GCM under the negotiated session key, AAD "eufy security". Inverse of `decryptLevel2`.
|
|
16503
|
+
* The 4-byte sub-header is cleartext (skipped on decrypt); `seq` counts up from {@link LEVEL2_SEQ_BASE}.
|
|
16504
|
+
*/
|
|
16346
16505
|
encryptLevel2(plaintext) {
|
|
16347
16506
|
if (!this.level2Key)
|
|
16348
16507
|
return void 0;
|
|
@@ -16350,8 +16509,8 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16350
16509
|
const c = createCipheriv3("aes-256-gcm", this.level2Key, nonce);
|
|
16351
16510
|
c.setAAD(GCM_AAD);
|
|
16352
16511
|
const ct = Buffer.concat([c.update(plaintext), c.final()]);
|
|
16353
|
-
const sub2 = Buffer.
|
|
16354
|
-
|
|
16512
|
+
const sub2 = Buffer.alloc(4);
|
|
16513
|
+
sub2.writeUInt32LE(LEVEL2_SEQ_BASE + this.level2Seq++ >>> 0);
|
|
16355
16514
|
return Buffer.concat([c.getAuthTag(), nonce, sub2, ct]);
|
|
16356
16515
|
}
|
|
16357
16516
|
/** Send a no-arg command frame (e.g. CMD_GATEWAYINFO) on a channel (default: the station channel). */
|
|
@@ -16397,8 +16556,14 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16397
16556
|
* The HomeBase streams back `CMD_DATABASE` (1306) frames `{cmd:10000,count,data:[…]}`,
|
|
16398
16557
|
* level-1-encrypted — decoded and emitted as `dbChunk` (decrypted text) per frame.
|
|
16399
16558
|
* Tables: `familiar_faces`, `person_basic_info`, `event_person_list`, `history_record_info`.
|
|
16559
|
+
*
|
|
16560
|
+
* Throws while a {@link readDatabase} is accumulating: every table answers `{data:[…]}` and the
|
|
16561
|
+
* frames tie no chunk to its request, so a second query's reply would be assembled into the first
|
|
16562
|
+
* one's buffer and answered as its rows.
|
|
16400
16563
|
*/
|
|
16401
16564
|
queryDatabase(table, opts = {}) {
|
|
16565
|
+
if (this.dbReadInFlight)
|
|
16566
|
+
throw new Error(`queryDatabase: ${this.cfg.stationSn} is already reading a table`);
|
|
16402
16567
|
if (!this.connectAddress)
|
|
16403
16568
|
throw new Error("not connected");
|
|
16404
16569
|
const channel = opts.channel ?? STATION_CHANNEL3;
|
|
@@ -16461,6 +16626,49 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16461
16626
|
query: this.fullTableQuery()
|
|
16462
16627
|
});
|
|
16463
16628
|
}
|
|
16629
|
+
/**
|
|
16630
|
+
* Query one on-station table and answer its rows, once the reply is whole.
|
|
16631
|
+
*
|
|
16632
|
+
* The request half of {@link queryDatabase} with its reply assembled: `CMD_DATABASE` arrives as
|
|
16633
|
+
* several frames whose decrypted text is a fragment of one document, so the fragments are
|
|
16634
|
+
* accumulated here and scanned after each one. Answers that document's `data` rows. Rejects when
|
|
16635
|
+
* `signal` aborts, and when `timeoutMs` (default {@link DB_TABLE_TIMEOUT_MS}) elapses with no
|
|
16636
|
+
* complete reply.
|
|
16637
|
+
*
|
|
16638
|
+
* One read at a time per session: while one is accumulating, every {@link queryDatabase} on the
|
|
16639
|
+
* session throws, so a second reply cannot land in this buffer.
|
|
16640
|
+
*/
|
|
16641
|
+
async readDatabase(table, opts = {}) {
|
|
16642
|
+
if (opts.signal?.aborted)
|
|
16643
|
+
throw new Error("readDatabase: aborted");
|
|
16644
|
+
this.queryDatabase(table, { accountId: opts.accountId, query: this.fullTableQuery() });
|
|
16645
|
+
this.dbReadInFlight = true;
|
|
16646
|
+
try {
|
|
16647
|
+
return await new Promise((resolve, reject) => {
|
|
16648
|
+
let text2 = "";
|
|
16649
|
+
const onChunk = (chunk) => {
|
|
16650
|
+
text2 += chunk.text;
|
|
16651
|
+
const rows = firstTableRows(text2);
|
|
16652
|
+
if (rows)
|
|
16653
|
+
settle(() => resolve(rows));
|
|
16654
|
+
};
|
|
16655
|
+
const timer = setTimeout(() => settle(() => reject(new Error(`readDatabase: ${this.cfg.stationSn} sent no complete ${table}`))), opts.timeoutMs ?? DB_TABLE_TIMEOUT_MS);
|
|
16656
|
+
const onAbort = () => settle(() => reject(new Error("readDatabase: aborted")));
|
|
16657
|
+
const settle = (finish) => {
|
|
16658
|
+
clearTimeout(timer);
|
|
16659
|
+
this.off("dbChunk", onChunk);
|
|
16660
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
16661
|
+
finish();
|
|
16662
|
+
};
|
|
16663
|
+
this.on("dbChunk", onChunk);
|
|
16664
|
+
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
16665
|
+
});
|
|
16666
|
+
} finally {
|
|
16667
|
+
this.dbReadInFlight = false;
|
|
16668
|
+
}
|
|
16669
|
+
}
|
|
16670
|
+
/** Whether a {@link readDatabase} is accumulating; see {@link queryDatabase} for what it bars. */
|
|
16671
|
+
dbReadInFlight = false;
|
|
16464
16672
|
/**
|
|
16465
16673
|
* Request the **face feature rows** over P2P (`face_feature_info`, inner `cmd 10000`). Each row
|
|
16466
16674
|
* carries `{person_id, face_name, face_id, face_picture_content, face_feature_file_path}` — where
|
|
@@ -16628,7 +16836,13 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16628
16836
|
clearTimeout(reorder.timer);
|
|
16629
16837
|
reorder.timer = void 0;
|
|
16630
16838
|
}
|
|
16631
|
-
/**
|
|
16839
|
+
/**
|
|
16840
|
+
* Reassemble one in-sequence datagram body into logical frames.
|
|
16841
|
+
*
|
|
16842
|
+
* Frames are packed back to back, so a frame header can itself be cut by a datagram boundary. The start
|
|
16843
|
+
* of a header left at the end of a datagram is carried into the next one rather than discarded; dropping
|
|
16844
|
+
* it would lose that frame and every frame after it until a datagram happened to begin on a header.
|
|
16845
|
+
*/
|
|
16632
16846
|
reassemble(dataType, datagramBody) {
|
|
16633
16847
|
const pending = this.pendingByDataType.get(dataType);
|
|
16634
16848
|
let body = pending ? Buffer.concat([pending.buf, datagramBody]) : datagramBody;
|
|
@@ -16652,6 +16866,9 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16652
16866
|
this.handleFrame(header, payload.subarray(0, header.bytesToRead), dataType);
|
|
16653
16867
|
body = body.subarray(P2P_DATA_HEADER_BYTES + header.bytesToRead);
|
|
16654
16868
|
}
|
|
16869
|
+
if (body.length > 0 && body.length < P2P_DATA_HEADER_BYTES && MAGIC_WORD.startsWith(body.subarray(0, 4).toString())) {
|
|
16870
|
+
this.pendingByDataType.set(dataType, { buf: body });
|
|
16871
|
+
}
|
|
16655
16872
|
}
|
|
16656
16873
|
/**
|
|
16657
16874
|
* Forget where each data type's sequence numbering had reached, and drop any half-reassembled frame.
|
|
@@ -18114,61 +18331,340 @@ function jpegGeometry(jpeg) {
|
|
|
18114
18331
|
return void 0;
|
|
18115
18332
|
return { height: jpeg.readUInt16BE(at + 5), width: jpeg.readUInt16BE(at + 7) };
|
|
18116
18333
|
}
|
|
18117
|
-
if (JPEG_STANDALONE_MARKERS.has(marker))
|
|
18118
|
-
at += 2;
|
|
18119
|
-
else if (at + 4 <= jpeg.length)
|
|
18120
|
-
at += 2 + jpeg.readUInt16BE(at + 2);
|
|
18334
|
+
if (JPEG_STANDALONE_MARKERS.has(marker))
|
|
18335
|
+
at += 2;
|
|
18336
|
+
else if (at + 4 <= jpeg.length)
|
|
18337
|
+
at += 2 + jpeg.readUInt16BE(at + 2);
|
|
18338
|
+
else
|
|
18339
|
+
return void 0;
|
|
18340
|
+
}
|
|
18341
|
+
return void 0;
|
|
18342
|
+
}
|
|
18343
|
+
function annexbToJpeg(annexb, spawn2) {
|
|
18344
|
+
const codec = annexbFfmpegFormat(annexb);
|
|
18345
|
+
return new Promise((resolve, reject) => {
|
|
18346
|
+
const ff = spawnFfmpeg([
|
|
18347
|
+
// prettier-ignore
|
|
18348
|
+
"-f",
|
|
18349
|
+
codec,
|
|
18350
|
+
"-i",
|
|
18351
|
+
"pipe:0",
|
|
18352
|
+
"-frames:v",
|
|
18353
|
+
"1",
|
|
18354
|
+
"-pix_fmt",
|
|
18355
|
+
"yuvj420p",
|
|
18356
|
+
"-f",
|
|
18357
|
+
"image2",
|
|
18358
|
+
"-vcodec",
|
|
18359
|
+
"mjpeg",
|
|
18360
|
+
"pipe:1"
|
|
18361
|
+
], spawn2);
|
|
18362
|
+
const out = [];
|
|
18363
|
+
const err = [];
|
|
18364
|
+
ff.stdout.on("data", (d) => out.push(d));
|
|
18365
|
+
ff.stderr.on("data", (d) => err.push(d));
|
|
18366
|
+
ff.on("error", (e) => reject(new LiveSnapshotUnavailableError("decoder-unavailable", `ffmpeg not runnable: ${e instanceof Error ? e.message : e}`, { cause: e })));
|
|
18367
|
+
ff.on("close", (code) => {
|
|
18368
|
+
const jpeg = Buffer.concat(out);
|
|
18369
|
+
const encoded = jpeg.length >= 3 && jpeg.subarray(0, 3).toString("hex") === "ffd8ff";
|
|
18370
|
+
const geometry = encoded ? jpegGeometry(jpeg) : void 0;
|
|
18371
|
+
if (geometry)
|
|
18372
|
+
return resolve({ jpeg, ...geometry });
|
|
18373
|
+
const diagnostics = Buffer.concat(err).toString().slice(0, 200).trim();
|
|
18374
|
+
const what = code !== 0 ? `ffmpeg exited ${code}` : encoded ? "encoded image declares no frame header" : "no complete frame in the burst";
|
|
18375
|
+
reject(new LiveSnapshotUnavailableError("undecodable-burst", diagnostics ? `${what}: ${diagnostics}` : what));
|
|
18376
|
+
});
|
|
18377
|
+
ff.stdin.on("error", () => {
|
|
18378
|
+
});
|
|
18379
|
+
ff.stdin.write(annexb);
|
|
18380
|
+
ff.stdin.end();
|
|
18381
|
+
});
|
|
18382
|
+
}
|
|
18383
|
+
|
|
18384
|
+
// dist/transport/p2p/recording-download.js
|
|
18385
|
+
import { createDecipheriv as createDecipheriv7 } from "node:crypto";
|
|
18386
|
+
|
|
18387
|
+
// dist/transport/p2p/adts.js
|
|
18388
|
+
var PROFILE_AAC_LC = 1;
|
|
18389
|
+
var FREQ_INDEX_16K = 8;
|
|
18390
|
+
var CHANNELS_MONO = 1;
|
|
18391
|
+
var AAC_SAMPLES_PER_FRAME = 1024;
|
|
18392
|
+
var AAC_SAMPLE_RATE = 16e3;
|
|
18393
|
+
var AAC_FRAME_MS = AAC_SAMPLES_PER_FRAME * 1e3 / AAC_SAMPLE_RATE;
|
|
18394
|
+
var MAX_AUDIO_FRAME_BYTES = 640;
|
|
18395
|
+
var HEADER_LEN_NO_CRC = 7;
|
|
18396
|
+
function parseAdtsHeader(buf, offset = 0) {
|
|
18397
|
+
if (offset + HEADER_LEN_NO_CRC > buf.length)
|
|
18398
|
+
return void 0;
|
|
18399
|
+
if (buf[offset] !== 255 || (buf[offset + 1] & 240) !== 240)
|
|
18400
|
+
return void 0;
|
|
18401
|
+
const headerLength = buf[offset + 1] & 1 ? 7 : 9;
|
|
18402
|
+
const frameLength = (buf[offset + 3] & 3) << 11 | buf[offset + 4] << 3 | (buf[offset + 5] & 224) >> 5;
|
|
18403
|
+
if (frameLength < headerLength)
|
|
18404
|
+
return void 0;
|
|
18405
|
+
return {
|
|
18406
|
+
frameLength,
|
|
18407
|
+
headerLength,
|
|
18408
|
+
profile: (buf[offset + 2] & 192) >> 6,
|
|
18409
|
+
frequencyIndex: (buf[offset + 2] & 60) >> 2,
|
|
18410
|
+
channels: (buf[offset + 2] & 1) << 2 | (buf[offset + 3] & 192) >> 6
|
|
18411
|
+
};
|
|
18412
|
+
}
|
|
18413
|
+
function buildAdtsHeader(payloadLength) {
|
|
18414
|
+
const frameLength = payloadLength + HEADER_LEN_NO_CRC;
|
|
18415
|
+
return Buffer.from([
|
|
18416
|
+
255,
|
|
18417
|
+
241,
|
|
18418
|
+
PROFILE_AAC_LC << 6 | FREQ_INDEX_16K << 2 | CHANNELS_MONO >> 2,
|
|
18419
|
+
(CHANNELS_MONO & 3) << 6 | frameLength >> 11 & 3,
|
|
18420
|
+
frameLength >> 3 & 255,
|
|
18421
|
+
(frameLength & 7) << 5 | 31,
|
|
18422
|
+
252
|
|
18423
|
+
]);
|
|
18424
|
+
}
|
|
18425
|
+
function isSupportedAdts(h) {
|
|
18426
|
+
return h.profile === PROFILE_AAC_LC && h.frequencyIndex === FREQ_INDEX_16K && h.channels === CHANNELS_MONO;
|
|
18427
|
+
}
|
|
18428
|
+
function describeAdts(h) {
|
|
18429
|
+
const rate = ADTS_SAMPLE_RATES[h.frequencyIndex] ?? `index ${h.frequencyIndex}`;
|
|
18430
|
+
const profile = h.profile === PROFILE_AAC_LC ? "AAC-LC" : `profile field ${h.profile}`;
|
|
18431
|
+
return `${profile}, ${rate} Hz, ${h.channels} channel(s)`;
|
|
18432
|
+
}
|
|
18433
|
+
var ADTS_SAMPLE_RATES = [
|
|
18434
|
+
96e3,
|
|
18435
|
+
88200,
|
|
18436
|
+
64e3,
|
|
18437
|
+
48e3,
|
|
18438
|
+
44100,
|
|
18439
|
+
32e3,
|
|
18440
|
+
24e3,
|
|
18441
|
+
22050,
|
|
18442
|
+
16e3,
|
|
18443
|
+
12e3,
|
|
18444
|
+
11025,
|
|
18445
|
+
8e3,
|
|
18446
|
+
7350
|
|
18447
|
+
];
|
|
18448
|
+
var AdtsFrameReader = class {
|
|
18449
|
+
buffered = Buffer.alloc(0);
|
|
18450
|
+
/**
|
|
18451
|
+
* Append `chunk` and return every complete frame now available, each a standalone buffer that still
|
|
18452
|
+
* carries its own ADTS header (the device expects the header on the wire).
|
|
18453
|
+
*/
|
|
18454
|
+
push(chunk) {
|
|
18455
|
+
this.buffered = this.buffered.length ? Buffer.concat([this.buffered, chunk]) : chunk;
|
|
18456
|
+
const frames = [];
|
|
18457
|
+
let at = 0;
|
|
18458
|
+
for (; ; ) {
|
|
18459
|
+
const h = parseAdtsHeader(this.buffered, at);
|
|
18460
|
+
if (!h) {
|
|
18461
|
+
if (at + HEADER_LEN_NO_CRC > this.buffered.length)
|
|
18462
|
+
break;
|
|
18463
|
+
at++;
|
|
18464
|
+
continue;
|
|
18465
|
+
}
|
|
18466
|
+
if (at + h.frameLength > this.buffered.length)
|
|
18467
|
+
break;
|
|
18468
|
+
frames.push(this.buffered.subarray(at, at + h.frameLength));
|
|
18469
|
+
at += h.frameLength;
|
|
18470
|
+
}
|
|
18471
|
+
this.buffered = at ? this.buffered.subarray(at) : this.buffered;
|
|
18472
|
+
return frames;
|
|
18473
|
+
}
|
|
18474
|
+
/** Bytes held back awaiting the rest of their frame — non-zero mid-stream, zero on a clean boundary. */
|
|
18475
|
+
get pending() {
|
|
18476
|
+
return this.buffered.length;
|
|
18477
|
+
}
|
|
18478
|
+
};
|
|
18479
|
+
|
|
18480
|
+
// dist/transport/p2p/recording-download.js
|
|
18481
|
+
var RECORDING_DATA_TYPE = 3;
|
|
18482
|
+
var VIDEO_HEADER_LEN2 = 22;
|
|
18483
|
+
var AUDIO_HEADER_LEN2 = 16;
|
|
18484
|
+
var AUDIO_NONCE_START = 32;
|
|
18485
|
+
var AUDIO_BODY_START = 44;
|
|
18486
|
+
var AUDIO_CODEC_AAC_LC = 0;
|
|
18487
|
+
var FINISH_CHANNEL = 0;
|
|
18488
|
+
var FIRST_FRAME_WAIT_MS = 2e4;
|
|
18489
|
+
var IDLE_END_MS = 15e3;
|
|
18490
|
+
var DEFAULT_TRANSFER_MS = 12e4;
|
|
18491
|
+
var MAX_TRANSFER_BYTES = 32 * 1024 * 1024;
|
|
18492
|
+
function homeBase2RecordingPath(channel, recording) {
|
|
18493
|
+
if (!/^\d{14}$/.test(recording) || !Number.isInteger(channel) || channel < 0 || channel > 99)
|
|
18494
|
+
return void 0;
|
|
18495
|
+
return `/media/mmcblk0p1/Camera${String(channel).padStart(2, "0")}/${recording}.dat`;
|
|
18496
|
+
}
|
|
18497
|
+
function receiveRecording(session, request) {
|
|
18498
|
+
let resolveFrames;
|
|
18499
|
+
let rejectFrames;
|
|
18500
|
+
const frames = new Promise((resolve, reject) => {
|
|
18501
|
+
resolveFrames = resolve;
|
|
18502
|
+
rejectFrames = reject;
|
|
18503
|
+
});
|
|
18504
|
+
let resolveDrained;
|
|
18505
|
+
const drained = new Promise((resolve) => resolveDrained = resolve);
|
|
18506
|
+
const limitMs = request.timeoutMs ?? DEFAULT_TRANSFER_MS;
|
|
18507
|
+
let collected = [];
|
|
18508
|
+
const started = Date.now();
|
|
18509
|
+
let lastFrameAt = started;
|
|
18510
|
+
let received = false;
|
|
18511
|
+
let bytes2 = 0;
|
|
18512
|
+
let draining;
|
|
18513
|
+
let delivered = false;
|
|
18514
|
+
let listening = true;
|
|
18515
|
+
const deliver = (error) => {
|
|
18516
|
+
if (delivered)
|
|
18517
|
+
return;
|
|
18518
|
+
delivered = true;
|
|
18519
|
+
request.signal?.removeEventListener("abort", onAbort);
|
|
18520
|
+
if (error)
|
|
18521
|
+
rejectFrames(error);
|
|
18121
18522
|
else
|
|
18122
|
-
|
|
18523
|
+
resolveFrames(collected);
|
|
18524
|
+
};
|
|
18525
|
+
const stop = () => {
|
|
18526
|
+
if (!listening)
|
|
18527
|
+
return;
|
|
18528
|
+
listening = false;
|
|
18529
|
+
clearInterval(tick);
|
|
18530
|
+
session.off("data", onData);
|
|
18531
|
+
resolveDrained();
|
|
18532
|
+
};
|
|
18533
|
+
const incomplete = (why) => deliver(new RecordingDownloadError("incomplete", `the recording transfer ${why} before its finish frame`));
|
|
18534
|
+
const drain = (why) => {
|
|
18535
|
+
draining ??= why;
|
|
18536
|
+
collected = [];
|
|
18537
|
+
};
|
|
18538
|
+
const onData = (frame) => {
|
|
18539
|
+
if (frame.commandId === CommandType.CMD_DOWNLOAD_FINISH) {
|
|
18540
|
+
if (frame.channel !== request.channel && frame.channel !== FINISH_CHANNEL)
|
|
18541
|
+
return;
|
|
18542
|
+
if (draining)
|
|
18543
|
+
incomplete(draining);
|
|
18544
|
+
else
|
|
18545
|
+
deliver();
|
|
18546
|
+
return stop();
|
|
18547
|
+
}
|
|
18548
|
+
if (frame.channel !== request.channel)
|
|
18549
|
+
return;
|
|
18550
|
+
if (frame.commandId !== CommandType.CMD_VIDEO_FRAME && frame.commandId !== CommandType.CMD_AUDIO_FRAME)
|
|
18551
|
+
return;
|
|
18552
|
+
if (frame.dataType !== RECORDING_DATA_TYPE)
|
|
18553
|
+
return;
|
|
18554
|
+
received = true;
|
|
18555
|
+
lastFrameAt = Date.now();
|
|
18556
|
+
if (draining)
|
|
18557
|
+
return;
|
|
18558
|
+
collected.push({ commandId: frame.commandId, signCode: frame.signCode, raw: Buffer.from(frame.raw) });
|
|
18559
|
+
bytes2 += frame.raw.length;
|
|
18560
|
+
if (bytes2 >= MAX_TRANSFER_BYTES)
|
|
18561
|
+
drain(`reached ${MAX_TRANSFER_BYTES} bytes`);
|
|
18562
|
+
};
|
|
18563
|
+
const onAbort = () => {
|
|
18564
|
+
deliver(request.signal?.reason instanceof Error ? request.signal.reason : new Error("aborted"));
|
|
18565
|
+
drain("was aborted");
|
|
18566
|
+
};
|
|
18567
|
+
const tick = setInterval(() => {
|
|
18568
|
+
const now2 = Date.now();
|
|
18569
|
+
if (!received && now2 - started > FIRST_FRAME_WAIT_MS) {
|
|
18570
|
+
deliver(new RecordingDownloadError("no-data", `the station sent nothing within ${FIRST_FRAME_WAIT_MS} ms`));
|
|
18571
|
+
stop();
|
|
18572
|
+
} else if (received && now2 - lastFrameAt > IDLE_END_MS) {
|
|
18573
|
+
incomplete(draining ?? `went quiet for ${IDLE_END_MS} ms`);
|
|
18574
|
+
stop();
|
|
18575
|
+
} else if (now2 - started > 2 * limitMs && draining) {
|
|
18576
|
+
incomplete(draining);
|
|
18577
|
+
stop();
|
|
18578
|
+
} else if (now2 - started > limitMs) {
|
|
18579
|
+
drain(`ran past ${limitMs} ms`);
|
|
18580
|
+
}
|
|
18581
|
+
}, 250);
|
|
18582
|
+
if (request.signal?.aborted) {
|
|
18583
|
+
onAbort();
|
|
18584
|
+
stop();
|
|
18585
|
+
return { frames, drained };
|
|
18586
|
+
}
|
|
18587
|
+
request.signal?.addEventListener("abort", onAbort, { once: true });
|
|
18588
|
+
session.on("data", onData);
|
|
18589
|
+
try {
|
|
18590
|
+
session.sendStringPairCommand(CommandType.CMD_DOWNLOAD_VIDEO, request.path, request.accountId, request.channel);
|
|
18591
|
+
} catch (error) {
|
|
18592
|
+
deliver(error instanceof Error ? error : new Error(String(error)));
|
|
18593
|
+
stop();
|
|
18123
18594
|
}
|
|
18124
|
-
return
|
|
18595
|
+
return { frames, drained };
|
|
18125
18596
|
}
|
|
18126
|
-
function
|
|
18127
|
-
|
|
18128
|
-
|
|
18129
|
-
|
|
18130
|
-
|
|
18131
|
-
|
|
18132
|
-
|
|
18133
|
-
|
|
18134
|
-
|
|
18135
|
-
|
|
18136
|
-
|
|
18137
|
-
|
|
18138
|
-
|
|
18139
|
-
|
|
18140
|
-
|
|
18141
|
-
|
|
18142
|
-
|
|
18143
|
-
|
|
18144
|
-
|
|
18145
|
-
|
|
18146
|
-
|
|
18147
|
-
|
|
18148
|
-
|
|
18149
|
-
|
|
18150
|
-
|
|
18151
|
-
const
|
|
18152
|
-
|
|
18153
|
-
|
|
18154
|
-
|
|
18155
|
-
|
|
18156
|
-
const
|
|
18157
|
-
|
|
18158
|
-
|
|
18159
|
-
})
|
|
18160
|
-
|
|
18161
|
-
|
|
18162
|
-
|
|
18163
|
-
|
|
18164
|
-
|
|
18597
|
+
function openAudio(key, nonce, tag2, body) {
|
|
18598
|
+
try {
|
|
18599
|
+
const decipher = createDecipheriv7("aes-256-gcm", key, nonce);
|
|
18600
|
+
decipher.setAAD(VIDEO_GCM_AAD);
|
|
18601
|
+
decipher.setAuthTag(tag2);
|
|
18602
|
+
return Buffer.concat([decipher.update(body), decipher.final()]);
|
|
18603
|
+
} catch {
|
|
18604
|
+
return void 0;
|
|
18605
|
+
}
|
|
18606
|
+
}
|
|
18607
|
+
function decodeRecording(frames, eccPrivateKeyHex) {
|
|
18608
|
+
const eccPrivateKey = Buffer.from(eccPrivateKeyHex, "hex");
|
|
18609
|
+
if (eccPrivateKey.length !== 32)
|
|
18610
|
+
throw new RecordingDownloadError("undecodable", "the cipher key is not a P-256 key");
|
|
18611
|
+
const decoder = new VideoFrameDecoder(eccPrivateKey);
|
|
18612
|
+
const video = [];
|
|
18613
|
+
const audio = [];
|
|
18614
|
+
const numbers = /* @__PURE__ */ new Set();
|
|
18615
|
+
let firstStamp;
|
|
18616
|
+
let lastStamp;
|
|
18617
|
+
for (const { commandId, raw } of frames) {
|
|
18618
|
+
if (commandId === CommandType.CMD_VIDEO_FRAME) {
|
|
18619
|
+
const header = parseVideoFrameHeader(raw);
|
|
18620
|
+
if (!header)
|
|
18621
|
+
continue;
|
|
18622
|
+
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;
|
|
18623
|
+
if (!h264?.length)
|
|
18624
|
+
continue;
|
|
18625
|
+
video.push(h264);
|
|
18626
|
+
numbers.add(header.sequence);
|
|
18627
|
+
const stamp = raw.readUIntLE(14, 6);
|
|
18628
|
+
firstStamp ??= stamp;
|
|
18629
|
+
lastStamp = stamp;
|
|
18630
|
+
} else if (commandId === CommandType.CMD_AUDIO_FRAME) {
|
|
18631
|
+
const mediaKey = decoder.currentMediaKey;
|
|
18632
|
+
if (!mediaKey || raw.length < AUDIO_BODY_START || raw[5] !== AUDIO_CODEC_AAC_LC)
|
|
18633
|
+
continue;
|
|
18634
|
+
const len = raw.readUInt32LE(0);
|
|
18635
|
+
if (raw.length < AUDIO_BODY_START + len)
|
|
18636
|
+
continue;
|
|
18637
|
+
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));
|
|
18638
|
+
if (aac?.length)
|
|
18639
|
+
audio.push(buildAdtsHeader(aac.length), aac);
|
|
18640
|
+
}
|
|
18641
|
+
}
|
|
18642
|
+
if (!video.length)
|
|
18643
|
+
throw new RecordingDownloadError("undecodable", "no video frame of the recording decoded");
|
|
18644
|
+
const ordered = [...numbers];
|
|
18645
|
+
let missingFrames = 0;
|
|
18646
|
+
for (let i = 1; i < ordered.length; i++) {
|
|
18647
|
+
const step = ordered[i] - ordered[i - 1] & 65535;
|
|
18648
|
+
if (step > 1 && step < 32768)
|
|
18649
|
+
missingFrames += step - 1;
|
|
18650
|
+
}
|
|
18651
|
+
const span = ordered.length > 1 ? ordered[ordered.length - 1] - ordered[0] & 65535 : 0;
|
|
18652
|
+
const durationMs = (lastStamp ?? 0) - (firstStamp ?? 0);
|
|
18653
|
+
return {
|
|
18654
|
+
video: Buffer.concat(video),
|
|
18655
|
+
...audio.length ? { audio: Buffer.concat(audio) } : {},
|
|
18656
|
+
frames: ordered.length,
|
|
18657
|
+
missingFrames,
|
|
18658
|
+
durationMs,
|
|
18659
|
+
fps: durationMs > 0 && span > 0 ? Math.round(span * 1e5 / durationMs) / 100 : 0
|
|
18660
|
+
};
|
|
18165
18661
|
}
|
|
18166
18662
|
|
|
18167
18663
|
// dist/transport/p2p/shared-live-source.js
|
|
18168
18664
|
import { EventEmitter as EventEmitter4 } from "node:events";
|
|
18169
|
-
|
|
18170
|
-
|
|
18171
|
-
return Number.isFinite(
|
|
18665
|
+
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
18666
|
+
function timerMs(value, fallback) {
|
|
18667
|
+
return value !== void 0 && Number.isFinite(value) && value >= 1 && value <= MAX_TIMER_DELAY_MS ? value : fallback;
|
|
18172
18668
|
}
|
|
18173
18669
|
function sameConfig(a, b) {
|
|
18174
18670
|
return b !== void 0 && a.codec === b.codec && a.width === b.width && a.height === b.height;
|
|
@@ -18421,14 +18917,14 @@ var SharedLiveSource = class {
|
|
|
18421
18917
|
tag;
|
|
18422
18918
|
constructor(opts) {
|
|
18423
18919
|
this.opts = opts;
|
|
18424
|
-
this.lingerMs = opts.lingerMs
|
|
18920
|
+
this.lingerMs = opts.lingerMs === 0 ? 1 : timerMs(opts.lingerMs, 8e3);
|
|
18425
18921
|
this.maxQueue = opts.maxQueue ?? 900;
|
|
18426
|
-
this.preBufferMs =
|
|
18427
|
-
this.warmRetryMs = opts.warmRetryMs
|
|
18428
|
-
this.warmTimeoutMs = opts.warmTimeoutMs
|
|
18922
|
+
this.preBufferMs = timerMs((opts.preBufferSeconds ?? 0) * 1e3, 0);
|
|
18923
|
+
this.warmRetryMs = timerMs(opts.warmRetryMs, 2e3);
|
|
18924
|
+
this.warmTimeoutMs = timerMs(opts.warmTimeoutMs, 2e4);
|
|
18429
18925
|
this.powered = opts.powered ?? "wired";
|
|
18430
|
-
this.batteryBudgetMs = opts.batteryBudgetMs
|
|
18431
|
-
this.budgetGraceMs = opts.budgetGraceMs
|
|
18926
|
+
this.batteryBudgetMs = timerMs(opts.batteryBudgetMs, 45e3);
|
|
18927
|
+
this.budgetGraceMs = timerMs(opts.budgetGraceMs, 1e4);
|
|
18432
18928
|
this.logger = opts.logger ?? noopLogger;
|
|
18433
18929
|
this.tag = opts.label ? `[live ${opts.label}]` : "[live]";
|
|
18434
18930
|
this.traceId = `pull-${++pullSequence}`;
|
|
@@ -18676,7 +19172,7 @@ var SharedLiveSource = class {
|
|
|
18676
19172
|
if (this.disposed || !this.stream)
|
|
18677
19173
|
return;
|
|
18678
19174
|
this.clearBudget();
|
|
18679
|
-
this.budgetTimer.arm(ms
|
|
19175
|
+
this.budgetTimer.arm(timerMs(ms, this.batteryBudgetMs), () => this.onBudgetExpire());
|
|
18680
19176
|
}
|
|
18681
19177
|
/** Settle a reuse watch on any frame — the join already holds a decodable picture. */
|
|
18682
19178
|
settleReuseWatch() {
|
|
@@ -18817,7 +19313,7 @@ var SharedLiveSource = class {
|
|
|
18817
19313
|
return this.bufferedMedia(seconds).filter((item) => item.kind === "video").map((item) => item.frame);
|
|
18818
19314
|
}
|
|
18819
19315
|
bufferedMedia(seconds) {
|
|
18820
|
-
const requested = Math.min(
|
|
19316
|
+
const requested = Number.isFinite(seconds) ? Math.min(seconds * 1e3, this.preBufferMs) : 0;
|
|
18821
19317
|
if (requested <= 0 || !this.ring.length)
|
|
18822
19318
|
return [];
|
|
18823
19319
|
const start = this.windowStart(Date.now() - requested);
|
|
@@ -19286,89 +19782,6 @@ function openReadableFromConsumer(consumer, opts = {}) {
|
|
|
19286
19782
|
// dist/transport/p2p/talkback.js
|
|
19287
19783
|
import { EventEmitter as EventEmitter5 } from "node:events";
|
|
19288
19784
|
import { Writable } from "node:stream";
|
|
19289
|
-
|
|
19290
|
-
// dist/transport/p2p/adts.js
|
|
19291
|
-
var PROFILE_AAC_LC = 1;
|
|
19292
|
-
var FREQ_INDEX_16K = 8;
|
|
19293
|
-
var CHANNELS_MONO = 1;
|
|
19294
|
-
var AAC_SAMPLES_PER_FRAME = 1024;
|
|
19295
|
-
var AAC_SAMPLE_RATE = 16e3;
|
|
19296
|
-
var AAC_FRAME_MS = AAC_SAMPLES_PER_FRAME * 1e3 / AAC_SAMPLE_RATE;
|
|
19297
|
-
var MAX_AUDIO_FRAME_BYTES = 640;
|
|
19298
|
-
var HEADER_LEN_NO_CRC = 7;
|
|
19299
|
-
function parseAdtsHeader(buf, offset = 0) {
|
|
19300
|
-
if (offset + HEADER_LEN_NO_CRC > buf.length)
|
|
19301
|
-
return void 0;
|
|
19302
|
-
if (buf[offset] !== 255 || (buf[offset + 1] & 240) !== 240)
|
|
19303
|
-
return void 0;
|
|
19304
|
-
const headerLength = buf[offset + 1] & 1 ? 7 : 9;
|
|
19305
|
-
const frameLength = (buf[offset + 3] & 3) << 11 | buf[offset + 4] << 3 | (buf[offset + 5] & 224) >> 5;
|
|
19306
|
-
if (frameLength < headerLength)
|
|
19307
|
-
return void 0;
|
|
19308
|
-
return {
|
|
19309
|
-
frameLength,
|
|
19310
|
-
headerLength,
|
|
19311
|
-
profile: (buf[offset + 2] & 192) >> 6,
|
|
19312
|
-
frequencyIndex: (buf[offset + 2] & 60) >> 2,
|
|
19313
|
-
channels: (buf[offset + 2] & 1) << 2 | (buf[offset + 3] & 192) >> 6
|
|
19314
|
-
};
|
|
19315
|
-
}
|
|
19316
|
-
function isSupportedAdts(h) {
|
|
19317
|
-
return h.profile === PROFILE_AAC_LC && h.frequencyIndex === FREQ_INDEX_16K && h.channels === CHANNELS_MONO;
|
|
19318
|
-
}
|
|
19319
|
-
function describeAdts(h) {
|
|
19320
|
-
const rate = ADTS_SAMPLE_RATES[h.frequencyIndex] ?? `index ${h.frequencyIndex}`;
|
|
19321
|
-
const profile = h.profile === PROFILE_AAC_LC ? "AAC-LC" : `profile field ${h.profile}`;
|
|
19322
|
-
return `${profile}, ${rate} Hz, ${h.channels} channel(s)`;
|
|
19323
|
-
}
|
|
19324
|
-
var ADTS_SAMPLE_RATES = [
|
|
19325
|
-
96e3,
|
|
19326
|
-
88200,
|
|
19327
|
-
64e3,
|
|
19328
|
-
48e3,
|
|
19329
|
-
44100,
|
|
19330
|
-
32e3,
|
|
19331
|
-
24e3,
|
|
19332
|
-
22050,
|
|
19333
|
-
16e3,
|
|
19334
|
-
12e3,
|
|
19335
|
-
11025,
|
|
19336
|
-
8e3,
|
|
19337
|
-
7350
|
|
19338
|
-
];
|
|
19339
|
-
var AdtsFrameReader = class {
|
|
19340
|
-
buffered = Buffer.alloc(0);
|
|
19341
|
-
/**
|
|
19342
|
-
* Append `chunk` and return every complete frame now available, each a standalone buffer that still
|
|
19343
|
-
* carries its own ADTS header (the device expects the header on the wire).
|
|
19344
|
-
*/
|
|
19345
|
-
push(chunk) {
|
|
19346
|
-
this.buffered = this.buffered.length ? Buffer.concat([this.buffered, chunk]) : chunk;
|
|
19347
|
-
const frames = [];
|
|
19348
|
-
let at = 0;
|
|
19349
|
-
for (; ; ) {
|
|
19350
|
-
const h = parseAdtsHeader(this.buffered, at);
|
|
19351
|
-
if (!h) {
|
|
19352
|
-
if (at + HEADER_LEN_NO_CRC > this.buffered.length)
|
|
19353
|
-
break;
|
|
19354
|
-
at++;
|
|
19355
|
-
continue;
|
|
19356
|
-
}
|
|
19357
|
-
if (at + h.frameLength > this.buffered.length)
|
|
19358
|
-
break;
|
|
19359
|
-
frames.push(this.buffered.subarray(at, at + h.frameLength));
|
|
19360
|
-
at += h.frameLength;
|
|
19361
|
-
}
|
|
19362
|
-
this.buffered = at ? this.buffered.subarray(at) : this.buffered;
|
|
19363
|
-
return frames;
|
|
19364
|
-
}
|
|
19365
|
-
/** Bytes held back awaiting the rest of their frame — non-zero mid-stream, zero on a clean boundary. */
|
|
19366
|
-
get pending() {
|
|
19367
|
-
return this.buffered.length;
|
|
19368
|
-
}
|
|
19369
|
-
};
|
|
19370
|
-
|
|
19371
|
-
// dist/transport/p2p/talkback.js
|
|
19372
19785
|
var DEFAULT_HIGH_WATER_FRAMES = Math.ceil(2e3 / AAC_FRAME_MS);
|
|
19373
19786
|
var DEFAULT_IDLE_TIMEOUT_MS = 3e4;
|
|
19374
19787
|
var Talkback = class extends EventEmitter5 {
|
|
@@ -20344,6 +20757,8 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
20344
20757
|
talkbacks = /* @__PURE__ */ new Map();
|
|
20345
20758
|
/** cipher_id → ECC private key (one eufylife get_ciphers call per cipher), shared across (re)opens. */
|
|
20346
20759
|
cipherKeyCache = /* @__PURE__ */ new Map();
|
|
20760
|
+
/** The recording download in flight per station; a station serves one at a time. */
|
|
20761
|
+
recordingDownloads = /* @__PURE__ */ new Map();
|
|
20347
20762
|
constructor(deps) {
|
|
20348
20763
|
this.deps = deps;
|
|
20349
20764
|
this.manager = new SessionManager({
|
|
@@ -20556,28 +20971,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
20556
20971
|
localAddress,
|
|
20557
20972
|
dskKey,
|
|
20558
20973
|
noBroadcast: this.deps.noBroadcast,
|
|
20559
|
-
resolveCipherKey:
|
|
20560
|
-
if (this.cipherKeyCache.has(cipherId))
|
|
20561
|
-
return this.cipherKeyCache.get(cipherId);
|
|
20562
|
-
let ecc;
|
|
20563
|
-
try {
|
|
20564
|
-
const ciphers = await this.deps.mega.getCiphers([cipherId], adminUserId, stationSn);
|
|
20565
|
-
ecc = ciphers.find((c) => Number(c.cipher_id) === cipherId)?.ecc_private_key;
|
|
20566
|
-
if (ecc === void 0 && ciphers[0]?.ecc_private_key !== void 0) {
|
|
20567
|
-
ecc = ciphers[0].ecc_private_key;
|
|
20568
|
-
this.traceOnStation(session, {
|
|
20569
|
-
phase: "cipher-fallback",
|
|
20570
|
-
cipherId,
|
|
20571
|
-
answeredCipherId: Number(ciphers[0].cipher_id)
|
|
20572
|
-
});
|
|
20573
|
-
}
|
|
20574
|
-
} catch (e) {
|
|
20575
|
-
this.deps.onError(e instanceof Error ? e : new Error(String(e)));
|
|
20576
|
-
}
|
|
20577
|
-
if (ecc !== void 0)
|
|
20578
|
-
this.cipherKeyCache.set(cipherId, ecc);
|
|
20579
|
-
return ecc;
|
|
20580
|
-
},
|
|
20974
|
+
resolveCipherKey: (cipherId) => this.cipherKeyFor(session, cipherId, adminUserId, stationSn),
|
|
20581
20975
|
logger: this.deps.logger ?? noopLogger
|
|
20582
20976
|
});
|
|
20583
20977
|
session.on("error", (e) => this.reportError(e));
|
|
@@ -20601,6 +20995,33 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
20601
20995
|
session.on("data", (f) => this.deps.onFrame(stationSn, f));
|
|
20602
20996
|
return session;
|
|
20603
20997
|
}
|
|
20998
|
+
/**
|
|
20999
|
+
* The ECC private key of `cipherId`, from the router's cache or one `get_ciphers` call. When the cloud
|
|
21000
|
+
* answers with another cipher, that one is used and traced on `session`. Only a successful lookup is
|
|
21001
|
+
* cached.
|
|
21002
|
+
*/
|
|
21003
|
+
async cipherKeyFor(session, cipherId, userId, stationSn) {
|
|
21004
|
+
if (this.cipherKeyCache.has(cipherId))
|
|
21005
|
+
return this.cipherKeyCache.get(cipherId);
|
|
21006
|
+
let ecc;
|
|
21007
|
+
try {
|
|
21008
|
+
const ciphers = await this.deps.mega.getCiphers([cipherId], userId, stationSn);
|
|
21009
|
+
ecc = ciphers.find((c) => Number(c.cipher_id) === cipherId)?.ecc_private_key;
|
|
21010
|
+
if (ecc === void 0 && ciphers[0]?.ecc_private_key !== void 0) {
|
|
21011
|
+
ecc = ciphers[0].ecc_private_key;
|
|
21012
|
+
this.traceOnStation(session, {
|
|
21013
|
+
phase: "cipher-fallback",
|
|
21014
|
+
cipherId,
|
|
21015
|
+
answeredCipherId: Number(ciphers[0].cipher_id)
|
|
21016
|
+
});
|
|
21017
|
+
}
|
|
21018
|
+
} catch (e) {
|
|
21019
|
+
this.deps.onError(e instanceof Error ? e : new Error(String(e)));
|
|
21020
|
+
}
|
|
21021
|
+
if (ecc !== void 0)
|
|
21022
|
+
this.cipherKeyCache.set(cipherId, ecc);
|
|
21023
|
+
return ecc;
|
|
21024
|
+
}
|
|
20604
21025
|
/**
|
|
20605
21026
|
* Open (or reuse) a station's P2P session and await its completed handshake. An optional abort only
|
|
20606
21027
|
* stops this wait; session ownership remains with {@link SessionManager} and its normal teardown.
|
|
@@ -20728,7 +21149,9 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
20728
21149
|
* start has no level-1 form for.
|
|
20729
21150
|
*/
|
|
20730
21151
|
mediaProviderFor(sn) {
|
|
21152
|
+
const recordings = this.downloadsRecordings(sn) ? { downloadRecording: (opts) => this.downloadRecording(sn, opts) } : {};
|
|
20731
21153
|
return {
|
|
21154
|
+
...recordings,
|
|
20732
21155
|
snapshotLive: async (opts) => {
|
|
20733
21156
|
const source = await this.sharedLiveSourceFor(sn, opts ?? {});
|
|
20734
21157
|
return captureSnapshotFromShared(source, {
|
|
@@ -20767,6 +21190,63 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
20767
21190
|
}
|
|
20768
21191
|
};
|
|
20769
21192
|
}
|
|
21193
|
+
/**
|
|
21194
|
+
* Whether `sn` is a camera attached to a HomeBase 2 (T8010), the one station whose recording path layout
|
|
21195
|
+
* and frame formats are confirmed.
|
|
21196
|
+
*/
|
|
21197
|
+
downloadsRecordings(sn) {
|
|
21198
|
+
const dev = this.recordFor(sn);
|
|
21199
|
+
if (!dev)
|
|
21200
|
+
return false;
|
|
21201
|
+
const parentSn = stationOf(dev);
|
|
21202
|
+
return parentSn !== sn && this.recordFor(parentSn)?.model === "T8010";
|
|
21203
|
+
}
|
|
21204
|
+
/**
|
|
21205
|
+
* Download one recording a HomeBase 2 holds for an attached camera, then decode it
|
|
21206
|
+
* ({@link decodeRecording}). Downloads queue per station: the station streams one recording at a time
|
|
21207
|
+
* on the camera's channel, so a download starts only once the previous one has drained
|
|
21208
|
+
* ({@link RecordingTransfer.drained}), even when that one's caller already has its answer. An abort
|
|
21209
|
+
* while waiting rejects without sending anything; an abort mid-transfer rejects at once and the station's
|
|
21210
|
+
* turn is held until the transfer drains.
|
|
21211
|
+
*/
|
|
21212
|
+
async downloadRecording(sn, opts) {
|
|
21213
|
+
opts.signal?.throwIfAborted();
|
|
21214
|
+
const parentSn = this.stationKeyOf(sn);
|
|
21215
|
+
const previous = this.recordingDownloads.get(parentSn) ?? Promise.resolve();
|
|
21216
|
+
const started = this.waitTurn(previous, opts.signal).then(() => this.startRecordingDownload(sn, opts));
|
|
21217
|
+
const turn = Promise.allSettled([previous, started.then((t) => t.drained)]).then(() => {
|
|
21218
|
+
if (this.recordingDownloads.get(parentSn) === turn)
|
|
21219
|
+
this.recordingDownloads.delete(parentSn);
|
|
21220
|
+
});
|
|
21221
|
+
this.recordingDownloads.set(parentSn, turn);
|
|
21222
|
+
return started.then((t) => t.recording);
|
|
21223
|
+
}
|
|
21224
|
+
/** Resolve once `previous` settles, or reject as soon as `signal` aborts. */
|
|
21225
|
+
waitTurn(previous, signal) {
|
|
21226
|
+
return abortable(previous.then(() => void 0, () => void 0), signal);
|
|
21227
|
+
}
|
|
21228
|
+
/**
|
|
21229
|
+
* Start one recording download, once the station's previous download has drained: the decoded recording,
|
|
21230
|
+
* and when the station is done sending it.
|
|
21231
|
+
*/
|
|
21232
|
+
async startRecordingDownload(sn, opts) {
|
|
21233
|
+
const { session, parentSn, channel, accountId } = await this.resolveSession(sn, { signal: opts.signal });
|
|
21234
|
+
const path = homeBase2RecordingPath(channel, opts.recording);
|
|
21235
|
+
if (!path)
|
|
21236
|
+
throw new RecordingDownloadError("invalid-recording", `no recording named ${opts.recording}`);
|
|
21237
|
+
const eccKey = await this.cipherKeyFor(session, opts.cipherId, accountId, parentSn);
|
|
21238
|
+
if (!eccKey) {
|
|
21239
|
+
throw new RecordingDownloadError("key-unavailable", `cipher ${opts.cipherId} could not be obtained`);
|
|
21240
|
+
}
|
|
21241
|
+
const transfer = receiveRecording(session, {
|
|
21242
|
+
path,
|
|
21243
|
+
accountId,
|
|
21244
|
+
channel,
|
|
21245
|
+
timeoutMs: opts.timeoutMs,
|
|
21246
|
+
signal: opts.signal
|
|
21247
|
+
});
|
|
21248
|
+
return { recording: transfer.frames.then((frames) => decodeRecording(frames, eccKey)), drained: transfer.drained };
|
|
21249
|
+
}
|
|
20770
21250
|
/**
|
|
20771
21251
|
* Open a {@link Talkback} on a device's camera channel.
|
|
20772
21252
|
*
|
|
@@ -24292,6 +24772,7 @@ var MEDIA_FAILURE_REASONS = [
|
|
|
24292
24772
|
// dist/transport/stored-image-cache.js
|
|
24293
24773
|
var MAX_JPEG_BYTES = 10 * 1024 * 1024;
|
|
24294
24774
|
var MAX_REMEMBERED_URLS = 64;
|
|
24775
|
+
var NOT_YET_PUBLISHED_RETRY_MS = [2e3, 4e3];
|
|
24295
24776
|
var DIAGNOSTIC_INTERVAL_MS = 6e4;
|
|
24296
24777
|
var StoredImageCache = class {
|
|
24297
24778
|
downloader;
|
|
@@ -24329,6 +24810,8 @@ var StoredImageCache = class {
|
|
|
24329
24810
|
if (state.seenUrls.has(url))
|
|
24330
24811
|
return;
|
|
24331
24812
|
state.seenUrls.add(url);
|
|
24813
|
+
clearTimeout(state.retrying);
|
|
24814
|
+
state.retrying = void 0;
|
|
24332
24815
|
while (state.seenUrls.size > MAX_REMEMBERED_URLS) {
|
|
24333
24816
|
const oldest = state.seenUrls.values().next();
|
|
24334
24817
|
if (oldest.done)
|
|
@@ -24340,14 +24823,15 @@ var StoredImageCache = class {
|
|
|
24340
24823
|
url,
|
|
24341
24824
|
sequence: ++state.nextSequence,
|
|
24342
24825
|
observedAt: this.clock(),
|
|
24343
|
-
generation: this.generation
|
|
24826
|
+
generation: this.generation,
|
|
24827
|
+
retries: 0
|
|
24344
24828
|
};
|
|
24345
24829
|
this.pump();
|
|
24346
24830
|
}
|
|
24347
24831
|
/** Return retained bytes without starting or awaiting network work. */
|
|
24348
24832
|
snapshotStored(deviceKey) {
|
|
24349
24833
|
const state = this.devices.get(deviceKey);
|
|
24350
|
-
const reason = state?.queued || state?.inFlight ? "pending" : state?.reason ?? "not-observed";
|
|
24834
|
+
const reason = state?.queued || state?.inFlight || state?.retrying ? "pending" : state?.reason ?? "not-observed";
|
|
24351
24835
|
if (reason !== "pending" && state?.lifecycleError)
|
|
24352
24836
|
return Promise.reject(state.lifecycleError);
|
|
24353
24837
|
if (state?.retained)
|
|
@@ -24381,6 +24865,8 @@ var StoredImageCache = class {
|
|
|
24381
24865
|
if (error !== void 0 && this.isLifecycleError(error)) {
|
|
24382
24866
|
state.lifecycleError = error;
|
|
24383
24867
|
state.reason = void 0;
|
|
24868
|
+
} else if (image === void 0 && this.retryLater(state, candidate, error)) {
|
|
24869
|
+
state.lifecycleError = void 0;
|
|
24384
24870
|
} else if (image === void 0) {
|
|
24385
24871
|
state.lifecycleError = void 0;
|
|
24386
24872
|
state.reason = "download-failed";
|
|
@@ -24397,6 +24883,21 @@ var StoredImageCache = class {
|
|
|
24397
24883
|
}
|
|
24398
24884
|
this.pump();
|
|
24399
24885
|
}
|
|
24886
|
+
/** Schedule the next attempt after a 404; answers whether one was scheduled. */
|
|
24887
|
+
retryLater(state, candidate, error) {
|
|
24888
|
+
const delay = NOT_YET_PUBLISHED_RETRY_MS[candidate.retries];
|
|
24889
|
+
const tag2 = mediaFailureTag(error);
|
|
24890
|
+
if (delay === void 0 || tag2.cause !== "http-status" || tag2.status !== 404 || state.queued)
|
|
24891
|
+
return false;
|
|
24892
|
+
const timer = setTimeout(() => {
|
|
24893
|
+
state.retrying = void 0;
|
|
24894
|
+
state.queued = { ...candidate, retries: candidate.retries + 1 };
|
|
24895
|
+
this.pump();
|
|
24896
|
+
}, delay);
|
|
24897
|
+
timer.unref?.();
|
|
24898
|
+
state.retrying = timer;
|
|
24899
|
+
return true;
|
|
24900
|
+
}
|
|
24400
24901
|
isValidJpeg(image) {
|
|
24401
24902
|
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;
|
|
24402
24903
|
}
|
|
@@ -24992,6 +25493,11 @@ function recordString(raw, key) {
|
|
|
24992
25493
|
const v = typeof nested === "string" && nested ? nested : raw[key];
|
|
24993
25494
|
return typeof v === "string" && v ? v : void 0;
|
|
24994
25495
|
}
|
|
25496
|
+
function adminUserIdFrom(raw) {
|
|
25497
|
+
const member = (raw ?? {}).member;
|
|
25498
|
+
const id = member?.admin_user_id;
|
|
25499
|
+
return typeof id === "string" && id ? id : void 0;
|
|
25500
|
+
}
|
|
24995
25501
|
function tuyaDevIdFrom(raw) {
|
|
24996
25502
|
for (const field of ["tuya_uuid", "tuya_virtual_id", "tuya_device_id", "virtualId"]) {
|
|
24997
25503
|
const v = recordString(raw, field);
|
|
@@ -25149,13 +25655,7 @@ var EufyMega = class extends EventEmitter9 {
|
|
|
25149
25655
|
onError: (e) => this.reportError(e),
|
|
25150
25656
|
// The `a2` account id an `eufy_life` DP frame embeds — the owning member's `admin_user_id`,
|
|
25151
25657
|
// falling back to the logged-in account's user id (the confirmed script path does the same).
|
|
25152
|
-
resolveAccountId: (dev) =>
|
|
25153
|
-
const member = (dev.raw ?? {}).member;
|
|
25154
|
-
const adminId = member?.admin_user_id;
|
|
25155
|
-
if (typeof adminId === "string" && adminId)
|
|
25156
|
-
return adminId;
|
|
25157
|
-
return this.mega.auth?.userId ?? "";
|
|
25158
|
-
},
|
|
25658
|
+
resolveAccountId: (dev) => adminUserIdFrom(dev.raw) ?? this.mega.auth?.userId ?? "",
|
|
25159
25659
|
// Fetch + parse a gallery effect from the HTTP catalogue into the serializable spec. The catalogue
|
|
25160
25660
|
// lives in the http layer (transport/http/light-catalog); the mqtt router owns the frame bytes.
|
|
25161
25661
|
resolvePreset: (presetId) => resolveLightEffect(this.mega, presetId),
|
|
@@ -26602,6 +27102,37 @@ var EufyMega = class extends EventEmitter9 {
|
|
|
26602
27102
|
getP2pSessions() {
|
|
26603
27103
|
return this.p2p.getSessions();
|
|
26604
27104
|
}
|
|
27105
|
+
/**
|
|
27106
|
+
* The people enrolled on a station, read from its own `person_basic_info` table over P2P.
|
|
27107
|
+
*
|
|
27108
|
+
* The cloud roster (`api.getFaces()`) answers empty for an account that keeps its faces on the
|
|
27109
|
+
* station, because they were never uploaded — the station holds them.
|
|
27110
|
+
*
|
|
27111
|
+
* Resolves with the rows the station returned. Rejects on a station that cannot be reached, on a
|
|
27112
|
+
* record stating no `admin_user_id`, and on the reply not arriving whole in time. Assembling that
|
|
27113
|
+
* reply is `P2PSession.readDatabase`'s; this resolves which station and which account id.
|
|
27114
|
+
*
|
|
27115
|
+
* `stranger<n>` names are the station's own placeholders for a face nobody has named, and are
|
|
27116
|
+
* returned as they arrive; a caller that wants only named people filters them.
|
|
27117
|
+
*/
|
|
27118
|
+
async getStationFaces(stationSn, opts = {}) {
|
|
27119
|
+
if (!this.registry.list().length)
|
|
27120
|
+
await this.getDevices();
|
|
27121
|
+
const station = this.p2p.stationKeyOf(stationSn);
|
|
27122
|
+
const accountId = this.adminUserIdOf(station);
|
|
27123
|
+
if (!accountId)
|
|
27124
|
+
throw new Error(`getStationFaces: no admin_user_id on record for ${station}`);
|
|
27125
|
+
await this.p2p.ensureStation(station, opts.signal);
|
|
27126
|
+
const session = this.p2p.getSessions().get(station);
|
|
27127
|
+
if (!session)
|
|
27128
|
+
throw new Error(`getStationFaces: no P2P session for ${station}`);
|
|
27129
|
+
const rows = await session.readDatabase("person_basic_info", { accountId, ...opts });
|
|
27130
|
+
return rows.filter((row) => typeof row === "object" && row !== null);
|
|
27131
|
+
}
|
|
27132
|
+
/** The station's own `admin_user_id`, which its database refuses the query without. */
|
|
27133
|
+
adminUserIdOf(stationSn) {
|
|
27134
|
+
return adminUserIdFrom(this.registry.list().find((d) => d.sn === stationSn)?.raw);
|
|
27135
|
+
}
|
|
26605
27136
|
/**
|
|
26606
27137
|
* The liveness facts for one device — see {@link DeviceState}. Facts, not an `online` verdict: "how
|
|
26607
27138
|
* long is too long" is a threshold that belongs to the caller, and it differs per device class.
|
|
@@ -26839,7 +27370,7 @@ var EufyMega = class extends EventEmitter9 {
|
|
|
26839
27370
|
if (ev.eventName === void 0 && ev.eventType != null)
|
|
26840
27371
|
ev.eventName = detectionName(ev.eventType);
|
|
26841
27372
|
if (ev.thumbnailCandidate)
|
|
26842
|
-
void this.observeStoredImage(ev.thumbnailCandidate);
|
|
27373
|
+
void this.observeStoredImage(ev.thumbnailCandidate, ev.deviceSn);
|
|
26843
27374
|
this.emit("push", ev);
|
|
26844
27375
|
const signal2 = {
|
|
26845
27376
|
source: "push",
|
|
@@ -26888,9 +27419,16 @@ var EufyMega = class extends EventEmitter9 {
|
|
|
26888
27419
|
client.on("error", (e) => this.reportError(e));
|
|
26889
27420
|
return client;
|
|
26890
27421
|
}
|
|
26891
|
-
/**
|
|
26892
|
-
|
|
26893
|
-
|
|
27422
|
+
/**
|
|
27423
|
+
* Admit only exact, account-known devices with resolved snapshot evidence into the passive store; a
|
|
27424
|
+
* station-only candidate is admitted under the event's device serial.
|
|
27425
|
+
*/
|
|
27426
|
+
async observeStoredImage(candidate, eventDeviceSn) {
|
|
27427
|
+
if (!this.storedImages)
|
|
27428
|
+
return;
|
|
27429
|
+
const { attribution } = candidate;
|
|
27430
|
+
const deviceSn = attribution.kind === "device" ? attribution.deviceSn : attribution.kind === "station" ? eventDeviceSn : void 0;
|
|
27431
|
+
if (!deviceSn)
|
|
26894
27432
|
return;
|
|
26895
27433
|
const account = this.mega.auth?.userId;
|
|
26896
27434
|
if (!account)
|
|
@@ -26900,9 +27438,9 @@ var EufyMega = class extends EventEmitter9 {
|
|
|
26900
27438
|
await this.registry.getDevices();
|
|
26901
27439
|
if (this.mega.auth?.userId !== account)
|
|
26902
27440
|
return;
|
|
26903
|
-
const caps = this.registry.capabilitiesForDevice(
|
|
27441
|
+
const caps = this.registry.capabilitiesForDevice(deviceSn);
|
|
26904
27442
|
if (caps && hasProvidedAction(caps, "snapshotStored")) {
|
|
26905
|
-
this.storedImages.observe(
|
|
27443
|
+
this.storedImages.observe(deviceSn, candidate.url);
|
|
26906
27444
|
}
|
|
26907
27445
|
} catch (e) {
|
|
26908
27446
|
this.reportError(e);
|
|
@@ -27127,26 +27665,16 @@ var SolixClient = class _SolixClient {
|
|
|
27127
27665
|
}
|
|
27128
27666
|
/** Build the encrypted, signed `/passport/login` request body + headers for the negotiated key. */
|
|
27129
27667
|
async postLogin(kx, verifyCode, limitedToken) {
|
|
27130
|
-
const { clientPublicKeyHex, encryptedPassword } = encryptLoginPassword(this.password);
|
|
27131
27668
|
const bodyObj = {
|
|
27132
|
-
email
|
|
27133
|
-
password: encryptedPassword,
|
|
27134
|
-
ab: this.country,
|
|
27135
|
-
client_secret_info: { public_key: clientPublicKeyHex },
|
|
27669
|
+
...loginCredentials(this.email, this.password, this.country),
|
|
27136
27670
|
answer: "",
|
|
27137
27671
|
captcha_id: "",
|
|
27138
27672
|
verify_code: verifyCode ?? "",
|
|
27139
27673
|
login_id: ""
|
|
27140
27674
|
};
|
|
27141
27675
|
const encBody = encryptBody(JSON.stringify(bodyObj), kx.shareKey);
|
|
27142
|
-
const ts = nowSec();
|
|
27143
|
-
const once = genId();
|
|
27144
27676
|
return this.post(this.apiHost, SOLIX_ENDPOINTS.login, encBody, this.authHeaders({
|
|
27145
|
-
|
|
27146
|
-
"x-key-ident": kx.keyIdent,
|
|
27147
|
-
"x-request-ts": ts,
|
|
27148
|
-
"x-request-once": once,
|
|
27149
|
-
"x-signature": signRequest(kx.shareKey, ts, once, encBody),
|
|
27677
|
+
...signedHeaders(kx, encBody),
|
|
27150
27678
|
...limitedToken ? { "x-auth-token": limitedToken } : {}
|
|
27151
27679
|
}));
|
|
27152
27680
|
}
|
|
@@ -27161,13 +27689,12 @@ var SolixClient = class _SolixClient {
|
|
|
27161
27689
|
* observed refusing the header, which is consistent with the two ids agreeing on the accounts seen.
|
|
27162
27690
|
*/
|
|
27163
27691
|
classifyLogin(data, isVerify) {
|
|
27164
|
-
const
|
|
27165
|
-
|
|
27166
|
-
if (!userId || !authToken)
|
|
27692
|
+
const reply = readLoginReply(data);
|
|
27693
|
+
if (!reply)
|
|
27167
27694
|
throw new Error(`Solix login returned no session: ${JSON.stringify(data).slice(0, 160)}`);
|
|
27168
|
-
const
|
|
27169
|
-
if (!isVerify &&
|
|
27170
|
-
this.pending2fa = { limitedToken: authToken, userId, geoKey:
|
|
27695
|
+
const { userId, authToken } = reply;
|
|
27696
|
+
if (!isVerify && reply.twoFactorPending) {
|
|
27697
|
+
this.pending2fa = { limitedToken: authToken, userId, geoKey: reply.geoKey };
|
|
27171
27698
|
return { status: "2fa", method: "code sent by the passport" };
|
|
27172
27699
|
}
|
|
27173
27700
|
this.pending2fa = void 0;
|
|
@@ -27176,7 +27703,7 @@ var SolixClient = class _SolixClient {
|
|
|
27176
27703
|
userId,
|
|
27177
27704
|
gtoken: gtoken(userId),
|
|
27178
27705
|
apiHost: this.apiHost,
|
|
27179
|
-
tokenExpiresAt:
|
|
27706
|
+
tokenExpiresAt: reply.tokenExpiresAt
|
|
27180
27707
|
};
|
|
27181
27708
|
this.persist();
|
|
27182
27709
|
return { status: "ok", session: this.session_ };
|
|
@@ -27331,14 +27858,8 @@ var SolixClient = class _SolixClient {
|
|
|
27331
27858
|
throw new Error("not authenticated \u2014 call login() first");
|
|
27332
27859
|
const kx = await this.keyExchange();
|
|
27333
27860
|
const encBody = encryptBody(JSON.stringify(payload), kx.shareKey);
|
|
27334
|
-
const ts = nowSec();
|
|
27335
|
-
const once = genId();
|
|
27336
27861
|
const env = await this.post(this.apiHost, path, encBody, this.baseHeaders({
|
|
27337
|
-
|
|
27338
|
-
"x-key-ident": kx.keyIdent,
|
|
27339
|
-
"x-request-ts": ts,
|
|
27340
|
-
"x-request-once": once,
|
|
27341
|
-
"x-signature": signRequest(kx.shareKey, ts, once, encBody),
|
|
27862
|
+
...signedHeaders(kx, encBody),
|
|
27342
27863
|
"x-auth-token": this.session_.authToken,
|
|
27343
27864
|
gtoken: this.session_.gtoken
|
|
27344
27865
|
}));
|
|
@@ -28106,6 +28627,7 @@ export {
|
|
|
28106
28627
|
ROOM_SUCTIONS,
|
|
28107
28628
|
RTSP_MEMBERS,
|
|
28108
28629
|
RawDpWriter,
|
|
28630
|
+
RecordingDownloadError,
|
|
28109
28631
|
RecordingQuality,
|
|
28110
28632
|
RtspRecordingMode,
|
|
28111
28633
|
SCENE_INVALID_REASONS,
|
|
@@ -28149,6 +28671,7 @@ export {
|
|
|
28149
28671
|
VACUUM_DOCK_MEMBERS,
|
|
28150
28672
|
VACUUM_SCHEDULE_ACTIONS,
|
|
28151
28673
|
VACUUM_SCHEDULE_WEEKDAYS,
|
|
28674
|
+
VIDEO_GCM_AAD,
|
|
28152
28675
|
VacuumMapStore,
|
|
28153
28676
|
VideoFrameDecoder,
|
|
28154
28677
|
WORKING_MODE_MAPS,
|