@mega-yfue/eufy-sdk 0.6.0-beta.14 → 0.6.0-beta.15
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 +42 -8
- package/dist/client/types.d.ts +3 -3
- package/dist/core/contracts.d.ts +3 -1
- package/dist/core/store.d.ts +12 -1
- package/dist/index.js +490 -215
- package/dist/index.js.map +4 -4
- package/dist/transport/http/mega-client.d.ts +3 -2
- package/dist/transport/p2p/codec.d.ts +2 -2
- package/dist/transport/p2p/command-router.d.ts +17 -0
- package/dist/transport/p2p/database-wire.d.ts +30 -0
- package/dist/transport/p2p/live-trace.d.ts +4 -4
- package/dist/transport/p2p/p2p-session.d.ts +25 -15
- package/dist/transport/p2p/readable-egress.d.ts +3 -1
- package/dist/transport/p2p/shared-live-source.d.ts +2 -1
- package/dist/transport/push/store.d.ts +7 -6
- package/dist/transport/rtc/command-router.d.ts +63 -3
- package/dist/transport/rtc/portal-packet.d.ts +3 -13
- package/dist/transport/stored-image-cache.d.ts +14 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -106,8 +106,9 @@ function encryptLoginPassword(password) {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
// dist/core/store.js
|
|
109
|
-
import {
|
|
110
|
-
import {
|
|
109
|
+
import { randomUUID } from "node:crypto";
|
|
110
|
+
import { lstatSync, mkdirSync, readFileSync, readlinkSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
111
|
+
import { dirname, resolve } from "node:path";
|
|
111
112
|
var MemorySessionStore = class {
|
|
112
113
|
s = null;
|
|
113
114
|
load() {
|
|
@@ -120,6 +121,22 @@ var MemorySessionStore = class {
|
|
|
120
121
|
this.s = null;
|
|
121
122
|
}
|
|
122
123
|
};
|
|
124
|
+
var IN_PLACE_CODES = /* @__PURE__ */ new Set(["EACCES", "EPERM", "EBUSY", "EXDEV", "EROFS"]);
|
|
125
|
+
function realTarget(path) {
|
|
126
|
+
try {
|
|
127
|
+
return realpathSync(path);
|
|
128
|
+
} catch (e) {
|
|
129
|
+
if (e.code !== "ENOENT")
|
|
130
|
+
throw e;
|
|
131
|
+
try {
|
|
132
|
+
if (!lstatSync(path).isSymbolicLink())
|
|
133
|
+
return path;
|
|
134
|
+
} catch {
|
|
135
|
+
return path;
|
|
136
|
+
}
|
|
137
|
+
return realTarget(resolve(dirname(path), readlinkSync(path)));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
123
140
|
var FileSessionStore = class {
|
|
124
141
|
path;
|
|
125
142
|
constructor(path) {
|
|
@@ -133,11 +150,22 @@ var FileSessionStore = class {
|
|
|
133
150
|
}
|
|
134
151
|
}
|
|
135
152
|
save(s) {
|
|
153
|
+
const target = realTarget(this.path);
|
|
136
154
|
try {
|
|
137
|
-
mkdirSync(dirname(
|
|
155
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
138
156
|
} catch {
|
|
139
157
|
}
|
|
140
|
-
|
|
158
|
+
const data = JSON.stringify(s, null, 2);
|
|
159
|
+
const tmp = `${target}.${randomUUID()}.tmp`;
|
|
160
|
+
try {
|
|
161
|
+
writeFileSync(tmp, data, { mode: 384, flag: "wx" });
|
|
162
|
+
renameSync(tmp, target);
|
|
163
|
+
} catch (e) {
|
|
164
|
+
rmSync(tmp, { force: true });
|
|
165
|
+
if (!IN_PLACE_CODES.has(e.code ?? ""))
|
|
166
|
+
throw e;
|
|
167
|
+
writeFileSync(target, data, { mode: 384 });
|
|
168
|
+
}
|
|
141
169
|
}
|
|
142
170
|
clear() {
|
|
143
171
|
try {
|
|
@@ -1973,22 +2001,22 @@ ${this.auth_?.authToken ?? ""}`;
|
|
|
1973
2001
|
return this.postSigned(`app-${service}-${this.region}.eufy.com`, path, body, true, {}, headerOverrides);
|
|
1974
2002
|
}
|
|
1975
2003
|
/**
|
|
1976
|
-
* Fetch the per-station DSK key
|
|
2004
|
+
* Fetch the per-station DSK key using each station's reported cloud category. Mirrors
|
|
1977
2005
|
* the legacy `get_dsk_keys` body shape on the mega devicerelation service.
|
|
1978
2006
|
* The P2P *local* lookup path does not need this; only the cloud path does.
|
|
1979
2007
|
*/
|
|
1980
|
-
async getDskKeys(
|
|
2008
|
+
async getDskKeys(stations, priorDsks = {}) {
|
|
1981
2009
|
const invalid = {};
|
|
1982
2010
|
const deviceDsks = [];
|
|
1983
|
-
for (const sn of
|
|
2011
|
+
for (const { sn, category } of stations) {
|
|
1984
2012
|
const prev = priorDsks[sn] ?? "";
|
|
1985
2013
|
invalid[sn] = prev;
|
|
1986
|
-
deviceDsks.push({ invalid_dsk: prev, device_sn: sn, category: "eufy_security" });
|
|
2014
|
+
deviceDsks.push({ invalid_dsk: prev, device_sn: sn, category: category || "eufy_security" });
|
|
1987
2015
|
}
|
|
1988
2016
|
const res = await this.post("devicerelation", "/app/devicerelation/get_dsk_keys", {
|
|
1989
2017
|
device_dsks: deviceDsks,
|
|
1990
2018
|
invalid_dsks: invalid,
|
|
1991
|
-
station_sns:
|
|
2019
|
+
station_sns: stations.map(({ sn }) => sn)
|
|
1992
2020
|
});
|
|
1993
2021
|
const out = {};
|
|
1994
2022
|
for (const k of res.device_dsks ?? [])
|
|
@@ -2665,7 +2693,7 @@ var SecureMqtt = class extends EventEmitter {
|
|
|
2665
2693
|
const port = c.endpoint_port ?? 8883;
|
|
2666
2694
|
const pinned = this.o.instanceIp;
|
|
2667
2695
|
const reconnectPeriod = this.o.reconnectPeriod ?? 5e3;
|
|
2668
|
-
return await new Promise((
|
|
2696
|
+
return await new Promise((resolve2, reject) => {
|
|
2669
2697
|
const client = pinned ? mqtt.connect({
|
|
2670
2698
|
host: pinned,
|
|
2671
2699
|
port,
|
|
@@ -2697,7 +2725,7 @@ var SecureMqtt = class extends EventEmitter {
|
|
|
2697
2725
|
client.on("connect", () => {
|
|
2698
2726
|
if (!settled) {
|
|
2699
2727
|
settled = true;
|
|
2700
|
-
|
|
2728
|
+
resolve2();
|
|
2701
2729
|
}
|
|
2702
2730
|
this.emit("connect");
|
|
2703
2731
|
});
|
|
@@ -2840,7 +2868,7 @@ function parseStateInfoSignal(topic, raw) {
|
|
|
2840
2868
|
}
|
|
2841
2869
|
|
|
2842
2870
|
// dist/transport/mqtt/dp-codec.js
|
|
2843
|
-
import { randomUUID } from "node:crypto";
|
|
2871
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
2844
2872
|
var DP_MAGIC = [255, 9];
|
|
2845
2873
|
var DP_HEADER_LEN = 9;
|
|
2846
2874
|
function xorChecksum(buf) {
|
|
@@ -2878,7 +2906,7 @@ function buildDpEnvelope(opts) {
|
|
|
2878
2906
|
};
|
|
2879
2907
|
const head = {
|
|
2880
2908
|
version: "1.0.0.1",
|
|
2881
|
-
client_id: `ios-eufy_mega-${opts.accountId}-${
|
|
2909
|
+
client_id: `ios-eufy_mega-${opts.accountId}-${randomUUID2()}`,
|
|
2882
2910
|
sess_id: "0000",
|
|
2883
2911
|
msg_seq: 1,
|
|
2884
2912
|
seed: "",
|
|
@@ -15560,7 +15588,7 @@ function readAddress(buf, offset) {
|
|
|
15560
15588
|
};
|
|
15561
15589
|
}
|
|
15562
15590
|
function parseLookupAddr(msg) {
|
|
15563
|
-
return readAddress(msg, 4);
|
|
15591
|
+
return msg.length >= 12 ? readAddress(msg, 4) : void 0;
|
|
15564
15592
|
}
|
|
15565
15593
|
function parseRelayList(msg) {
|
|
15566
15594
|
const out = [];
|
|
@@ -16209,6 +16237,27 @@ function traceLiveStart(logger, trace, source) {
|
|
|
16209
16237
|
// dist/transport/p2p/level2-settle.js
|
|
16210
16238
|
var LEVEL2_SETTLE_MS = 8e3;
|
|
16211
16239
|
|
|
16240
|
+
// dist/transport/p2p/database-wire.js
|
|
16241
|
+
var CMD_DATABASE_IMAGE = 1308;
|
|
16242
|
+
var CMD_DATABASE = 1306;
|
|
16243
|
+
var DB_QUERY = { FULL_TABLE: 1e4, COMBINATION_WITH_AI: 10011, QUERY_LOCAL: 10017 };
|
|
16244
|
+
var DB_TABLE_TIMEOUT_MS = 15e3;
|
|
16245
|
+
function fullTableQuery(count2 = 2e3) {
|
|
16246
|
+
return {
|
|
16247
|
+
count: count2,
|
|
16248
|
+
start_date: "",
|
|
16249
|
+
end_date: "",
|
|
16250
|
+
start_id: 0,
|
|
16251
|
+
end_id: 1,
|
|
16252
|
+
flag: 0,
|
|
16253
|
+
need_ai: 1,
|
|
16254
|
+
res_unzip: 1,
|
|
16255
|
+
update_time: "0",
|
|
16256
|
+
start_time: "0",
|
|
16257
|
+
alarm_id: ""
|
|
16258
|
+
};
|
|
16259
|
+
}
|
|
16260
|
+
|
|
16212
16261
|
// dist/transport/p2p/p2p-session.js
|
|
16213
16262
|
var LOCAL_LOOKUP_PORT = 32108;
|
|
16214
16263
|
var HEARTBEAT_MS = 5e3;
|
|
@@ -16262,9 +16311,6 @@ var CMD_SET_PAYLOAD = 1350;
|
|
|
16262
16311
|
var CMD_NOTIFY_PAYLOAD = 1351;
|
|
16263
16312
|
var PADDED_RESULT_BYTES = 132;
|
|
16264
16313
|
var CMD_CAMERA_INFO = 1103;
|
|
16265
|
-
var CMD_DATABASE_IMAGE = 1308;
|
|
16266
|
-
var CMD_DATABASE = 1306;
|
|
16267
|
-
var DB_QUERY = { FULL_TABLE: 1e4, COMBINATION_WITH_AI: 10011, QUERY_LOCAL: 10017 };
|
|
16268
16314
|
var GCM_AAD = Buffer.from("eufy security");
|
|
16269
16315
|
var LEVEL2_SEQ_BASE = 16909056;
|
|
16270
16316
|
function paramReport(payload) {
|
|
@@ -16281,7 +16327,6 @@ function paramReport(payload) {
|
|
|
16281
16327
|
return Object.keys(out).length ? out : void 0;
|
|
16282
16328
|
}
|
|
16283
16329
|
var traceSequence = 0;
|
|
16284
|
-
var DB_TABLE_TIMEOUT_MS = 15e3;
|
|
16285
16330
|
function firstTableRows(text2) {
|
|
16286
16331
|
const decoded = Buffer.from(text2, "latin1").toString("utf8");
|
|
16287
16332
|
for (let start = decoded.indexOf("{"); start >= 0; start = decoded.indexOf("{", start + 1)) {
|
|
@@ -16408,11 +16453,14 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16408
16453
|
level2Seq = 0;
|
|
16409
16454
|
rsaPrivateKey;
|
|
16410
16455
|
rsaModulusHex;
|
|
16411
|
-
/**
|
|
16456
|
+
/**
|
|
16457
|
+
* Whether a level-2 key negotiation kicked off by a GATEWAYINFO reply is in flight. Cleared when it
|
|
16458
|
+
* settles, so a later reply — the answer to {@link repromptLevel2Key} — negotiates again.
|
|
16459
|
+
*/
|
|
16412
16460
|
level2Negotiating = false;
|
|
16413
16461
|
/**
|
|
16414
16462
|
* Whether waiting for a level-2 key can still change the answer: `false` once one has been negotiated,
|
|
16415
|
-
* once
|
|
16463
|
+
* once a negotiation concluded without one, and from the start when nothing can negotiate one.
|
|
16416
16464
|
*/
|
|
16417
16465
|
level2Pending;
|
|
16418
16466
|
/** Waiters parked in {@link awaitLevel2Key}, woken the moment the negotiation settles either way. */
|
|
@@ -16517,7 +16565,8 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16517
16565
|
* The session grace runs from connect, when the station is prompted for `CMD_GATEWAYINFO`. A negotiation
|
|
16518
16566
|
* beginning later does not restart it: best-effort media can proceed without the key, and restarting
|
|
16519
16567
|
* would charge another grace to a source that may already be streaming. A session whose negotiation has
|
|
16520
|
-
* settled answers without waiting at all
|
|
16568
|
+
* settled answers without waiting at all: the answer stands until {@link repromptLevel2Key} re-opens the
|
|
16569
|
+
* wait, since the station is prompted for `CMD_GATEWAYINFO` only on connect and on that re-prompt.
|
|
16521
16570
|
*
|
|
16522
16571
|
* An own-session camera whose grace expires here is NOT thereby a camera that will fail to stream. Measured
|
|
16523
16572
|
* on one account: own-session cameras of two device types negotiated a key, three others never did, and
|
|
@@ -16550,7 +16599,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16550
16599
|
this.logger.debug(`[p2p] ${this.cfg.stationSn} waiting up to ${remaining}ms for the level-2 key`);
|
|
16551
16600
|
this.trace({ phase: "level2-wait", waitMs: remaining });
|
|
16552
16601
|
const waiters = this.level2Waiters;
|
|
16553
|
-
const outcome = await new Promise((
|
|
16602
|
+
const outcome = await new Promise((resolve2) => {
|
|
16554
16603
|
let deadline;
|
|
16555
16604
|
let settled = false;
|
|
16556
16605
|
const finish = (result) => {
|
|
@@ -16561,7 +16610,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16561
16610
|
const at = waiters.indexOf(wake);
|
|
16562
16611
|
if (at >= 0)
|
|
16563
16612
|
waiters.splice(at, 1);
|
|
16564
|
-
|
|
16613
|
+
resolve2(result);
|
|
16565
16614
|
};
|
|
16566
16615
|
const wake = (result) => finish(result);
|
|
16567
16616
|
deadline = setTimeout(() => finish("timeout"), remaining);
|
|
@@ -16583,12 +16632,13 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16583
16632
|
/**
|
|
16584
16633
|
* Ask the station for its gateway info a second time, re-opening a negotiation that concluded without a key.
|
|
16585
16634
|
*
|
|
16586
|
-
*
|
|
16587
|
-
*
|
|
16635
|
+
* Without this the station is prompted once on connect, and a negotiation that settles without a key
|
|
16636
|
+
* leaves {@link awaitLevel2Key} answering `false` at once for the rest of that connection. That is the
|
|
16588
16637
|
* right answer for best-effort media, which proceeds at level-1 — but an operation whose ONLY wire is
|
|
16589
16638
|
* level-2 is then refused for the whole life of that connection, while a fresh session over the same
|
|
16590
16639
|
* device negotiates a key normally. Measured: a session that had settled refused every such operation
|
|
16591
|
-
* until it was rebuilt, at which point the station answered with a cipher id straight away.
|
|
16640
|
+
* until it was rebuilt, at which point the station answered with a cipher id straight away. The reply to
|
|
16641
|
+
* this ask runs the negotiation again and settles the re-opened wait either way.
|
|
16592
16642
|
*
|
|
16593
16643
|
* Bounded to one extra ask per connection, so a burst of such operations cannot turn a silent station into a
|
|
16594
16644
|
* flood, and answers whether it asked — `false` when a key is already held, when nothing can negotiate one,
|
|
@@ -16609,6 +16659,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16609
16659
|
/** Record that the level-2 negotiation has finished, with or without a key, and wake every waiter. */
|
|
16610
16660
|
settleLevel2(reason = "terminal") {
|
|
16611
16661
|
this.level2Pending = false;
|
|
16662
|
+
this.level2Negotiating = false;
|
|
16612
16663
|
const outcome = this.level2Key ? "key" : reason;
|
|
16613
16664
|
for (const wake of this.level2Waiters.splice(0))
|
|
16614
16665
|
wake(outcome);
|
|
@@ -16618,13 +16669,14 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16618
16669
|
* `cipher_id`, resolve that cipher's private keys once (cloud `get_ciphers`, via the configured
|
|
16619
16670
|
* `resolveCipherKey`), run the ECIES unwrap with the ECC key, and `setLevel2Key()`. A cipher with no
|
|
16620
16671
|
* ECC key uses its RSA key instead and holds the RSA-decrypted 16-byte key as the AES-128-ECB level-2
|
|
16621
|
-
* key.
|
|
16672
|
+
* key. Emits `level2Ready` only when the AES-256-GCM key is negotiated, traces
|
|
16622
16673
|
* `level2-set-payload-only` instead for the AES-128-ECB key, and emits `error` when the ECC unwrap
|
|
16623
16674
|
* fails; an RSA unwrap failure is only traced (non-fatal either way — level-1 traffic keeps working).
|
|
16624
16675
|
*
|
|
16625
|
-
* Every outcome settles the wait in {@link awaitLevel2Key},
|
|
16626
|
-
*
|
|
16627
|
-
*
|
|
16676
|
+
* Every outcome settles the wait in {@link awaitLevel2Key}, so a later command is told at once rather than
|
|
16677
|
+
* left to time out against a key that is not coming. Settling also re-opens the negotiation, so a later
|
|
16678
|
+
* GATEWAYINFO reply in the same connection generation runs it again; a lookup that completes after its
|
|
16679
|
+
* generation ended settles nothing.
|
|
16628
16680
|
*/
|
|
16629
16681
|
negotiateLevel2Key(gwPayload) {
|
|
16630
16682
|
this.level2Negotiating = true;
|
|
@@ -16746,14 +16798,14 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16746
16798
|
this.socket = socket;
|
|
16747
16799
|
socket.on("message", (msg, rinfo) => this.onMessage(msg, rinfo, socket));
|
|
16748
16800
|
socket.on("error", (e) => this.emit("error", e));
|
|
16749
|
-
await new Promise((
|
|
16801
|
+
await new Promise((resolve2) => {
|
|
16750
16802
|
socket.bind(() => {
|
|
16751
16803
|
try {
|
|
16752
16804
|
socket.setBroadcast(true);
|
|
16753
16805
|
} catch {
|
|
16754
16806
|
}
|
|
16755
16807
|
this.requestReceiveBuffer(socket);
|
|
16756
|
-
|
|
16808
|
+
resolve2();
|
|
16757
16809
|
});
|
|
16758
16810
|
});
|
|
16759
16811
|
void _P2PSession.detectLocalIp().then((host) => {
|
|
@@ -16816,7 +16868,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16816
16868
|
*/
|
|
16817
16869
|
static detectLocalIp() {
|
|
16818
16870
|
if (!_P2PSession.localIpPromise) {
|
|
16819
|
-
_P2PSession.localIpPromise = new Promise((
|
|
16871
|
+
_P2PSession.localIpPromise = new Promise((resolve2) => {
|
|
16820
16872
|
const probe = dgram.createSocket("udp4");
|
|
16821
16873
|
let settled = false;
|
|
16822
16874
|
const finish = (result) => {
|
|
@@ -16828,7 +16880,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16828
16880
|
probe.close();
|
|
16829
16881
|
} catch {
|
|
16830
16882
|
}
|
|
16831
|
-
|
|
16883
|
+
resolve2(result);
|
|
16832
16884
|
};
|
|
16833
16885
|
const timer = setTimeout(() => finish(void 0), 2e3);
|
|
16834
16886
|
probe.connect(53, "8.8.8.8", () => {
|
|
@@ -16980,9 +17032,11 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16980
17032
|
} else if (hasHeader(msg, ResponseMessageType.LOOKUP_ADDR)) {
|
|
16981
17033
|
if (!this.connected) {
|
|
16982
17034
|
const addr = parseLookupAddr(msg);
|
|
16983
|
-
|
|
16984
|
-
|
|
16985
|
-
|
|
17035
|
+
if (addr) {
|
|
17036
|
+
this.logger.debug(`[p2p] ${this.cfg.stationSn} LOOKUP_ADDR -> ${addr.host}:${addr.port}`);
|
|
17037
|
+
if (addr.host !== "0.0.0.0")
|
|
17038
|
+
this.beginCheckCam(addr, socket);
|
|
17039
|
+
}
|
|
16986
17040
|
}
|
|
16987
17041
|
} else if (hasHeader(msg, ResponseMessageType.RLY_TO)) {
|
|
16988
17042
|
const to = parseRelayTo(msg);
|
|
@@ -17036,7 +17090,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
17036
17090
|
probe.on("error", (error) => this.logger.warn("[p2p] lookup socket error", error.message));
|
|
17037
17091
|
probe.on("message", (msg, rinfo) => this.onMessage(msg, rinfo, probe));
|
|
17038
17092
|
this.probeSockets.push(probe);
|
|
17039
|
-
await new Promise((
|
|
17093
|
+
await new Promise((resolve2) => {
|
|
17040
17094
|
let settled = false;
|
|
17041
17095
|
const finish = (bound) => {
|
|
17042
17096
|
if (settled)
|
|
@@ -17051,7 +17105,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
17051
17105
|
} catch {
|
|
17052
17106
|
}
|
|
17053
17107
|
}
|
|
17054
|
-
|
|
17108
|
+
resolve2();
|
|
17055
17109
|
};
|
|
17056
17110
|
const onError = () => finish(false);
|
|
17057
17111
|
const onClose = () => finish(false);
|
|
@@ -17837,25 +17891,6 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
17837
17891
|
databasePayload(value, channel) {
|
|
17838
17892
|
return !this.level2Key && this.sendsInTheClear() ? buildRawCommandPayload(Buffer.from(value, "utf-8"), channel, 0, this.nextClearCommandBytes(), 0) : buildStringCommandPayload(value, channel, this.level1Key, 1);
|
|
17839
17893
|
}
|
|
17840
|
-
/**
|
|
17841
|
-
* Standard "give me the whole table" query params the eufy app uses for a direct
|
|
17842
|
-
* `CMD_DATABASE` read (inner `cmd 10000`). `count` bounds the row count.
|
|
17843
|
-
*/
|
|
17844
|
-
fullTableQuery(count2 = 2e3) {
|
|
17845
|
-
return {
|
|
17846
|
-
count: count2,
|
|
17847
|
-
start_date: "",
|
|
17848
|
-
end_date: "",
|
|
17849
|
-
start_id: 0,
|
|
17850
|
-
end_id: 1,
|
|
17851
|
-
flag: 0,
|
|
17852
|
-
need_ai: 1,
|
|
17853
|
-
res_unzip: 1,
|
|
17854
|
-
update_time: "0",
|
|
17855
|
-
start_time: "0",
|
|
17856
|
-
alarm_id: ""
|
|
17857
|
-
};
|
|
17858
|
-
}
|
|
17859
17894
|
/**
|
|
17860
17895
|
* Request the on-device edge-AI **face roster** over P2P — the phone-free path.
|
|
17861
17896
|
*
|
|
@@ -17872,7 +17907,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
17872
17907
|
accountId: opts.accountId,
|
|
17873
17908
|
channel: opts.channel,
|
|
17874
17909
|
innerCmd: DB_QUERY.FULL_TABLE,
|
|
17875
|
-
query:
|
|
17910
|
+
query: fullTableQuery()
|
|
17876
17911
|
});
|
|
17877
17912
|
}
|
|
17878
17913
|
/**
|
|
@@ -17921,11 +17956,11 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
17921
17956
|
this.queryDatabase(table, {
|
|
17922
17957
|
accountId: opts.accountId,
|
|
17923
17958
|
innerCmd: opts.innerCmd,
|
|
17924
|
-
query: opts.query ??
|
|
17959
|
+
query: opts.query ?? fullTableQuery()
|
|
17925
17960
|
});
|
|
17926
17961
|
this.dbReadInFlight = true;
|
|
17927
17962
|
try {
|
|
17928
|
-
return await new Promise((
|
|
17963
|
+
return await new Promise((resolve2, reject) => {
|
|
17929
17964
|
let text2 = "";
|
|
17930
17965
|
const onChunk = (chunk) => {
|
|
17931
17966
|
text2 += chunk.text;
|
|
@@ -17933,7 +17968,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
17933
17968
|
if (result?.error !== void 0)
|
|
17934
17969
|
settle(() => reject(new Error(`readDatabase: ${table} refused: ${result.error}`)));
|
|
17935
17970
|
else if (result?.rows)
|
|
17936
|
-
settle(() =>
|
|
17971
|
+
settle(() => resolve2(result.rows));
|
|
17937
17972
|
};
|
|
17938
17973
|
const timer = setTimeout(() => settle(() => reject(new Error(`readDatabase: ${this.cfg.stationSn} sent no complete ${table}`))), opts.timeoutMs ?? DB_TABLE_TIMEOUT_MS);
|
|
17939
17974
|
const onAbort = () => settle(() => reject(new Error("readDatabase: aborted")));
|
|
@@ -17963,7 +17998,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
17963
17998
|
accountId: opts.accountId,
|
|
17964
17999
|
channel: opts.channel,
|
|
17965
18000
|
innerCmd: DB_QUERY.FULL_TABLE,
|
|
17966
|
-
query:
|
|
18001
|
+
query: fullTableQuery()
|
|
17967
18002
|
});
|
|
17968
18003
|
}
|
|
17969
18004
|
/**
|
|
@@ -18027,8 +18062,12 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
18027
18062
|
*
|
|
18028
18063
|
* Only once the data type's reorder wait passes, or {@link REORDER_MAX_DATAGRAMS} pile up, is the datagram
|
|
18029
18064
|
* treated as lost: a pending frame is discarded and delivery resumes from the earliest held datagram.
|
|
18065
|
+
*
|
|
18066
|
+
* A datagram shorter than the 8-byte DATA header carries no sequence number and is dropped unacknowledged.
|
|
18030
18067
|
*/
|
|
18031
18068
|
onData(msg, addr) {
|
|
18069
|
+
if (msg.length < 8)
|
|
18070
|
+
return;
|
|
18032
18071
|
const dataTypeBuffer = msg.subarray(4, 6);
|
|
18033
18072
|
const seqNo = msg.subarray(6, 8).readUInt16BE();
|
|
18034
18073
|
const dataType = dataTypeBuffer[1];
|
|
@@ -18194,6 +18233,13 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
18194
18233
|
return P2PDataTypeHeader.DATA;
|
|
18195
18234
|
}
|
|
18196
18235
|
}
|
|
18236
|
+
/**
|
|
18237
|
+
* Decrypt and dispatch one inbound data frame.
|
|
18238
|
+
*
|
|
18239
|
+
* A `CMD_GATEWAYINFO` (1100) reply carries the ECIES envelope for the level-2 session key. When a
|
|
18240
|
+
* cipher-key resolver is configured, no key that seals a set-payload is held and no negotiation is in
|
|
18241
|
+
* flight, the reply starts one (async, fire-and-forget); a reply landing while one is in flight is ignored.
|
|
18242
|
+
*/
|
|
18197
18243
|
handleFrame(header, payload, dataType) {
|
|
18198
18244
|
let data = payload;
|
|
18199
18245
|
const isMedia = header.commandId === CMD_VIDEO_FRAME || header.commandId === CMD_AUDIO_FRAME;
|
|
@@ -18299,12 +18345,12 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
18299
18345
|
this.resetInboundSequencing();
|
|
18300
18346
|
const socket = this.socket;
|
|
18301
18347
|
if (socket && this.connectAddress) {
|
|
18302
|
-
await new Promise((
|
|
18348
|
+
await new Promise((resolve2) => this.send(this.connectAddress, RequestMessageType.END, void 0, socket, resolve2));
|
|
18303
18349
|
}
|
|
18304
|
-
await new Promise((
|
|
18350
|
+
await new Promise((resolve2) => {
|
|
18305
18351
|
if (!socket)
|
|
18306
|
-
return
|
|
18307
|
-
socket.close(() =>
|
|
18352
|
+
return resolve2();
|
|
18353
|
+
socket.close(() => resolve2());
|
|
18308
18354
|
});
|
|
18309
18355
|
this.socket = void 0;
|
|
18310
18356
|
this.emit("close");
|
|
@@ -19512,7 +19558,7 @@ async function captureSnapshotFromShared(source, opts = {}) {
|
|
|
19512
19558
|
const skip = opts.skipKeyframes ?? (source.state === "live" ? 0 : 1);
|
|
19513
19559
|
let burst;
|
|
19514
19560
|
try {
|
|
19515
|
-
burst = await new Promise((
|
|
19561
|
+
burst = await new Promise((resolve2, reject) => {
|
|
19516
19562
|
const bufs = [];
|
|
19517
19563
|
let sets = source.parameterSets;
|
|
19518
19564
|
let codec = "h264";
|
|
@@ -19535,7 +19581,7 @@ async function captureSnapshotFromShared(source, opts = {}) {
|
|
|
19535
19581
|
if (!settle)
|
|
19536
19582
|
settle = setTimeout(() => {
|
|
19537
19583
|
cleanup();
|
|
19538
|
-
|
|
19584
|
+
resolve2({ h264: Buffer.concat(bufs), codec, sets });
|
|
19539
19585
|
}, collectMs);
|
|
19540
19586
|
};
|
|
19541
19587
|
const onError = (err) => {
|
|
@@ -19579,7 +19625,7 @@ async function recordClip(session, seconds, opts = {}) {
|
|
|
19579
19625
|
const stream = await openLiveStream(session, opts);
|
|
19580
19626
|
let h264;
|
|
19581
19627
|
try {
|
|
19582
|
-
h264 = await new Promise((
|
|
19628
|
+
h264 = await new Promise((resolve2, reject) => {
|
|
19583
19629
|
const bufs = [];
|
|
19584
19630
|
let keyCount = 0, capturing = false;
|
|
19585
19631
|
let sets;
|
|
@@ -19592,7 +19638,7 @@ async function recordClip(session, seconds, opts = {}) {
|
|
|
19592
19638
|
};
|
|
19593
19639
|
const collected = () => {
|
|
19594
19640
|
detach();
|
|
19595
|
-
|
|
19641
|
+
resolve2(primeForDecode(Buffer.concat(bufs), sets, codec2));
|
|
19596
19642
|
};
|
|
19597
19643
|
const failed = (reason, cause) => {
|
|
19598
19644
|
detach();
|
|
@@ -19622,7 +19668,7 @@ async function recordClip(session, seconds, opts = {}) {
|
|
|
19622
19668
|
stream.stop();
|
|
19623
19669
|
}
|
|
19624
19670
|
const codec = annexbFfmpegFormat(h264);
|
|
19625
|
-
return new Promise((
|
|
19671
|
+
return new Promise((resolve2, reject) => {
|
|
19626
19672
|
const ff = spawnFfmpeg([
|
|
19627
19673
|
// prettier-ignore
|
|
19628
19674
|
"-f",
|
|
@@ -19645,7 +19691,7 @@ async function recordClip(session, seconds, opts = {}) {
|
|
|
19645
19691
|
ff.on("close", (code) => {
|
|
19646
19692
|
const mp4 = Buffer.concat(out);
|
|
19647
19693
|
if (mp4.length)
|
|
19648
|
-
|
|
19694
|
+
resolve2(mp4);
|
|
19649
19695
|
else
|
|
19650
19696
|
reject(new Error(`ffmpeg mux failed (code ${code}): ${Buffer.concat(err).toString().slice(0, 200)}`));
|
|
19651
19697
|
});
|
|
@@ -19684,7 +19730,7 @@ function jpegGeometry(jpeg) {
|
|
|
19684
19730
|
}
|
|
19685
19731
|
function annexbToJpeg(annexb, spawn2) {
|
|
19686
19732
|
const codec = annexbFfmpegFormat(annexb);
|
|
19687
|
-
return new Promise((
|
|
19733
|
+
return new Promise((resolve2, reject) => {
|
|
19688
19734
|
const ff = spawnFfmpeg([
|
|
19689
19735
|
// prettier-ignore
|
|
19690
19736
|
"-f",
|
|
@@ -19711,7 +19757,7 @@ function annexbToJpeg(annexb, spawn2) {
|
|
|
19711
19757
|
const encoded = jpeg.length >= 3 && jpeg.subarray(0, 3).toString("hex") === "ffd8ff";
|
|
19712
19758
|
const geometry = encoded ? jpegGeometry(jpeg) : void 0;
|
|
19713
19759
|
if (geometry)
|
|
19714
|
-
return
|
|
19760
|
+
return resolve2({ jpeg, ...geometry });
|
|
19715
19761
|
const diagnostics = Buffer.concat(err).toString().slice(0, 200).trim();
|
|
19716
19762
|
const what = code !== 0 ? `ffmpeg exited ${code}` : encoded ? "encoded image declares no frame header" : "no complete frame in the burst";
|
|
19717
19763
|
reject(new LiveSnapshotUnavailableError("undecodable-burst", diagnostics ? `${what}: ${diagnostics}` : what));
|
|
@@ -19744,12 +19790,12 @@ function homeBase2RecordingPath(channel, recording) {
|
|
|
19744
19790
|
function receiveRecording(session, request) {
|
|
19745
19791
|
let resolveFrames;
|
|
19746
19792
|
let rejectFrames;
|
|
19747
|
-
const frames = new Promise((
|
|
19748
|
-
resolveFrames =
|
|
19793
|
+
const frames = new Promise((resolve2, reject) => {
|
|
19794
|
+
resolveFrames = resolve2;
|
|
19749
19795
|
rejectFrames = reject;
|
|
19750
19796
|
});
|
|
19751
19797
|
let resolveDrained;
|
|
19752
|
-
const drained = new Promise((
|
|
19798
|
+
const drained = new Promise((resolve2) => resolveDrained = resolve2);
|
|
19753
19799
|
const limitMs = request.timeoutMs ?? DEFAULT_TRANSFER_MS;
|
|
19754
19800
|
let collected = [];
|
|
19755
19801
|
const started = Date.now();
|
|
@@ -20287,7 +20333,8 @@ var SharedLiveSource = class {
|
|
|
20287
20333
|
*
|
|
20288
20334
|
* The previous stream is dropped rather than stopped through the state machine: it speaks to a session that
|
|
20289
20335
|
* is gone, and its `stop` would be read as an upstream end. Only a warm-up that asked for a replacement
|
|
20290
|
-
* rewarms, so this is inert on a source that is streaming or has already failed.
|
|
20336
|
+
* rewarms, so this is inert on a source that is streaming or has already failed. Each call replaces the
|
|
20337
|
+
* re-issue ticker rather than adding one, so the start goes out once per interval however often it runs.
|
|
20291
20338
|
*/
|
|
20292
20339
|
rewarm() {
|
|
20293
20340
|
if (this.disposed || !this.sessionReplacementAsked || this._state !== "warming")
|
|
@@ -20299,6 +20346,8 @@ var SharedLiveSource = class {
|
|
|
20299
20346
|
this.openStream();
|
|
20300
20347
|
this.warmAttempts++;
|
|
20301
20348
|
this.logger.debug(`${this.tag} warming again on a replacement session (attempt ${this.warmAttempts})`);
|
|
20349
|
+
if (this.warmRetryTimer)
|
|
20350
|
+
clearInterval(this.warmRetryTimer);
|
|
20302
20351
|
this.warmRetryTimer = setInterval(() => this.reissueStart(), this.warmRetryMs);
|
|
20303
20352
|
}
|
|
20304
20353
|
/**
|
|
@@ -21019,6 +21068,7 @@ function openReadableFromConsumer(consumer, opts = {}) {
|
|
|
21019
21068
|
if (!readable.push(value))
|
|
21020
21069
|
consumer.pause();
|
|
21021
21070
|
});
|
|
21071
|
+
consumer.on("budget", (notice) => readable.emit("budget", notice));
|
|
21022
21072
|
consumer.on("stop", () => readable.push(null));
|
|
21023
21073
|
consumer.on("error", (err) => readable.destroy(err));
|
|
21024
21074
|
return readable;
|
|
@@ -21954,7 +22004,7 @@ var FragmentRecording = class extends EventEmitter6 {
|
|
|
21954
22004
|
throw this.failure;
|
|
21955
22005
|
if (this.ended)
|
|
21956
22006
|
return;
|
|
21957
|
-
await new Promise((
|
|
22007
|
+
await new Promise((resolve2) => this.wake = resolve2);
|
|
21958
22008
|
}
|
|
21959
22009
|
} finally {
|
|
21960
22010
|
this.stop();
|
|
@@ -22070,7 +22120,7 @@ var TALKBACK_CODEC_WAIT_MS = 3e3;
|
|
|
22070
22120
|
var OWN_LIVE_SESSION_MODELS = /* @__PURE__ */ new Set(["T8030"]);
|
|
22071
22121
|
var ELD_TALKBACK_MODELS = /* @__PURE__ */ new Set(["T8410C"]);
|
|
22072
22122
|
function speakerCodec(consumer, talk, timeoutMs) {
|
|
22073
|
-
return new Promise((
|
|
22123
|
+
return new Promise((resolve2) => {
|
|
22074
22124
|
let timer;
|
|
22075
22125
|
const settle = (codec) => {
|
|
22076
22126
|
clearTimeout(timer);
|
|
@@ -22078,7 +22128,7 @@ function speakerCodec(consumer, talk, timeoutMs) {
|
|
|
22078
22128
|
consumer.off("audio", onAudio);
|
|
22079
22129
|
consumer.off("stop", onStop);
|
|
22080
22130
|
talk.off("stop", onStop);
|
|
22081
|
-
|
|
22131
|
+
resolve2(codec);
|
|
22082
22132
|
};
|
|
22083
22133
|
const onVideo = () => {
|
|
22084
22134
|
timer ??= setTimeout(() => settle("aac-lc"), timeoutMs);
|
|
@@ -22130,6 +22180,18 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
22130
22180
|
* {@link liveSources} is a claim in flight.
|
|
22131
22181
|
*/
|
|
22132
22182
|
liveSessionKeys = /* @__PURE__ */ new Map();
|
|
22183
|
+
/**
|
|
22184
|
+
* The live source build in flight per `${parentSn}:${channel}`, settled once its connection is opened
|
|
22185
|
+
* or refused.
|
|
22186
|
+
*
|
|
22187
|
+
* A build awaits its media session before it can file the source in {@link liveSources}, so a second
|
|
22188
|
+
* request for the same camera in that window finds nothing cached. Building a second source there
|
|
22189
|
+
* overwrites the first, and the orphan's teardown then closes the connection the cached one streams
|
|
22190
|
+
* on. A request that finds a build here waits for it, until its own signal aborts, then resolves its
|
|
22191
|
+
* session again and reuses what the build filed, or builds its own when that build failed. The entry
|
|
22192
|
+
* is removed when the build settles and its promise never rejects, so each wait ends on a later state.
|
|
22193
|
+
*/
|
|
22194
|
+
liveSourceBuilds = /* @__PURE__ */ new Map();
|
|
22133
22195
|
/**
|
|
22134
22196
|
* The open talkback per `${parentSn}:${channel}`, if any. The device plays one audio stream at a
|
|
22135
22197
|
* time and the session carries one audio sequence, so this path is exclusive where a live pull is
|
|
@@ -22309,17 +22371,30 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
22309
22371
|
* so a start issued before the key arrives is dropped as `media-command-unsent`, and the stream then
|
|
22310
22372
|
* shows nothing until a later keepalive tick happens to find the key. A connected session that cannot
|
|
22311
22373
|
* carry the start is not a connected session, so this refuses rather than returning one.
|
|
22374
|
+
*
|
|
22375
|
+
* A session refused its key within the grace, or never connected, is closed and dropped from the manager at
|
|
22376
|
+
* once. Nothing else releases it: no source holds it, a wired station's session never idles out, and the
|
|
22377
|
+
* manager hands the same session to every later open of the camera, so the next open dials a fresh
|
|
22378
|
+
* connection instead. A session another open has since replaced is left to its own close.
|
|
22312
22379
|
*/
|
|
22313
22380
|
async openMediaSession(parentSn, channel, signal) {
|
|
22314
|
-
const
|
|
22315
|
-
await this.
|
|
22316
|
-
|
|
22317
|
-
|
|
22318
|
-
ready = await abortable(session.awaitLevel2Key(LEVEL2_GRACE_MS, "call"), signal);
|
|
22319
|
-
|
|
22320
|
-
|
|
22321
|
-
|
|
22322
|
-
|
|
22381
|
+
const mediaKey = _P2PCommandRouter.mediaSessionKey(parentSn, channel);
|
|
22382
|
+
const session = await this.openSession(mediaKey, parentSn);
|
|
22383
|
+
try {
|
|
22384
|
+
await this.awaitConnected(session, parentSn, signal);
|
|
22385
|
+
let ready = await abortable(session.awaitLevel2Key(LEVEL2_GRACE_MS, "call"), signal);
|
|
22386
|
+
if (!ready && session.repromptLevel2Key()) {
|
|
22387
|
+
ready = await abortable(session.awaitLevel2Key(LEVEL2_GRACE_MS, "call"), signal);
|
|
22388
|
+
}
|
|
22389
|
+
if (!ready)
|
|
22390
|
+
throw new StationKeyUnavailableError(parentSn);
|
|
22391
|
+
return session;
|
|
22392
|
+
} catch (error) {
|
|
22393
|
+
if (!signal?.aborted && this.manager.get(mediaKey) === session) {
|
|
22394
|
+
void this.manager.close(mediaKey).catch((e) => this.reportError(e instanceof Error ? e : new Error(String(e))));
|
|
22395
|
+
}
|
|
22396
|
+
throw error;
|
|
22397
|
+
}
|
|
22323
22398
|
}
|
|
22324
22399
|
/**
|
|
22325
22400
|
* Wait for `session` to connect, for up to the session's own connect deadline, and throw
|
|
@@ -22357,7 +22432,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
22357
22432
|
throw new Error(`no P2P endpoint (p2p_did) for station ${parentSn}`);
|
|
22358
22433
|
let dskKey;
|
|
22359
22434
|
try {
|
|
22360
|
-
dskKey = (await this.deps.mega.getDskKeys([
|
|
22435
|
+
dskKey = (await this.deps.mega.getDskKeys([stationDev]))[parentSn]?.dskKey;
|
|
22361
22436
|
} catch (e) {
|
|
22362
22437
|
this.deps.onError(e instanceof Error ? e : new Error(String(e)));
|
|
22363
22438
|
}
|
|
@@ -22459,7 +22534,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
22459
22534
|
const session = await this.openSession(parentSn, parentSn);
|
|
22460
22535
|
if (session.isConnected)
|
|
22461
22536
|
return;
|
|
22462
|
-
await new Promise((
|
|
22537
|
+
await new Promise((resolve2, reject) => {
|
|
22463
22538
|
const cleanup = () => {
|
|
22464
22539
|
signal?.removeEventListener("abort", onAbort);
|
|
22465
22540
|
session.off("connect", onConnect);
|
|
@@ -22468,7 +22543,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
22468
22543
|
};
|
|
22469
22544
|
const onConnect = () => {
|
|
22470
22545
|
cleanup();
|
|
22471
|
-
|
|
22546
|
+
resolve2();
|
|
22472
22547
|
};
|
|
22473
22548
|
const onError = (error) => {
|
|
22474
22549
|
cleanup();
|
|
@@ -22828,6 +22903,11 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
22828
22903
|
signal: opts.signal
|
|
22829
22904
|
});
|
|
22830
22905
|
const key = `${parentSn}:${channel}`;
|
|
22906
|
+
const build = this.liveSourceBuilds.get(key);
|
|
22907
|
+
if (build) {
|
|
22908
|
+
await abortable(build, opts.signal);
|
|
22909
|
+
return this.sharedLiveSourceFor(sn, opts, continuous);
|
|
22910
|
+
}
|
|
22831
22911
|
const ownSession = continuous && homeBaseAttached && this.ownsLiveSession(parentSn);
|
|
22832
22912
|
let source = this.liveSources.get(key);
|
|
22833
22913
|
if (source && (source.state === "stopped" || ownSession && source.state !== "idle" && source.consumerCount === 0 && this.liveSessionKeys.get(key) === parentSn)) {
|
|
@@ -22839,6 +22919,8 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
22839
22919
|
const logger = this.deps.logger ?? noopLogger;
|
|
22840
22920
|
const sessionKey = ownSession || continuous && homeBaseAttached && this.stationSessionInUse(parentSn, key) ? _P2PCommandRouter.mediaSessionKey(parentSn, channel) : parentSn;
|
|
22841
22921
|
this.liveSessionKeys.set(key, sessionKey);
|
|
22922
|
+
const built = Promise.withResolvers();
|
|
22923
|
+
this.liveSourceBuilds.set(key, built.promise);
|
|
22842
22924
|
let held;
|
|
22843
22925
|
try {
|
|
22844
22926
|
held = {
|
|
@@ -22847,6 +22929,9 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
22847
22929
|
} catch (error) {
|
|
22848
22930
|
this.liveSessionKeys.delete(key);
|
|
22849
22931
|
throw error;
|
|
22932
|
+
} finally {
|
|
22933
|
+
this.liveSourceBuilds.delete(key);
|
|
22934
|
+
built.resolve();
|
|
22850
22935
|
}
|
|
22851
22936
|
source = new SharedLiveSource({
|
|
22852
22937
|
makeStream: (ctx) => new LiveStream(held.session, {
|
|
@@ -23151,7 +23236,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
23151
23236
|
async p2pQuery(sn, subCmd, opts = {}) {
|
|
23152
23237
|
const { session, channel, accountId } = await this.resolveSession(sn, { waitLevel2: true });
|
|
23153
23238
|
const timeoutMs = opts.timeoutMs ?? 15e3;
|
|
23154
|
-
return await new Promise((
|
|
23239
|
+
return await new Promise((resolve2, reject) => {
|
|
23155
23240
|
const timer = setTimeout(() => {
|
|
23156
23241
|
cleanup();
|
|
23157
23242
|
reject(new Error(`timeout waiting for p2p query ${subCmd} reply from ${sn}`));
|
|
@@ -23160,7 +23245,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
23160
23245
|
const j = f.json;
|
|
23161
23246
|
if (f.channel === channel && f.commandId === CommandType.CMD_NOTIFY_PAYLOAD && j?.cmd === subCmd && j.payload && typeof j.payload === "object") {
|
|
23162
23247
|
cleanup();
|
|
23163
|
-
|
|
23248
|
+
resolve2(j.payload);
|
|
23164
23249
|
}
|
|
23165
23250
|
};
|
|
23166
23251
|
const cleanup = () => {
|
|
@@ -23190,7 +23275,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
23190
23275
|
async p2pControlQuery(sn, param, data, opts = {}) {
|
|
23191
23276
|
const { session } = await this.resolveSession(sn, { waitLevel2: false });
|
|
23192
23277
|
const timeoutMs = opts.timeoutMs ?? 15e3;
|
|
23193
|
-
return await new Promise((
|
|
23278
|
+
return await new Promise((resolve2, reject) => {
|
|
23194
23279
|
const timer = setTimeout(() => {
|
|
23195
23280
|
cleanup();
|
|
23196
23281
|
reject(new Error(`timeout waiting for p2p control query ${param} reply from ${sn}`));
|
|
@@ -23199,7 +23284,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
23199
23284
|
const j = f.json;
|
|
23200
23285
|
if (j?.cmd === param && j.payload && typeof j.payload === "object") {
|
|
23201
23286
|
cleanup();
|
|
23202
|
-
|
|
23287
|
+
resolve2(j.payload);
|
|
23203
23288
|
}
|
|
23204
23289
|
};
|
|
23205
23290
|
const cleanup = () => {
|
|
@@ -23296,12 +23381,12 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
23296
23381
|
* is ignored rather than resolving the wrong read.
|
|
23297
23382
|
*/
|
|
23298
23383
|
awaitRtspUrl(session, channel, signal) {
|
|
23299
|
-
return new Promise((
|
|
23384
|
+
return new Promise((resolve2, reject) => {
|
|
23300
23385
|
const onUrl = (ev) => {
|
|
23301
23386
|
if (ev.channel !== channel)
|
|
23302
23387
|
return;
|
|
23303
23388
|
cleanup();
|
|
23304
|
-
|
|
23389
|
+
resolve2(ev.url);
|
|
23305
23390
|
};
|
|
23306
23391
|
const onAbort = () => {
|
|
23307
23392
|
cleanup();
|
|
@@ -23621,7 +23706,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
23621
23706
|
* wrapper has to share {@link serializeKeyed}.
|
|
23622
23707
|
*/
|
|
23623
23708
|
awaitKeyedReply(sn, session, channel, engage, send) {
|
|
23624
|
-
return new Promise((
|
|
23709
|
+
return new Promise((resolve2, reject) => {
|
|
23625
23710
|
let refused = false;
|
|
23626
23711
|
let mark;
|
|
23627
23712
|
let timer;
|
|
@@ -23641,7 +23726,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
23641
23726
|
if (refused)
|
|
23642
23727
|
return;
|
|
23643
23728
|
refused = true;
|
|
23644
|
-
expire(_P2PCommandRouter.KEYED_REFUSAL_TWIN_MS, () => settle(() =>
|
|
23729
|
+
expire(_P2PCommandRouter.KEYED_REFUSAL_TWIN_MS, () => settle(() => resolve2({ code: KEYED_PAYLOAD_SEQ_ERROR, mark })));
|
|
23645
23730
|
};
|
|
23646
23731
|
const onResult = (r) => {
|
|
23647
23732
|
if (r.channel !== channel)
|
|
@@ -23649,7 +23734,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
23649
23734
|
if (r.code === KEYED_PAYLOAD_SEQ_ERROR)
|
|
23650
23735
|
refuse();
|
|
23651
23736
|
else if (!refused)
|
|
23652
|
-
settle(() =>
|
|
23737
|
+
settle(() => resolve2({ code: r.code }));
|
|
23653
23738
|
};
|
|
23654
23739
|
const onData = (f) => {
|
|
23655
23740
|
if (f.channel !== channel || f.commandId !== CommandType.CMD_NOTIFY_PAYLOAD)
|
|
@@ -23661,7 +23746,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
23661
23746
|
}
|
|
23662
23747
|
const code = keyedPayloadReportCode(f.json, engage);
|
|
23663
23748
|
if (code !== void 0 && !refused)
|
|
23664
|
-
settle(() =>
|
|
23749
|
+
settle(() => resolve2({ code }));
|
|
23665
23750
|
};
|
|
23666
23751
|
session.on("commandResult", onResult);
|
|
23667
23752
|
session.on("data", onData);
|
|
@@ -23766,7 +23851,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
23766
23851
|
clearTimeout(timer);
|
|
23767
23852
|
session.off("data", onData);
|
|
23768
23853
|
};
|
|
23769
|
-
const getReplyPromise = new Promise((
|
|
23854
|
+
const getReplyPromise = new Promise((resolve2) => {
|
|
23770
23855
|
onData = (f) => {
|
|
23771
23856
|
const j = f.json;
|
|
23772
23857
|
if (j?.cmd !== CMD_TRANSFER_PAYLOAD || !j.payload)
|
|
@@ -23779,11 +23864,11 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
23779
23864
|
if (keyTime === void 0 || keyTime !== query.time)
|
|
23780
23865
|
return;
|
|
23781
23866
|
cleanup();
|
|
23782
|
-
|
|
23867
|
+
resolve2({ lockPayload, keyTime });
|
|
23783
23868
|
};
|
|
23784
23869
|
timer = setTimeout(() => {
|
|
23785
23870
|
cleanup();
|
|
23786
|
-
|
|
23871
|
+
resolve2(void 0);
|
|
23787
23872
|
}, _P2PCommandRouter.FF09_SETTINGS_GET_TIMEOUT_MS);
|
|
23788
23873
|
session.on("data", onData);
|
|
23789
23874
|
});
|
|
@@ -23912,7 +23997,9 @@ var PORTAL_KEEPALIVE = Buffer.from("0009000010000000000000006300000000000000585a
|
|
|
23912
23997
|
var PORTAL_KEEPALIVE_MS = 29e3;
|
|
23913
23998
|
|
|
23914
23999
|
// dist/transport/rtc/portal-packet.js
|
|
24000
|
+
import { zstdDecompressSync } from "node:zlib";
|
|
23915
24001
|
var MAGIC2 = Buffer.from("XZYH", "ascii");
|
|
24002
|
+
var ZSTD_MAGIC_LE = 4247762216;
|
|
23916
24003
|
var PORTAL_HEADER_LENGTH = 16;
|
|
23917
24004
|
var PORTAL_CMD_SET_PAYLOAD = 1350;
|
|
23918
24005
|
var PORTAL_STATION_CHANNEL = 255;
|
|
@@ -23976,11 +24063,23 @@ function parseJsonBody(buf) {
|
|
|
23976
24063
|
return text2;
|
|
23977
24064
|
}
|
|
23978
24065
|
}
|
|
24066
|
+
function unwrapZstd(buf) {
|
|
24067
|
+
if (buf.length < 4 || buf.readUInt32LE(0) !== ZSTD_MAGIC_LE)
|
|
24068
|
+
return buf;
|
|
24069
|
+
try {
|
|
24070
|
+
return zstdDecompressSync(buf);
|
|
24071
|
+
} catch {
|
|
24072
|
+
return void 0;
|
|
24073
|
+
}
|
|
24074
|
+
}
|
|
23979
24075
|
function parsePortalPacket(buf, linkType = PortalLinkType.COMMAND) {
|
|
23980
|
-
const
|
|
24076
|
+
const packet = unwrapZstd(buf);
|
|
24077
|
+
if (!packet)
|
|
24078
|
+
return void 0;
|
|
24079
|
+
const header = parsePortalHeader(packet);
|
|
23981
24080
|
if (!header)
|
|
23982
24081
|
return void 0;
|
|
23983
|
-
const body =
|
|
24082
|
+
const body = packet.subarray(PORTAL_HEADER_LENGTH, PORTAL_HEADER_LENGTH + header.paramLength);
|
|
23984
24083
|
if (linkType === PortalLinkType.NOTIFY) {
|
|
23985
24084
|
const data = parseJsonBody(body);
|
|
23986
24085
|
const cmd = typeof data === "object" && data !== null && "cmd" in data ? Number(data.cmd) : void 0;
|
|
@@ -24712,12 +24811,12 @@ var RtcPeer = class extends EventEmitter9 {
|
|
|
24712
24811
|
this.remoteMid = media ? offer.match(/^a=mid:([^\r\n]+)/m)?.[1]?.trim() ?? HUB_SDP_MID : midOf(offer, "application") ?? HUB_SDP_MID;
|
|
24713
24812
|
if (media)
|
|
24714
24813
|
this.videoMid = midOf(offer, "video");
|
|
24715
|
-
const answerWait = new Promise((
|
|
24814
|
+
const answerWait = new Promise((resolve2, reject) => {
|
|
24716
24815
|
const timer = setTimeout(() => {
|
|
24717
24816
|
this.localAnswer = void 0;
|
|
24718
24817
|
reject(new Error("timed out waiting for the local SDP answer"));
|
|
24719
24818
|
}, ANSWER_TIMEOUT_MS);
|
|
24720
|
-
this.localAnswer = { resolve, reject, timer };
|
|
24819
|
+
this.localAnswer = { resolve: resolve2, reject, timer };
|
|
24721
24820
|
});
|
|
24722
24821
|
pc.setRemoteDescription(offer, "offer");
|
|
24723
24822
|
this.remoteSet = true;
|
|
@@ -24879,7 +24978,7 @@ var RtcPeer = class extends EventEmitter9 {
|
|
|
24879
24978
|
|
|
24880
24979
|
// dist/transport/rtc/signaling.js
|
|
24881
24980
|
import { EventEmitter as EventEmitter10 } from "node:events";
|
|
24882
|
-
import { createHmac as createHmac3, randomUUID as
|
|
24981
|
+
import { createHmac as createHmac3, randomUUID as randomUUID3 } from "node:crypto";
|
|
24883
24982
|
var RTC_WS_PATH = "/v1/rtc/ws/join?reqtype=nvr";
|
|
24884
24983
|
var RTC_SIGN_PATH = "/v1/smart/nvr/ws/sign";
|
|
24885
24984
|
var PORTAL_ORIGIN = "https://security.eufy.com";
|
|
@@ -24919,7 +25018,7 @@ var RtcSignalingClient = class extends EventEmitter10 {
|
|
|
24919
25018
|
this.createSocket = opts.createSocket ?? defaultSocket;
|
|
24920
25019
|
this.logger = opts.logger ?? noopLogger;
|
|
24921
25020
|
this.now = opts.now ?? Date.now;
|
|
24922
|
-
this.makeMsgId = opts.makeMsgId ?? (() =>
|
|
25021
|
+
this.makeMsgId = opts.makeMsgId ?? (() => randomUUID3().replace(/-/g, ""));
|
|
24923
25022
|
}
|
|
24924
25023
|
get isOpen() {
|
|
24925
25024
|
return this.ws?.readyState === WS_OPEN;
|
|
@@ -24963,7 +25062,7 @@ var RtcSignalingClient = class extends EventEmitter10 {
|
|
|
24963
25062
|
};
|
|
24964
25063
|
const protocols = ["v1", Buffer.from(JSON.stringify(auth)).toString("base64url")];
|
|
24965
25064
|
this.logger.debug(`[rtc] ${this.opts.stationSn} signalling connect ${this.wsUrl}`);
|
|
24966
|
-
await new Promise((
|
|
25065
|
+
await new Promise((resolve2, reject) => {
|
|
24967
25066
|
const timer = setTimeout(() => {
|
|
24968
25067
|
reject(new Error(`RTC signalling connect timeout after ${SIGNALING_CONNECT_TIMEOUT_MS}ms`));
|
|
24969
25068
|
this.close();
|
|
@@ -24977,7 +25076,7 @@ var RtcSignalingClient = class extends EventEmitter10 {
|
|
|
24977
25076
|
this.sendAuth(sign);
|
|
24978
25077
|
this.startKeepalive();
|
|
24979
25078
|
this.emit("open");
|
|
24980
|
-
|
|
25079
|
+
resolve2();
|
|
24981
25080
|
});
|
|
24982
25081
|
ws.addEventListener("message", (ev) => {
|
|
24983
25082
|
void this.handleWireMessage(ev.data);
|
|
@@ -25250,12 +25349,12 @@ var RtcSession = class extends EventEmitter11 {
|
|
|
25250
25349
|
waitForAuth() {
|
|
25251
25350
|
if (this.authOk)
|
|
25252
25351
|
return Promise.resolve();
|
|
25253
|
-
return new Promise((
|
|
25352
|
+
return new Promise((resolve2, reject) => {
|
|
25254
25353
|
const timer = setTimeout(() => {
|
|
25255
25354
|
this.authWaiter = void 0;
|
|
25256
25355
|
reject(new Error("RTC signalling auth timeout"));
|
|
25257
25356
|
}, AUTH_TIMEOUT_MS);
|
|
25258
|
-
this.authWaiter = { resolve, reject, timer };
|
|
25357
|
+
this.authWaiter = { resolve: resolve2, reject, timer };
|
|
25259
25358
|
});
|
|
25260
25359
|
}
|
|
25261
25360
|
/** Settle a pending auth wait: resolve it, or reject it with `err`; either way its timer stops. */
|
|
@@ -25382,6 +25481,7 @@ var RtcCommandRouter = class {
|
|
|
25382
25481
|
lives = /* @__PURE__ */ new Map();
|
|
25383
25482
|
warm = /* @__PURE__ */ new Map();
|
|
25384
25483
|
callSources = /* @__PURE__ */ new Map();
|
|
25484
|
+
lastTransaction = 0;
|
|
25385
25485
|
constructor(deps) {
|
|
25386
25486
|
this.deps = deps;
|
|
25387
25487
|
}
|
|
@@ -25391,21 +25491,80 @@ var RtcCommandRouter = class {
|
|
|
25391
25491
|
}
|
|
25392
25492
|
if (route.mediaTrack)
|
|
25393
25493
|
throw new Error(`rtc: ${route.stationSn} serves media only and takes no portal command`);
|
|
25394
|
-
const identity = this.deps.identity();
|
|
25395
|
-
if (!identity)
|
|
25396
|
-
throw new Error(`rtc: not logged in, cannot drive ${route.stationSn}`);
|
|
25397
|
-
const { stationSn } = route;
|
|
25398
|
-
const adminUserId = route.adminUserId || identity.userId;
|
|
25399
25494
|
const channel = route.attached ? cmd.channel : PORTAL_STATION_CHANNEL;
|
|
25400
|
-
|
|
25401
|
-
|
|
25402
|
-
|
|
25403
|
-
|
|
25404
|
-
|
|
25405
|
-
|
|
25406
|
-
|
|
25407
|
-
|
|
25408
|
-
|
|
25495
|
+
if (cmd.kind === "set-json") {
|
|
25496
|
+
return this.sendQueued(route, PORTAL_CMD_CONTROL_PAYLOAD, cmd.param, channel, {
|
|
25497
|
+
cmd: cmd.param,
|
|
25498
|
+
commandType: cmd.param,
|
|
25499
|
+
data: cmd.data
|
|
25500
|
+
});
|
|
25501
|
+
}
|
|
25502
|
+
return this.sendQueued(route, PORTAL_CMD_SET_PAYLOAD, cmd.cmd, channel, {
|
|
25503
|
+
cmd: cmd.cmd,
|
|
25504
|
+
mValue3: cmd.mValue3 ?? 0,
|
|
25505
|
+
payload: cmd.payload
|
|
25506
|
+
});
|
|
25507
|
+
}
|
|
25508
|
+
/**
|
|
25509
|
+
* Query one on-station table over the station's control channel and answer its rows.
|
|
25510
|
+
*
|
|
25511
|
+
* Sends the `1350` envelope with inner `{cmd: 1306, payload: {cmd: 10000, table, payload, transaction}}`,
|
|
25512
|
+
* the whole-table query, on the station channel. The hub ACKs on the command link and answers with one notify frame whose body
|
|
25513
|
+
* carries `{cmd, table, data, mIntRet, msg, transaction, …}`, echoing the read's `transaction`; that echo
|
|
25514
|
+
* is the only thing a reply is attributed by, and every read sends a `transaction` no other read on this
|
|
25515
|
+
* router has sent. Resolves with the reply's `data` rows. Rejects with the hub's `msg` on a non-zero
|
|
25516
|
+
* `mIntRet`, on an ACK refusing the envelope, at once when `signal` aborts (queued or sent), when the
|
|
25517
|
+
* session closes, and when `timeoutMs` (default {@link DB_TABLE_TIMEOUT_MS}) elapses with no reply.
|
|
25518
|
+
*
|
|
25519
|
+
* One read or command at a time per station: the read holds the station's send queue until it settles.
|
|
25520
|
+
*/
|
|
25521
|
+
readTable(route, table, opts = {}) {
|
|
25522
|
+
const transaction = this.nextTransaction();
|
|
25523
|
+
const inner = {
|
|
25524
|
+
cmd: DB_QUERY.FULL_TABLE,
|
|
25525
|
+
table,
|
|
25526
|
+
payload: fullTableQuery(),
|
|
25527
|
+
transaction
|
|
25528
|
+
};
|
|
25529
|
+
return this.sendQueued(route, PORTAL_CMD_SET_PAYLOAD, CMD_DATABASE, PORTAL_STATION_CHANNEL, { cmd: CMD_DATABASE, mValue3: 0, payload: inner }, {
|
|
25530
|
+
timeoutMs: opts.timeoutMs,
|
|
25531
|
+
signal: opts.signal,
|
|
25532
|
+
pick: (body) => {
|
|
25533
|
+
if (String(body.transaction) !== transaction)
|
|
25534
|
+
return void 0;
|
|
25535
|
+
if (typeof body.mIntRet === "number" && body.mIntRet !== 0) {
|
|
25536
|
+
throw new Error(`rtc: ${route.stationSn} refused ${table}: ${body.msg ?? body.mIntRet} (${body.mIntRet})`);
|
|
25537
|
+
}
|
|
25538
|
+
return Array.isArray(body.data) ? body.data : void 0;
|
|
25539
|
+
}
|
|
25540
|
+
});
|
|
25541
|
+
}
|
|
25542
|
+
/**
|
|
25543
|
+
* Fetch one stored image by its on-station path over the station's control channel.
|
|
25544
|
+
*
|
|
25545
|
+
* Sends the `1350` envelope with inner `{cmd: 1308, payload: [{file}]}` on the station channel. The hub
|
|
25546
|
+
* ACKs on the command link and answers with one notify frame whose body is `{file, content}`, `content`
|
|
25547
|
+
* the image in base64, and no `transaction`; the reply is the one whose `file` equals `filePath`. A reply
|
|
25548
|
+
* arriving after an abandoned read of the same path carries that path's bytes, which are the bytes a
|
|
25549
|
+
* later read of it asks for. Resolves with the decoded bytes. Rejects on an ACK refusing the envelope,
|
|
25550
|
+
* at once when `signal` aborts (queued or sent), when the session closes, and when `timeoutMs` (default
|
|
25551
|
+
* {@link DB_TABLE_TIMEOUT_MS}) elapses with no reply.
|
|
25552
|
+
*
|
|
25553
|
+
* One read or command at a time per station: the read holds the station's send queue until it settles.
|
|
25554
|
+
*/
|
|
25555
|
+
readImage(route, filePath, opts = {}) {
|
|
25556
|
+
return this.sendQueued(route, PORTAL_CMD_SET_PAYLOAD, CMD_DATABASE_IMAGE, PORTAL_STATION_CHANNEL, { cmd: CMD_DATABASE_IMAGE, mValue3: 0, payload: [{ file: filePath }] }, {
|
|
25557
|
+
...opts,
|
|
25558
|
+
pick: (body) => body.file === filePath && typeof body.content === "string" ? Buffer.from(body.content, "base64") : void 0
|
|
25559
|
+
});
|
|
25560
|
+
}
|
|
25561
|
+
/**
|
|
25562
|
+
* A `transaction` stamp in the vendor app's shape, a decimal epoch-in-milliseconds string, strictly
|
|
25563
|
+
* greater than the last one this router issued, so two reads in the same millisecond never share one.
|
|
25564
|
+
*/
|
|
25565
|
+
nextTransaction() {
|
|
25566
|
+
this.lastTransaction = Math.max(Date.now(), this.lastTransaction + 1);
|
|
25567
|
+
return String(this.lastTransaction);
|
|
25409
25568
|
}
|
|
25410
25569
|
/**
|
|
25411
25570
|
* The media of a camera on a T9000: live video, a readable feed and a live still over the station's
|
|
@@ -25581,45 +25740,109 @@ var RtcCommandRouter = class {
|
|
|
25581
25740
|
return source;
|
|
25582
25741
|
}
|
|
25583
25742
|
/**
|
|
25584
|
-
*
|
|
25585
|
-
*
|
|
25586
|
-
*
|
|
25743
|
+
* Send one envelope on the station's session, queued behind its other sends, and settle as
|
|
25744
|
+
* {@link sendAwaitAck} does. `body` is the inner payload; the station session, the `account_id` it is
|
|
25745
|
+
* stamped with and the packet are resolved here, so a command and a read reach the wire through the same
|
|
25746
|
+
* steps. With `reply.signal`, the caller's wait rejects the moment it aborts, also while the send is still
|
|
25747
|
+
* queued; a send not yet made by then is skipped.
|
|
25587
25748
|
*/
|
|
25588
|
-
|
|
25589
|
-
|
|
25749
|
+
async sendQueued(route, outerCmd, innerCmd, channel, body, reply) {
|
|
25750
|
+
reply?.signal?.throwIfAborted();
|
|
25751
|
+
const identity = this.deps.identity();
|
|
25752
|
+
if (!identity)
|
|
25753
|
+
throw new Error(`rtc: not logged in, cannot drive ${route.stationSn}`);
|
|
25754
|
+
const adminUserId = route.adminUserId || identity.userId;
|
|
25755
|
+
const st = await abortable(this.stationSession(route, adminUserId, identity), reply?.signal);
|
|
25756
|
+
const segment = st.seg.next();
|
|
25757
|
+
const packet = buildPortalPacket({
|
|
25758
|
+
commandId: outerCmd,
|
|
25759
|
+
channel,
|
|
25760
|
+
segment,
|
|
25761
|
+
payload: { account_id: adminUserId, ...body }
|
|
25762
|
+
});
|
|
25763
|
+
const run = st.queue.then(() => this.sendAwaitAck(route.stationSn, st, packet, outerCmd, innerCmd, channel, segment, reply));
|
|
25764
|
+
st.queue = run.catch(() => void 0);
|
|
25765
|
+
return abortable(run, reply?.signal);
|
|
25766
|
+
}
|
|
25767
|
+
/**
|
|
25768
|
+
* Without `reply`, completes on the command ACK or a correlated control-result notification. The
|
|
25769
|
+
* notification must match the request channel, parameter and segment and contain a structured payload.
|
|
25770
|
+
* Receipt does not establish physical actuation. Bounded by {@link ACK_TIMEOUT_MS}.
|
|
25771
|
+
*
|
|
25772
|
+
* With `reply`, a zero-result ACK is not the answer: the first notify body (an object) for which
|
|
25773
|
+
* `reply.pick` answers a value resolves with it, one `pick` throws on rejects with that error, and every
|
|
25774
|
+
* other notify frame is left alone. Bounded by `reply.timeoutMs` (default {@link DB_TABLE_TIMEOUT_MS}),
|
|
25775
|
+
* and ended by `reply.signal`; one already aborted is not sent.
|
|
25776
|
+
*
|
|
25777
|
+
* Either way an ACK with a non-zero result code rejects, and the command is sent once.
|
|
25778
|
+
*/
|
|
25779
|
+
sendAwaitAck(sn, st, packet, outerCmd, innerCmd, channel, segment, reply) {
|
|
25780
|
+
const timeoutMs = reply ? reply.timeoutMs ?? DB_TABLE_TIMEOUT_MS : ACK_TIMEOUT_MS;
|
|
25781
|
+
const awaited = reply ? "reply" : "ACK";
|
|
25782
|
+
return new Promise((resolve2, reject) => {
|
|
25590
25783
|
const onData = (frame, linkType) => {
|
|
25591
25784
|
const p = parsePortalPacket(frame, linkType);
|
|
25592
|
-
if (!p
|
|
25785
|
+
if (!p)
|
|
25786
|
+
return;
|
|
25787
|
+
if (reply && linkType === PortalLinkType.NOTIFY && typeof p.data === "object" && p.data !== null) {
|
|
25788
|
+
let picked;
|
|
25789
|
+
try {
|
|
25790
|
+
picked = reply.pick(p.data);
|
|
25791
|
+
} catch (e) {
|
|
25792
|
+
cleanup();
|
|
25793
|
+
reject(e instanceof Error ? e : new Error(String(e)));
|
|
25794
|
+
return;
|
|
25795
|
+
}
|
|
25796
|
+
if (picked === void 0)
|
|
25797
|
+
return;
|
|
25798
|
+
cleanup();
|
|
25799
|
+
this.deps.logger?.debug?.(`[rtc] ${sn} cmd ${innerCmd} answered`);
|
|
25800
|
+
resolve2(picked);
|
|
25801
|
+
return;
|
|
25802
|
+
}
|
|
25803
|
+
if (p.segment !== segment)
|
|
25593
25804
|
return;
|
|
25594
25805
|
const acknowledgement = linkType === PortalLinkType.COMMAND && p.commandId === outerCmd && !!p.isResponse;
|
|
25595
25806
|
const d = p.data;
|
|
25596
25807
|
const notification = outerCmd === PORTAL_CMD_CONTROL_PAYLOAD && linkType === PortalLinkType.NOTIFY && p.commandId === PORTAL_CMD_NOTIFY_PAYLOAD && p.isResponse === 0 && p.channel === channel && p.cmd === innerCmd && typeof d?.payload === "object" && d.payload !== null;
|
|
25597
25808
|
if (!acknowledgement && !notification)
|
|
25598
25809
|
return;
|
|
25810
|
+
if (reply && p.errCode === 0)
|
|
25811
|
+
return;
|
|
25599
25812
|
cleanup();
|
|
25600
25813
|
if (acknowledgement && p.errCode !== 0) {
|
|
25601
25814
|
reject(new Error(`rtc: ${sn} rejected cmd ${innerCmd} (err ${p.errCode})`));
|
|
25602
25815
|
} else {
|
|
25603
25816
|
this.deps.logger?.debug?.(`[rtc] ${sn} cmd ${innerCmd} acked`);
|
|
25604
|
-
|
|
25817
|
+
resolve2(void 0);
|
|
25605
25818
|
}
|
|
25606
25819
|
};
|
|
25607
25820
|
const onClose = () => {
|
|
25608
25821
|
cleanup();
|
|
25609
|
-
reject(new Error(`rtc: ${sn} session closed while waiting for cmd ${innerCmd}
|
|
25822
|
+
reject(new Error(`rtc: ${sn} session closed while waiting for cmd ${innerCmd} ${awaited}`));
|
|
25823
|
+
};
|
|
25824
|
+
const onAbort = () => {
|
|
25825
|
+
cleanup();
|
|
25826
|
+
reject(reply?.signal?.reason);
|
|
25610
25827
|
};
|
|
25611
25828
|
const timer = setTimeout(() => {
|
|
25612
25829
|
cleanup();
|
|
25613
|
-
reject(new Error(`rtc: ${sn} cmd ${innerCmd}
|
|
25614
|
-
},
|
|
25830
|
+
reject(new Error(`rtc: ${sn} cmd ${innerCmd} ${awaited} timed out after ${timeoutMs}ms`));
|
|
25831
|
+
}, timeoutMs);
|
|
25615
25832
|
const cleanup = () => {
|
|
25616
25833
|
clearTimeout(timer);
|
|
25617
25834
|
st.session.off("commandData", onData);
|
|
25618
25835
|
st.session.off("close", onClose);
|
|
25836
|
+
reply?.signal?.removeEventListener("abort", onAbort);
|
|
25619
25837
|
this.touch(sn, st);
|
|
25620
25838
|
};
|
|
25839
|
+
if (reply?.signal?.aborted) {
|
|
25840
|
+
onAbort();
|
|
25841
|
+
return;
|
|
25842
|
+
}
|
|
25621
25843
|
st.session.on("commandData", onData);
|
|
25622
25844
|
st.session.on("close", onClose);
|
|
25845
|
+
reply?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
25623
25846
|
if (!st.session.sendCommand(packet)) {
|
|
25624
25847
|
cleanup();
|
|
25625
25848
|
reject(new Error(`rtc: ${sn} command channel not open, cmd ${innerCmd} not sent`));
|
|
@@ -25675,7 +25898,7 @@ var RtcCommandRouter = class {
|
|
|
25675
25898
|
* promise, so an early failure cannot leave a second one rejecting unheard.
|
|
25676
25899
|
*/
|
|
25677
25900
|
bringUp(sn, session) {
|
|
25678
|
-
return new Promise((
|
|
25901
|
+
return new Promise((resolve2, reject) => {
|
|
25679
25902
|
let settled = false;
|
|
25680
25903
|
const finish = (err) => {
|
|
25681
25904
|
if (settled)
|
|
@@ -25692,7 +25915,7 @@ var RtcCommandRouter = class {
|
|
|
25692
25915
|
reject(err);
|
|
25693
25916
|
} else {
|
|
25694
25917
|
this.deps.logger?.info?.(`[rtc] ${sn} command channel up`);
|
|
25695
|
-
|
|
25918
|
+
resolve2();
|
|
25696
25919
|
}
|
|
25697
25920
|
};
|
|
25698
25921
|
const onConnected = () => finish();
|
|
@@ -26121,10 +26344,10 @@ function dpColorFields(spec) {
|
|
|
26121
26344
|
}
|
|
26122
26345
|
|
|
26123
26346
|
// dist/transport/mqtt/clean-codec.js
|
|
26124
|
-
import { randomUUID as
|
|
26347
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
26125
26348
|
function buildCleanDpEnvelope(accountId, deviceSn, dp, value, opts) {
|
|
26126
26349
|
const payload = JSON.stringify({
|
|
26127
|
-
t: opts?.uuid ??
|
|
26350
|
+
t: opts?.uuid ?? randomUUID4(),
|
|
26128
26351
|
protocol: 2,
|
|
26129
26352
|
account_id: accountId,
|
|
26130
26353
|
device_sn: deviceSn,
|
|
@@ -26363,13 +26586,13 @@ var MqttCommandRouter = class _MqttCommandRouter {
|
|
|
26363
26586
|
* timeout with no unhandled listener left behind either way.
|
|
26364
26587
|
*/
|
|
26365
26588
|
waitForMqttMessage(mqtt, match, timeoutMs) {
|
|
26366
|
-
return new Promise((
|
|
26589
|
+
return new Promise((resolve2) => {
|
|
26367
26590
|
const onMessage = (m) => {
|
|
26368
26591
|
const result = match(m);
|
|
26369
26592
|
if (result === void 0)
|
|
26370
26593
|
return;
|
|
26371
26594
|
cleanup();
|
|
26372
|
-
|
|
26595
|
+
resolve2(result);
|
|
26373
26596
|
};
|
|
26374
26597
|
const cleanup = () => {
|
|
26375
26598
|
clearTimeout(timer);
|
|
@@ -26377,7 +26600,7 @@ var MqttCommandRouter = class _MqttCommandRouter {
|
|
|
26377
26600
|
};
|
|
26378
26601
|
const timer = setTimeout(() => {
|
|
26379
26602
|
cleanup();
|
|
26380
|
-
|
|
26603
|
+
resolve2(void 0);
|
|
26381
26604
|
}, timeoutMs);
|
|
26382
26605
|
timer.unref?.();
|
|
26383
26606
|
mqtt.on("message", onMessage);
|
|
@@ -26897,7 +27120,7 @@ function decryptReply(key, resultB64) {
|
|
|
26897
27120
|
}
|
|
26898
27121
|
|
|
26899
27122
|
// dist/transport/tuya/request.js
|
|
26900
|
-
import { randomUUID as
|
|
27123
|
+
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
26901
27124
|
var TUYA_API_ENDPOINT = "https://a1.tuyaeu.com/api.json";
|
|
26902
27125
|
var TUYA_APP_KEY = "w8x4ppqkdxvqnd73ahj9";
|
|
26903
27126
|
var TUYA_APP_SECRET = "pt585qhmt75hwcynchnps9dnxh9suhwd";
|
|
@@ -26929,7 +27152,7 @@ function buildApiParams(action, opts) {
|
|
|
26929
27152
|
a: action.a,
|
|
26930
27153
|
v: action.v,
|
|
26931
27154
|
time: String(opts.time ?? Math.floor(Date.now() / 1e3)),
|
|
26932
|
-
requestId: opts.requestId ??
|
|
27155
|
+
requestId: opts.requestId ?? randomUUID5(),
|
|
26933
27156
|
sid: opts.session.sid,
|
|
26934
27157
|
deviceId: opts.session.deviceId,
|
|
26935
27158
|
chKey: opts.session.chKey,
|
|
@@ -28268,8 +28491,6 @@ var FcmRegistrar = class {
|
|
|
28268
28491
|
};
|
|
28269
28492
|
|
|
28270
28493
|
// dist/transport/push/store.js
|
|
28271
|
-
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, rmSync as rmSync2 } from "node:fs";
|
|
28272
|
-
import { dirname as dirname2 } from "node:path";
|
|
28273
28494
|
var MemoryFcmStore = class {
|
|
28274
28495
|
p = null;
|
|
28275
28496
|
load() {
|
|
@@ -28282,31 +28503,7 @@ var MemoryFcmStore = class {
|
|
|
28282
28503
|
this.p = null;
|
|
28283
28504
|
}
|
|
28284
28505
|
};
|
|
28285
|
-
var FileFcmStore = class {
|
|
28286
|
-
path;
|
|
28287
|
-
constructor(path) {
|
|
28288
|
-
this.path = path;
|
|
28289
|
-
}
|
|
28290
|
-
load() {
|
|
28291
|
-
try {
|
|
28292
|
-
return JSON.parse(readFileSync2(this.path, "utf-8"));
|
|
28293
|
-
} catch {
|
|
28294
|
-
return null;
|
|
28295
|
-
}
|
|
28296
|
-
}
|
|
28297
|
-
save(p) {
|
|
28298
|
-
try {
|
|
28299
|
-
mkdirSync2(dirname2(this.path), { recursive: true });
|
|
28300
|
-
} catch {
|
|
28301
|
-
}
|
|
28302
|
-
writeFileSync2(this.path, JSON.stringify(p, null, 2), { mode: 384 });
|
|
28303
|
-
}
|
|
28304
|
-
clear() {
|
|
28305
|
-
try {
|
|
28306
|
-
rmSync2(this.path);
|
|
28307
|
-
} catch {
|
|
28308
|
-
}
|
|
28309
|
-
}
|
|
28506
|
+
var FileFcmStore = class extends FileSessionStore {
|
|
28310
28507
|
};
|
|
28311
28508
|
|
|
28312
28509
|
// dist/transport/media-failure.js
|
|
@@ -28335,6 +28532,12 @@ var StoredImageCache = class {
|
|
|
28335
28532
|
activeDevices = /* @__PURE__ */ new Set();
|
|
28336
28533
|
activeDownloads = 0;
|
|
28337
28534
|
generation = 0;
|
|
28535
|
+
/**
|
|
28536
|
+
* Bumped by {@link clearLifecycleErrors}. A lifecycle failure from a download started under an older
|
|
28537
|
+
* count is not recorded; the candidate is queued again, unless a newer one is already queued, so the
|
|
28538
|
+
* current session fetches it.
|
|
28539
|
+
*/
|
|
28540
|
+
lifecycle = 0;
|
|
28338
28541
|
constructor(downloader, logger, clock = Date.now, isLifecycleError = () => false) {
|
|
28339
28542
|
this.downloader = downloader;
|
|
28340
28543
|
this.logger = logger;
|
|
@@ -28390,6 +28593,18 @@ var StoredImageCache = class {
|
|
|
28390
28593
|
return Promise.resolve(state.retained.image);
|
|
28391
28594
|
return Promise.reject(new StoredSnapshotUnavailableError(reason, "No stored snapshot is available"));
|
|
28392
28595
|
}
|
|
28596
|
+
/**
|
|
28597
|
+
* Forget every device's recorded lifecycle failure, keeping retained bytes and candidate state.
|
|
28598
|
+
*
|
|
28599
|
+
* A lifecycle failure describes the session that was current when the download ran, so it no longer
|
|
28600
|
+
* holds once a login succeeds again; {@link snapshotStored} then answers from the retained image, or
|
|
28601
|
+
* with the reason a stored image is absent. A download in flight across the call cannot record one.
|
|
28602
|
+
*/
|
|
28603
|
+
clearLifecycleErrors() {
|
|
28604
|
+
this.lifecycle += 1;
|
|
28605
|
+
for (const state of this.devices.values())
|
|
28606
|
+
state.lifecycleError = void 0;
|
|
28607
|
+
}
|
|
28393
28608
|
/** Invalidate all retained and candidate state. */
|
|
28394
28609
|
clear() {
|
|
28395
28610
|
this.generation += 1;
|
|
@@ -28405,18 +28620,23 @@ var StoredImageCache = class {
|
|
|
28405
28620
|
state.inFlight = candidate;
|
|
28406
28621
|
this.activeDownloads += 1;
|
|
28407
28622
|
this.activeDevices.add(candidate.deviceKey);
|
|
28408
|
-
|
|
28623
|
+
const lifecycle = this.lifecycle;
|
|
28624
|
+
void this.downloader(candidate.url, candidate.deviceKey).then((image) => this.complete(candidate, lifecycle, image), (error) => this.complete(candidate, lifecycle, void 0, error));
|
|
28409
28625
|
}
|
|
28410
28626
|
}
|
|
28411
|
-
complete(candidate, image, error) {
|
|
28627
|
+
complete(candidate, lifecycle, image, error) {
|
|
28412
28628
|
this.activeDownloads -= 1;
|
|
28413
28629
|
this.activeDevices.delete(candidate.deviceKey);
|
|
28414
28630
|
const state = this.devices.get(candidate.deviceKey);
|
|
28415
28631
|
if (candidate.generation === this.generation && state?.inFlight === candidate) {
|
|
28416
28632
|
state.inFlight = void 0;
|
|
28417
28633
|
if (error !== void 0 && this.isLifecycleError(error)) {
|
|
28418
|
-
|
|
28419
|
-
|
|
28634
|
+
if (lifecycle === this.lifecycle) {
|
|
28635
|
+
state.lifecycleError = error;
|
|
28636
|
+
state.reason = void 0;
|
|
28637
|
+
} else {
|
|
28638
|
+
state.queued ??= candidate;
|
|
28639
|
+
}
|
|
28420
28640
|
} else if (image === void 0 && this.retryLater(state, candidate, error)) {
|
|
28421
28641
|
state.lifecycleError = void 0;
|
|
28422
28642
|
} else if (image === void 0) {
|
|
@@ -28908,8 +29128,8 @@ var DeviceRegistry = class {
|
|
|
28908
29128
|
const waiters = this.stateWaiters.get(sn);
|
|
28909
29129
|
if (waiters) {
|
|
28910
29130
|
this.stateWaiters.delete(sn);
|
|
28911
|
-
for (const
|
|
28912
|
-
|
|
29131
|
+
for (const resolve2 of waiters)
|
|
29132
|
+
resolve2();
|
|
28913
29133
|
}
|
|
28914
29134
|
}
|
|
28915
29135
|
/**
|
|
@@ -28954,15 +29174,15 @@ var DeviceRegistry = class {
|
|
|
28954
29174
|
awaitRealtimeState(sn, timeoutMs) {
|
|
28955
29175
|
if (this.hasRealtimeState(sn) || timeoutMs <= 0)
|
|
28956
29176
|
return Promise.resolve();
|
|
28957
|
-
return new Promise((
|
|
29177
|
+
return new Promise((resolve2) => {
|
|
28958
29178
|
const timer = setTimeout(() => {
|
|
28959
29179
|
this.stateWaiters.get(sn)?.delete(done);
|
|
28960
|
-
|
|
29180
|
+
resolve2();
|
|
28961
29181
|
}, timeoutMs);
|
|
28962
29182
|
timer.unref?.();
|
|
28963
29183
|
const done = () => {
|
|
28964
29184
|
clearTimeout(timer);
|
|
28965
|
-
|
|
29185
|
+
resolve2();
|
|
28966
29186
|
};
|
|
28967
29187
|
const set = this.stateWaiters.get(sn) ?? /* @__PURE__ */ new Set();
|
|
28968
29188
|
set.add(done);
|
|
@@ -29666,7 +29886,7 @@ var EufyMega = class extends EventEmitter14 {
|
|
|
29666
29886
|
return true;
|
|
29667
29887
|
const delay = Math.min(500, deadline - Date.now());
|
|
29668
29888
|
if (delay > 0)
|
|
29669
|
-
await new Promise((
|
|
29889
|
+
await new Promise((resolve2) => setTimeout(resolve2, delay));
|
|
29670
29890
|
if (refresh.expected !== void 0 && settled())
|
|
29671
29891
|
return true;
|
|
29672
29892
|
}
|
|
@@ -29715,7 +29935,7 @@ var EufyMega = class extends EventEmitter14 {
|
|
|
29715
29935
|
* manager's own log, since by then nothing is waiting to receive it.
|
|
29716
29936
|
*/
|
|
29717
29937
|
recycleStandaloneSession(sn) {
|
|
29718
|
-
const releaseWrite = new Promise((
|
|
29938
|
+
const releaseWrite = new Promise((resolve2) => void setTimeout(resolve2, SESSION_RECYCLE_WAIT_MS).unref?.());
|
|
29719
29939
|
return Promise.race([
|
|
29720
29940
|
this.p2p.resetStandaloneSession(sn).finally(() => this.reopenWiredStation(sn)),
|
|
29721
29941
|
releaseWrite.then(() => this.opts.logger?.debug(`[session ${sn}] recycle still waiting on an attached viewer \u2014 releasing the write that asked for it`))
|
|
@@ -29752,11 +29972,11 @@ var EufyMega = class extends EventEmitter14 {
|
|
|
29752
29972
|
}
|
|
29753
29973
|
/** Limit waiting on an unabortable dependency operation to the remaining semantic-event refresh window. */
|
|
29754
29974
|
beforeDeadline(operation, timeoutMs) {
|
|
29755
|
-
return new Promise((
|
|
29975
|
+
return new Promise((resolve2, reject) => {
|
|
29756
29976
|
const timeout = setTimeout(() => reject(new RefreshWindowClosedError("refresh window closed")), Math.max(0, timeoutMs));
|
|
29757
29977
|
operation.then((value) => {
|
|
29758
29978
|
clearTimeout(timeout);
|
|
29759
|
-
|
|
29979
|
+
resolve2(value);
|
|
29760
29980
|
}, (error) => {
|
|
29761
29981
|
clearTimeout(timeout);
|
|
29762
29982
|
reject(error);
|
|
@@ -29861,12 +30081,16 @@ var EufyMega = class extends EventEmitter14 {
|
|
|
29861
30081
|
/**
|
|
29862
30082
|
* On a successful login, kick off auto-realtime (unless `autoRealtime:false`). Fire-and-forget so
|
|
29863
30083
|
* `login()` returns as soon as the session is ready — realtime channels come up in the background and
|
|
29864
|
-
* surface failures via `error`. Idempotent through the retained generation promise.
|
|
30084
|
+
* surface failures via `error`. Idempotent through the retained generation promise. A login for a
|
|
30085
|
+
* different account drops every stored image; one for the same account drops only the lifecycle
|
|
30086
|
+
* failures recorded against the session it replaces.
|
|
29865
30087
|
*/
|
|
29866
30088
|
afterLogin(result) {
|
|
29867
30089
|
if (result.status === LoginStatus.Ok) {
|
|
29868
30090
|
if (this.storedImageAccount && this.storedImageAccount !== result.session.userId)
|
|
29869
30091
|
this.storedImages?.clear();
|
|
30092
|
+
else
|
|
30093
|
+
this.storedImages?.clearLifecycleErrors();
|
|
29870
30094
|
this.storedImageAccount = result.session.userId;
|
|
29871
30095
|
const auth = this.mega.auth;
|
|
29872
30096
|
if (auth?.userId)
|
|
@@ -29903,12 +30127,12 @@ var EufyMega = class extends EventEmitter14 {
|
|
|
29903
30127
|
if (options.timeoutMs === void 0)
|
|
29904
30128
|
return generation.promise;
|
|
29905
30129
|
const timeoutMs = Math.max(0, options.timeoutMs);
|
|
29906
|
-
return new Promise((
|
|
29907
|
-
const timer = setTimeout(() =>
|
|
30130
|
+
return new Promise((resolve2) => {
|
|
30131
|
+
const timer = setTimeout(() => resolve2(readinessSnapshot(generation.readiness, "timed-out")), timeoutMs);
|
|
29908
30132
|
timer.unref?.();
|
|
29909
30133
|
void generation.promise.then((result) => {
|
|
29910
30134
|
clearTimeout(timer);
|
|
29911
|
-
|
|
30135
|
+
resolve2(result);
|
|
29912
30136
|
});
|
|
29913
30137
|
});
|
|
29914
30138
|
}
|
|
@@ -29949,10 +30173,10 @@ var EufyMega = class extends EventEmitter14 {
|
|
|
29949
30173
|
let acknowledge;
|
|
29950
30174
|
let reject;
|
|
29951
30175
|
let acknowledged = false;
|
|
29952
|
-
const acknowledgement = new Promise((
|
|
30176
|
+
const acknowledgement = new Promise((resolve2, rejectPromise) => {
|
|
29953
30177
|
acknowledge = () => {
|
|
29954
30178
|
acknowledged = true;
|
|
29955
|
-
|
|
30179
|
+
resolve2();
|
|
29956
30180
|
};
|
|
29957
30181
|
reject = rejectPromise;
|
|
29958
30182
|
});
|
|
@@ -30319,8 +30543,8 @@ var EufyMega = class extends EventEmitter14 {
|
|
|
30319
30543
|
if (this.realtimeGeneration?.epoch === this.realtimeEpoch)
|
|
30320
30544
|
return this.realtimeGeneration;
|
|
30321
30545
|
const epoch = this.realtimeEpoch;
|
|
30322
|
-
let
|
|
30323
|
-
const promise = new Promise((done) =>
|
|
30546
|
+
let resolve2;
|
|
30547
|
+
const promise = new Promise((done) => resolve2 = done);
|
|
30324
30548
|
const generation = {
|
|
30325
30549
|
epoch,
|
|
30326
30550
|
readiness: {
|
|
@@ -30330,7 +30554,7 @@ var EufyMega = class extends EventEmitter14 {
|
|
|
30330
30554
|
wiredP2p: createPlaneReadiness()
|
|
30331
30555
|
},
|
|
30332
30556
|
promise,
|
|
30333
|
-
resolve,
|
|
30557
|
+
resolve: resolve2,
|
|
30334
30558
|
abort: new AbortController(),
|
|
30335
30559
|
settled: false
|
|
30336
30560
|
};
|
|
@@ -30481,6 +30705,9 @@ var EufyMega = class extends EventEmitter14 {
|
|
|
30481
30705
|
* device absent from the previous pass is skipped — first sight is discovery, not a transition;
|
|
30482
30706
|
* {@link deviceState} answers an initial reading.
|
|
30483
30707
|
*
|
|
30708
|
+
* A device with changed params is re-resolved by {@link widenCapabilities} once per pass, however many
|
|
30709
|
+
* of its params moved, since each re-resolution is a cloud param-list read.
|
|
30710
|
+
*
|
|
30484
30711
|
* Then re-reads every Tuya clean-line robot from the Tuya cloud ({@link readTuyaDevices}): its state
|
|
30485
30712
|
* is not in the cloud record this pass diffs and no live channel carries it, so the poll is where it
|
|
30486
30713
|
* stays fresh. That read runs after the `try`, not inside it, so a cloud error on the list diff does
|
|
@@ -30499,8 +30726,8 @@ var EufyMega = class extends EventEmitter14 {
|
|
|
30499
30726
|
this.emitSemantic(out.event, out.payload, { refresh: out.refresh });
|
|
30500
30727
|
for (const dev of diff.reported)
|
|
30501
30728
|
this.emit("deviceState", this.stateOf(dev));
|
|
30502
|
-
for (const
|
|
30503
|
-
await this.widenCapabilities(
|
|
30729
|
+
for (const sn of new Set(diff.params.map((c) => c.deviceSn)))
|
|
30730
|
+
await this.widenCapabilities(sn);
|
|
30504
30731
|
} catch (e) {
|
|
30505
30732
|
this.reportError(e);
|
|
30506
30733
|
}
|
|
@@ -30886,31 +31113,72 @@ var EufyMega = class extends EventEmitter14 {
|
|
|
30886
31113
|
return this.p2p.getSessions();
|
|
30887
31114
|
}
|
|
30888
31115
|
/**
|
|
30889
|
-
* The people enrolled on a station, read from its own `person_basic_info` table
|
|
31116
|
+
* The people enrolled on a station, read from its own `person_basic_info` table through
|
|
31117
|
+
* {@link readStationTable}.
|
|
30890
31118
|
*
|
|
30891
31119
|
* The cloud roster (`api.getFaces()`) answers empty for an account that keeps its faces on the
|
|
30892
31120
|
* station, because they were never uploaded — the station holds them.
|
|
30893
31121
|
*
|
|
30894
|
-
* Resolves with the rows the station returned
|
|
30895
|
-
*
|
|
30896
|
-
* reply is `P2PSession.readDatabase`'s; this resolves which station and which account id.
|
|
31122
|
+
* Resolves with the rows the station returned that are objects. Rejects as {@link readStationTable}
|
|
31123
|
+
* does.
|
|
30897
31124
|
*
|
|
30898
31125
|
* `stranger<n>` names are the station's own placeholders for a face nobody has named, and are
|
|
30899
31126
|
* returned as they arrive; a caller that wants only named people filters them.
|
|
30900
31127
|
*/
|
|
30901
31128
|
async getStationFaces(stationSn, opts = {}) {
|
|
31129
|
+
const rows = await this.readStationTable(stationSn, "person_basic_info", opts);
|
|
31130
|
+
return rows.filter((row) => typeof row === "object" && row !== null);
|
|
31131
|
+
}
|
|
31132
|
+
/**
|
|
31133
|
+
* The rows of one of a station's own tables (`history_record_info`, `person_basic_info`, …), read over
|
|
31134
|
+
* the transport the station speaks: the control channel of a HomeBase S1 Pro (T9000), P2P for any
|
|
31135
|
+
* other station. The query is the table's whole content; the rows are the station's, shaped by it.
|
|
31136
|
+
*
|
|
31137
|
+
* Resolves with the rows the station returned. Rejects on a serial not in the device list, on a record
|
|
31138
|
+
* stating no `admin_user_id`, on a station that cannot be reached, on a query the station refuses, and
|
|
31139
|
+
* on the reply not arriving in time. Assembling the reply is the transport's; this resolves which
|
|
31140
|
+
* station, which transport and which account id.
|
|
31141
|
+
*/
|
|
31142
|
+
async readStationTable(stationSn, table, opts = {}) {
|
|
31143
|
+
const { station, accountId, route } = await this.stationReadTarget("readStationTable", stationSn);
|
|
31144
|
+
if (route)
|
|
31145
|
+
return this.rtc.readTable(route, table, opts);
|
|
31146
|
+
await this.p2p.ensureStation(station, opts.signal);
|
|
31147
|
+
const session = this.p2p.getSessions().get(station);
|
|
31148
|
+
if (!session)
|
|
31149
|
+
throw new Error(`readStationTable: no P2P session for ${station}`);
|
|
31150
|
+
return session.readDatabase(table, { accountId, ...opts });
|
|
31151
|
+
}
|
|
31152
|
+
/**
|
|
31153
|
+
* One image stored on a HomeBase S1 Pro (T9000), by the on-station path a table row names
|
|
31154
|
+
* (`thumb_path`, …), read over the station's control channel.
|
|
31155
|
+
*
|
|
31156
|
+
* Resolves with the image bytes as the station holds them. Rejects on a serial not in the device list,
|
|
31157
|
+
* on a record stating no `admin_user_id`, on a station other than a T9000, on a station that cannot be
|
|
31158
|
+
* reached, and on the reply not arriving in time. Fetching the image is the transport's; this resolves
|
|
31159
|
+
* which station and which account id.
|
|
31160
|
+
*/
|
|
31161
|
+
async readStationImage(stationSn, filePath, opts = {}) {
|
|
31162
|
+
const { station, route } = await this.stationReadTarget("readStationImage", stationSn);
|
|
31163
|
+
if (!route)
|
|
31164
|
+
throw new Error(`readStationImage: ${station} is not a T9000; its stored images are not read here`);
|
|
31165
|
+
return this.rtc.readImage(route, filePath, opts);
|
|
31166
|
+
}
|
|
31167
|
+
/**
|
|
31168
|
+
* The station a database read goes to, its `admin_user_id` and, for a T9000, its RTC route. Throws,
|
|
31169
|
+
* prefixed with `op`, on a serial the device list does not carry and on a record stating no admin id.
|
|
31170
|
+
*/
|
|
31171
|
+
async stationReadTarget(op, stationSn) {
|
|
30902
31172
|
if (!this.registry.list().length)
|
|
30903
31173
|
await this.getDevices();
|
|
30904
31174
|
const station = this.p2p.stationKeyOf(stationSn);
|
|
31175
|
+
const found = this.stationOf(station);
|
|
31176
|
+
if (!found.station)
|
|
31177
|
+
throw new Error(`${op}: ${station} is not in the device list`);
|
|
30905
31178
|
const accountId = this.adminUserIdOf(station);
|
|
30906
31179
|
if (!accountId)
|
|
30907
|
-
throw new Error(
|
|
30908
|
-
|
|
30909
|
-
const session = this.p2p.getSessions().get(station);
|
|
30910
|
-
if (!session)
|
|
30911
|
-
throw new Error(`getStationFaces: no P2P session for ${station}`);
|
|
30912
|
-
const rows = await session.readDatabase("person_basic_info", { accountId, ...opts });
|
|
30913
|
-
return rows.filter((row) => typeof row === "object" && row !== null);
|
|
31180
|
+
throw new Error(`${op}: no admin_user_id on record for ${station}`);
|
|
31181
|
+
return { station, accountId, route: found.t9000 ? this.rtcRouteFor(station) : void 0 };
|
|
30914
31182
|
}
|
|
30915
31183
|
/** The station's own `admin_user_id`, which its database refuses the query without. */
|
|
30916
31184
|
adminUserIdOf(stationSn) {
|
|
@@ -31137,6 +31405,9 @@ var EufyMega = class extends EventEmitter14 {
|
|
|
31137
31405
|
* Returns the connected client rather than installing it. Registration can outlive a `disconnect()`,
|
|
31138
31406
|
* and {@link ensureRealtime} owns the decision of whether a finished bring-up is still the current
|
|
31139
31407
|
* one — so this never overwrites a channel a later login already brought up.
|
|
31408
|
+
*
|
|
31409
|
+
* Persisting the seen ids after each push reports a failed `save` through `error` rather than throwing:
|
|
31410
|
+
* the push handler runs inside the MCS socket's `data` callback, where a throw is an uncaught exception.
|
|
31140
31411
|
*/
|
|
31141
31412
|
async startPush(signal) {
|
|
31142
31413
|
if (!this.mega.auth)
|
|
@@ -31183,9 +31454,13 @@ var EufyMega = class extends EventEmitter14 {
|
|
|
31183
31454
|
if (dsn)
|
|
31184
31455
|
this.prewarmForEvent(out.event, dsn);
|
|
31185
31456
|
}
|
|
31186
|
-
|
|
31457
|
+
try {
|
|
31458
|
+
store.save({ creds: persistedCreds, persistentIds: client.getPersistentIds() });
|
|
31459
|
+
} catch (e) {
|
|
31460
|
+
this.reportError(e);
|
|
31461
|
+
}
|
|
31187
31462
|
});
|
|
31188
|
-
await new Promise((
|
|
31463
|
+
await new Promise((resolve2, reject) => {
|
|
31189
31464
|
const cleanup = () => {
|
|
31190
31465
|
signal?.removeEventListener("abort", onAbort);
|
|
31191
31466
|
client.off("connect", onConnect);
|
|
@@ -31193,7 +31468,7 @@ var EufyMega = class extends EventEmitter14 {
|
|
|
31193
31468
|
};
|
|
31194
31469
|
const onConnect = () => {
|
|
31195
31470
|
cleanup();
|
|
31196
|
-
|
|
31471
|
+
resolve2();
|
|
31197
31472
|
};
|
|
31198
31473
|
const onError = (error) => {
|
|
31199
31474
|
cleanup();
|