@mega-yfue/eufy-sdk 0.2.0-beta.0 → 0.2.0-beta.2

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/README.md CHANGED
@@ -23,11 +23,6 @@
23
23
 
24
24
  ---
25
25
 
26
- > [!IMPORTANT]
27
- > **Not usable yet.** This repository is being set up: the scaffold, the CI gate and the docs pipeline
28
- > are in place, the library source lands next. `0.0.1` exists on npm only to prove the release
29
- > pipeline works — it is an empty package. Wait for `0.1.0`.
30
-
31
26
  ## What it is
32
27
 
33
28
  A TypeScript SDK for the Anker eufy cloud that the current eufy app speaks. It logs in (captcha and 2FA
@@ -37,14 +32,18 @@ you drive through a **typed, fluent API**:
37
32
  ```ts
38
33
  const dev = await eufy.getDevice(sn);
39
34
 
40
- const stored = await dev.camera()?.snapshotStored?.(); // latest retained push JPEG
41
- const fresh = await dev.camera()?.snapshotLive(); // explicit fresh live capture
42
- await dev.panTilt()?.rotate(PtzDirection.left);
43
- await dev.light()?.setBrightness(60);
35
+ const stored = await dev.camera?.()?.snapshotStored?.(); // latest retained push JPEG
36
+ const fresh = await dev.camera?.()?.snapshotLive?.(); // explicit fresh live capture
37
+ await dev.ptz?.()?.rotate(PtzDirection.left);
38
+ await dev.light?.()?.setBrightness(60);
44
39
 
45
- eufy.on("motion", (e) => console.log(e.device.name, "saw something"));
40
+ eufy.on("motion", (e) => console.log(e.deviceSn, "saw something"));
46
41
  ```
47
42
 
43
+ Accessors and methods are optional because both are **evidence-gated**: a device exposes exactly the
44
+ features it reported, so the optionality states that one may be absent. An **unverified write path
45
+ throws** rather than send a frame it cannot stand behind.
46
+
48
47
  Realtime arrives over **P2P** (cameras and HomeBases), **secure MQTT** (appliances) and **push**
49
48
  (events), all surfaced as typed semantic events. Live **video streaming** works, with one shared pull
50
49
  fanned out to every consumer.
@@ -56,12 +55,10 @@ code path and an unlisted or future device resolves the same way as a known one.
56
55
  ## Install
57
56
 
58
57
  ```bash
59
- npm install @mega-yfue/eufy-sdk
58
+ npm install @mega-yfue/eufy-sdk # latest stable
59
+ npm install @mega-yfue/eufy-sdk@beta # the prerelease of the version in review
60
60
  ```
61
61
 
62
- Releases go to **npmjs**, published from CI with provenance. Prereleases ship on the `beta` channel
63
- (`npm install @mega-yfue/eufy-sdk@beta`) while a version is still under review.
64
-
65
62
  **Node.js ≥ 24.5.0** is required, not just recommended (see [`.nvmrc`](./.nvmrc)). `ffmpeg` is
66
63
  optional — only the live JPEG snapshot and one-shot mp4 record paths use it,
67
64
  and a host that ships its own build names it with `new EufyMega({ ffmpegPath })` rather than needing
@@ -69,34 +66,13 @@ one on `PATH`.
69
66
 
70
67
  ## Documentation
71
68
 
72
- The guides at **<https://mega-yfue.github.io/>** are the source of truth; this README stays a thin
73
- landing page. They cover installing and logging in, devices and capabilities, events and realtime
74
- transports, consuming live media, and the generated API reference. Runnable, typechecked samples live
75
- in [`examples/`](./examples/).
76
-
77
- ## Design
78
-
79
- Four layers, one dependency direction — `core` → `transport` → `model` → `client`:
80
-
81
- ```
82
- src/
83
- core/ shared floor: crypto, cross-layer contracts, value types, session store
84
- transport/ every byte-on-a-wire module: http, mqtt, p2p, push, tuya
85
- model/ Device + one self-contained module per capability
86
- client/ the facade: login, device registry, event fan-out
87
- index.ts public surface — one `export *` per layer barrel
88
- ```
89
-
90
- **Capability ↔ transport decorrelation is a hard, CI-enforced rule:** `model/` never imports
91
- `transport/` and vice versa. A capability describes what a value MEANS; a transport moves bytes and
92
- never names a feature. Anything genuinely shared is a contract in `core/`. That rule and the rest of
93
- the code practice are in [AGENTS.md](./AGENTS.md).
94
-
95
- Three runtime dependencies — `mqtt`, `protobufjs`, `jpeg-js` — and that is deliberate. HTTP is native
96
- `fetch`, hashing and ciphers are `node:crypto`, 64-bit integers are `BigInt`.
69
+ The guides at **<https://mega-yfue.github.io/>** cover installing and logging in, devices and
70
+ capabilities, events and realtime transports, consuming live media, and the generated API reference.
71
+ Runnable, typechecked samples live in [`examples/`](./examples/).
97
72
 
98
- **Unverified write paths throw rather than guess.** Some writes are fire-and-forget, so a guessed
99
- frame looks exactly like success; the SDK refuses instead of pretending.
73
+ The architecture four layers with one dependency direction, and the CI-enforced rule that keeps
74
+ capabilities and transports from importing each other is in [AGENTS.md](./AGENTS.md), with the rest of
75
+ the code practice.
100
76
 
101
77
  ## Develop
102
78
 
@@ -707,11 +707,38 @@ export declare class EufyMega extends EventEmitter {
707
707
  * {@link ensureMqttStarted} owns installing it, subscribing devices, and the epoch check, so that
708
708
  * lifecycle lives in exactly one place. Only ever called through {@link ensureMqttStarted}.
709
709
  *
710
+ * Identified by a client id built from this client's `openudid`, not by the certificate's name:
711
+ * that name is `{user_id}-{app_name}`, which every client on the account shares per line, and a
712
+ * duplicate client id is a takeover the broker resolves by evicting the incumbent. The id shape is
713
+ * the app's own (`android-{app_name}-{uid}-{uuid}-{ts}`, see {@link buildAppShapedClientId}), which
714
+ * the broker grants on the `eufy_security` credential.
715
+ *
716
+ * It separates two clients exactly as far as their `openudid` does: a caller that supplies none
717
+ * gets the value derived from the account, which every such client shares — the same condition
718
+ * under which their logins already displace each other (`MegaClientConfig.openudid`).
719
+ *
720
+ * A client id the broker REFUSES falls back to the certificate's name, since a shared channel beats
721
+ * none, and that transport then keeps that name until {@link disconnect}. Only a refusal: a connect
722
+ * that fails for any other reason rejects the bring-up, which clears its memo in
723
+ * {@link ensureMqttStarted} so the next one asks under this client's own id again — a dropped
724
+ * socket must not be what moves a process onto the shared name for good.
725
+ */
726
+ private startMqtt;
727
+ /**
728
+ * Connect one secure-MQTT transport under `clientId`, or under the certificate's own name when it is
729
+ * omitted, and wire its decode and fan-out. See {@link startMqtt} for which id is used and why.
730
+ *
731
+ * The fan-out is wired once the connection stands, so an attempt that is discarded — a client id the
732
+ * broker refuses, a socket that dies mid-handshake — never reports a connection a consumer never had;
733
+ * the connect it just completed is announced here instead. Errors raised while connecting are held
734
+ * only to keep an emitter without an `error` listener from throwing, and are reported once the
735
+ * transport is one a consumer owns.
736
+ *
710
737
  * The inbound decode is gated by the reporting device's own capabilities, so one line's decoder never
711
738
  * runs against another's traffic, and the DP frame is unwrapped here — the layer that may import the
712
739
  * transport — so a capability reads tags without owning any framing.
713
740
  */
714
- private startMqtt;
741
+ private connectMqtt;
715
742
  /**
716
743
  * Subscribe the devices on one credential scope; a failing subscribe is reported, not fatal (one
717
744
  * unreachable device must not stop the rest of the roster from coming up).
package/dist/index.js CHANGED
@@ -967,7 +967,15 @@ var MegaHttpClient = class {
967
967
  * restored-session short-circuit must NOT treat it as a usable session. Cleared on Ok/reset. */
968
968
  pending2fa = false;
969
969
  tokenExpiresAt = 0;
970
- /** Stable per-install device id — the auth token is bound to it. */
970
+ /**
971
+ * Stable per-install device id: `openudid` as configured, as restored from the session store, or as
972
+ * derived from the ACCOUNT when neither supplied one — two clients that configure none therefore
973
+ * share it, and are one install as far as everything keyed on this is concerned.
974
+ *
975
+ * Two things are keyed on it, and both fail the same way when it is shared: the auth token is bound
976
+ * to it, so each login displaces the other's session, and the secure-MQTT client id is built from it,
977
+ * so each connection evicts the other's channel.
978
+ */
971
979
  openudid;
972
980
  /** The device model reported to the cloud (explicit `phoneModel`, else a stable random one). */
973
981
  phoneModel;
@@ -1805,6 +1813,9 @@ function parseSecureTopic(topic) {
1805
1813
 
1806
1814
  // dist/transport/mqtt/secure-mqtt.js
1807
1815
  var SUBACK_FAILURE = 128;
1816
+ function isNotAuthorized(err) {
1817
+ return err instanceof Error && /connection refused/i.test(err.message) && /not authori[sz]ed/i.test(err.message);
1818
+ }
1808
1819
  var SecureMqtt = class extends EventEmitter {
1809
1820
  kind = "smqtt";
1810
1821
  client;
@@ -1864,6 +1875,7 @@ var SecureMqtt = class extends EventEmitter {
1864
1875
  }
1865
1876
  this.emit("connect");
1866
1877
  });
1878
+ client.on("connect", () => this.logger.info(`[smqtt] connected (${c.app_name ?? "default"})`));
1867
1879
  client.on("reconnect", () => this.logger.warn("[smqtt] reconnecting"));
1868
1880
  client.on("close", () => this.emit("disconnect", "close"));
1869
1881
  client.on("error", (err) => {
@@ -1932,6 +1944,16 @@ var SecureMqtt = class extends EventEmitter {
1932
1944
  }
1933
1945
  };
1934
1946
 
1947
+ // dist/transport/mqtt/app-client-id.js
1948
+ import { createHash as createHash4 } from "node:crypto";
1949
+ function buildAppShapedClientId(input) {
1950
+ const ts = input.timestamp ?? Math.floor(Date.now() / 1e3);
1951
+ return `android-${input.appName}-${input.uid}-${input.mqttUuid}-${ts}`;
1952
+ }
1953
+ function mqttUuidFrom(installId) {
1954
+ return createHash4("sha256").update(installId).digest("hex").slice(0, 16);
1955
+ }
1956
+
1935
1957
  // dist/core/util.js
1936
1958
  var Timer = class {
1937
1959
  handle;
@@ -6868,6 +6890,16 @@ var KEYPAD = {
6868
6890
  // dist/model/capabilities/arming.js
6869
6891
  var STATION_CHANNEL2 = 255;
6870
6892
  var ArmingMode = {
6893
+ /** Armed — full protection, nobody home (wire value 0). */
6894
+ away: "away",
6895
+ /** Armed for occupancy — reduced/perimeter protection while home (wire value 1). */
6896
+ home: "home",
6897
+ /** Custom 1 — a user-defined posture configured in the app (wire value 3). */
6898
+ custom1: "custom1",
6899
+ /** Disarmed — no alarms; sensors still report state (wire value 63). */
6900
+ disarmed: "disarmed"
6901
+ };
6902
+ var AlarmDelayMode = {
6871
6903
  /** Armed — full protection, nobody home (wire value 0). */
6872
6904
  away: "away",
6873
6905
  /** Armed for occupancy — reduced/perimeter protection while home (wire value 1). */
@@ -6883,10 +6915,11 @@ var ARMING_CMD = {
6883
6915
  * mValue3:0, `payload:{mode_type:<int>, user_name:<string>}`.
6884
6916
  *
6885
6917
  * ⚠️ Only 3 of the 9 modes were exercised in that capture — `mode_type` 0 (away), 63 (disarmed), 1
6886
- * (home), all confirmed byte-exact, and those three are the whole of {@link ArmingMode}. Re-confirmed
6887
- * live 2026-08-05: each reported its own MODE_SWITCH push within ~5s of the write. The remaining six are
6888
- * named by the app but never observed leaving it, so this capability reads them and refuses to send
6889
- * them. See `ARMING_MODE_WIRE` for the per-value breakdown.
6918
+ * (home), all confirmed byte-exact. Re-confirmed live 2026-08-05: each reported its own MODE_SWITCH push
6919
+ * within ~5s of the write. `custom1` 3 joined {@link ArmingMode} on a live confirmation rather than a
6920
+ * capture, making four settable in total. The remaining five are named by the app but never observed
6921
+ * leaving it, so this capability reads them and refuses to send them. See `ARMING_MODE_WIRE` for the
6922
+ * per-value breakdown.
6890
6923
  */
6891
6924
  SET_ARMING: 1224,
6892
6925
  /**
@@ -6922,7 +6955,6 @@ var ARMING_MODE_WIRE = {
6922
6955
  schedule: 2,
6923
6956
  // ⚠️ reportable, NOT settable — see the doc comment above
6924
6957
  custom1: 3,
6925
- // ⚠️ reportable, NOT settable — see the doc comment above
6926
6958
  custom2: 4,
6927
6959
  // ⚠️ reportable, NOT settable — see the doc comment above
6928
6960
  custom3: 5,
@@ -6965,9 +6997,9 @@ function alarmDelayCommand(mode, config, ctx) {
6965
6997
  var ARMING_MEMBERS = {
6966
6998
  /**
6967
6999
  * The one member whose write domain is NARROWER than its read: `enumValues` names all nine modes a
6968
- * station can report, and the argument's `values` publishes only the three whose wire was captured. That
6969
- * argument IS the domain the derived setter enforces and the refusal names, so an uncaptured mode is
6970
- * refused by naming the three that work — nine labels for the read and three for the write, off one
7000
+ * station can report, and the argument's `values` publishes only the four whose write is confirmed. That
7001
+ * argument IS the domain the derived setter enforces and the refusal names, so an unconfirmed mode is
7002
+ * refused by naming the four that work — nine labels for the read and four for the write, off one
6971
7003
  * declaration.
6972
7004
  *
6973
7005
  * `armingCommand` may also throw synchronously (missing account identity) and `bindMembers` turns that
@@ -6988,7 +7020,7 @@ var ARMING_MEMBERS = {
6988
7020
  enumValues: ARMING_MODE_LABELS,
6989
7021
  provenance: "verified",
6990
7022
  args: [{ name: "mode", kind: "enum", values: SETTABLE_MODES }],
6991
- description: "Guard mode (verified: param 1224 = GUARD_MODE, read/write mechanism confirmed). Reads all 9 modes the app defines; SETS only the 3 whose write is wire-captured (away/home/disarmed) \u2014 schedule/custom1/custom2/custom3/off/geo are named by the app but no capture shows one being sent, so they are refused rather than guessed; see ARMING_MODE_WIRE in arming.ts for the breakdown.",
7023
+ description: "Guard mode (verified: param 1224 = GUARD_MODE, read/write mechanism confirmed). Reads all 9 modes the app defines; SETS only the 4 whose write is confirmed (away/home/custom1/disarmed) \u2014 schedule/custom2/custom3/off/geo are named by the app but no capture shows one being sent, so they are refused rather than guessed; see ARMING_MODE_WIRE in arming.ts for the breakdown.",
6992
7024
  observation: {
6993
7025
  event: "armingModeChanged",
6994
7026
  reflects: (value) => ({ param: ARMING_CMD.SET_ARMING, expected: ARMING_MODE_WIRE[armingModeOf(value)] }),
@@ -7009,9 +7041,10 @@ var ARMING_MEMBERS = {
7009
7041
  * this mode — there is no known GET to fetch it automatically, and a wrong guess here can silently
7010
7042
  * misconfigure which sensors arm/trigger for real.
7011
7043
  *
7012
- * Takes {@link ArmingMode}, so a delay can only be configured for a mode whose `mode_id` integer is
7013
- * captured. The frame carries that same integer, so a schedule/custom mode would be the identical guess
7014
- * `setMode` refuses.
7044
+ * Takes {@link AlarmDelayMode}, not {@link ArmingMode}: a delay is configurable only for a mode whose
7045
+ * integer is captured on THIS command, and `custom1` is confirmed on cmd 1224 only. The frame carries
7046
+ * that integer in `mode_id` with no runtime validation and no readback, so a mode outside this union
7047
+ * would be the same unverified guess `setMode` refuses.
7015
7048
  */
7016
7049
  setAlarmDelayConfig: method(({ ctx, sink }) => (mode, config) => {
7017
7050
  try {
@@ -20168,17 +20201,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
20168
20201
  };
20169
20202
 
20170
20203
  // dist/transport/mqtt/command-router.js
20171
- import { randomBytes as randomBytes4 } from "node:crypto";
20172
-
20173
- // dist/transport/mqtt/app-client-id.js
20174
20204
  import { randomBytes as randomBytes3 } from "node:crypto";
20175
- function buildAppShapedClientId(input) {
20176
- const ts = input.timestamp ?? Math.floor(Date.now() / 1e3);
20177
- return `android-${input.appName}-${input.uid}-${input.mqttUuid}-${ts}`;
20178
- }
20179
- function generateMqttUuid() {
20180
- return randomBytes3(8).toString("hex");
20181
- }
20182
20205
 
20183
20206
  // dist/transport/mqtt/broker-discovery.js
20184
20207
  import { resolve4 } from "node:dns/promises";
@@ -20556,9 +20579,9 @@ function buildFf09MqttEnvelope(input) {
20556
20579
  const head = {
20557
20580
  version: "1.0.0.1",
20558
20581
  client_id: input.clientId,
20559
- sess_id: input.sessId ?? randomBytes4(2).toString("hex"),
20582
+ sess_id: input.sessId ?? randomBytes3(2).toString("hex"),
20560
20583
  msg_seq: 1,
20561
- seed: input.seed ?? randomBytes4(16).toString("hex"),
20584
+ seed: input.seed ?? randomBytes3(16).toString("hex"),
20562
20585
  timestamp: input.timestamp ?? Math.floor(Date.now() / 1e3),
20563
20586
  cmd_status: 2,
20564
20587
  cmd: 9,
@@ -20569,9 +20592,6 @@ function buildFf09MqttEnvelope(input) {
20569
20592
  var MqttCommandRouter = class _MqttCommandRouter {
20570
20593
  deps;
20571
20594
  logger;
20572
- /** Stable per-process install id for the app-shaped MQTT client_id (see {@link buildAppShapedClientId}) —
20573
- * generated once, reused for every security-MQTT connect this router makes. */
20574
- mqttUuid;
20575
20595
  constructor(deps) {
20576
20596
  this.deps = deps;
20577
20597
  this.logger = deps.logger ?? noopLogger;
@@ -20678,9 +20698,7 @@ var MqttCommandRouter = class _MqttCommandRouter {
20678
20698
  */
20679
20699
  async ensureSecurityMqttFor(dev) {
20680
20700
  const creds = await this.deps.mega.getUserMqttInfo("eufy_security");
20681
- if (!this.mqttUuid)
20682
- this.mqttUuid = generateMqttUuid();
20683
- const mqttUuid = this.mqttUuid;
20701
+ const mqttUuid = mqttUuidFrom(this.deps.mega.openudid);
20684
20702
  const clientIdFor = () => buildAppShapedClientId({ appName: "eufy_security", uid: creds.user_id ?? "", mqttUuid });
20685
20703
  const results = await discoverReachableInstance({
20686
20704
  hostname: creds.endpoint_addr,
@@ -21175,7 +21193,7 @@ function deriveTuyaAccount(eufyUserId, phoneCode) {
21175
21193
  }
21176
21194
 
21177
21195
  // dist/transport/tuya/sign.js
21178
- import { createHash as createHash4, createHmac as createHmac3 } from "node:crypto";
21196
+ import { createHash as createHash5, createHmac as createHmac3 } from "node:crypto";
21179
21197
  var SIGN_ALLOWLIST = /* @__PURE__ */ new Set([
21180
21198
  "a",
21181
21199
  "v",
@@ -21199,7 +21217,7 @@ var SIGN_ALLOWLIST = /* @__PURE__ */ new Set([
21199
21217
  "sp"
21200
21218
  ]);
21201
21219
  function swapMd5(postData) {
21202
- const h = createHash4("md5").update(postData, "utf-8").digest("hex");
21220
+ const h = createHash5("md5").update(postData, "utf-8").digest("hex");
21203
21221
  return h.slice(8, 16) + h.slice(0, 8) + h.slice(24, 32) + h.slice(16, 24);
21204
21222
  }
21205
21223
  function buildSignPreimage(params) {
@@ -21224,7 +21242,7 @@ var HmacSigner = class {
21224
21242
  var TUYA_CHKEY = "7cbfe6d8";
21225
21243
 
21226
21244
  // dist/transport/tuya/client.js
21227
- import { randomBytes as randomBytes5, createHash as createHash5, createPublicKey, publicEncrypt, constants as constants2 } from "node:crypto";
21245
+ import { randomBytes as randomBytes4, createHash as createHash6, createPublicKey, publicEncrypt, constants as constants2 } from "node:crypto";
21228
21246
 
21229
21247
  // dist/transport/tuya/request.js
21230
21248
  import { randomUUID as randomUUID3 } from "node:crypto";
@@ -21336,7 +21354,7 @@ function buildUidTokenCreateAction(countryCode, uid) {
21336
21354
 
21337
21355
  // dist/transport/tuya/client.js
21338
21356
  function genDeviceId() {
21339
- return randomBytes5(22).toString("hex");
21357
+ return randomBytes4(22).toString("hex");
21340
21358
  }
21341
21359
  function rsaEncryptPassword(password, publicKeyDecimal, exponentDecimal) {
21342
21360
  const bigintToBuffer = (n) => {
@@ -21351,7 +21369,7 @@ function rsaEncryptPassword(password, publicKeyDecimal, exponentDecimal) {
21351
21369
  },
21352
21370
  format: "jwk"
21353
21371
  });
21354
- const md5Hex = createHash5("md5").update(password).digest("hex");
21372
+ const md5Hex = createHash6("md5").update(password).digest("hex");
21355
21373
  return publicEncrypt({ key: rsaKey, padding: constants2.RSA_PKCS1_PADDING }, Buffer.from(md5Hex)).toString("hex");
21356
21374
  }
21357
21375
  var TuyaClient = class {
@@ -22443,7 +22461,7 @@ var PushClient = class _PushClient extends EventEmitter8 {
22443
22461
  };
22444
22462
 
22445
22463
  // dist/transport/push/fcm.js
22446
- import { randomBytes as randomBytes6 } from "node:crypto";
22464
+ import { randomBytes as randomBytes5 } from "node:crypto";
22447
22465
  import { setTimeout as sleep3 } from "node:timers/promises";
22448
22466
  var FCM = {
22449
22467
  PROJECT_ID: "batterycam-3250a",
@@ -22456,7 +22474,7 @@ var FCM = {
22456
22474
  SDK_VERSION: "a:16.3.1"
22457
22475
  };
22458
22476
  function generateFid() {
22459
- const b = randomBytes6(17);
22477
+ const b = randomBytes5(17);
22460
22478
  b[0] = 112 + b[0] % 16;
22461
22479
  return b.toString("base64url").slice(0, 22);
22462
22480
  }
@@ -24750,9 +24768,21 @@ var EufyMega = class extends EventEmitter9 {
24750
24768
  * {@link ensureMqttStarted} owns installing it, subscribing devices, and the epoch check, so that
24751
24769
  * lifecycle lives in exactly one place. Only ever called through {@link ensureMqttStarted}.
24752
24770
  *
24753
- * The inbound decode is gated by the reporting device's own capabilities, so one line's decoder never
24754
- * runs against another's traffic, and the DP frame is unwrapped here the layer that may import the
24755
- * transport so a capability reads tags without owning any framing.
24771
+ * Identified by a client id built from this client's `openudid`, not by the certificate's name:
24772
+ * that name is `{user_id}-{app_name}`, which every client on the account shares per line, and a
24773
+ * duplicate client id is a takeover the broker resolves by evicting the incumbent. The id shape is
24774
+ * the app's own (`android-{app_name}-{uid}-{uuid}-{ts}`, see {@link buildAppShapedClientId}), which
24775
+ * the broker grants on the `eufy_security` credential.
24776
+ *
24777
+ * It separates two clients exactly as far as their `openudid` does: a caller that supplies none
24778
+ * gets the value derived from the account, which every such client shares — the same condition
24779
+ * under which their logins already displace each other (`MegaClientConfig.openudid`).
24780
+ *
24781
+ * A client id the broker REFUSES falls back to the certificate's name, since a shared channel beats
24782
+ * none, and that transport then keeps that name until {@link disconnect}. Only a refusal: a connect
24783
+ * that fails for any other reason rejects the bring-up, which clears its memo in
24784
+ * {@link ensureMqttStarted} so the next one asks under this client's own id again — a dropped
24785
+ * socket must not be what moves a process onto the shared name for good.
24756
24786
  */
24757
24787
  async startMqtt(scope) {
24758
24788
  const auth = this.mega.auth;
@@ -24761,7 +24791,41 @@ var EufyMega = class extends EventEmitter9 {
24761
24791
  if (!this.registry.list().length)
24762
24792
  await this.getDevices();
24763
24793
  const creds = await this.getUserMqttInfo(mqttAppName(scope));
24764
- const transport = new SecureMqtt({ credentials: creds, logger: this.opts.logger });
24794
+ const ownClientId = buildAppShapedClientId({
24795
+ appName: creds.app_name ?? mqttAppName(scope) ?? "eufy_mega",
24796
+ uid: creds.user_id ?? auth.userId,
24797
+ mqttUuid: mqttUuidFrom(this.mega.openudid)
24798
+ });
24799
+ try {
24800
+ return await this.connectMqtt(creds, ownClientId);
24801
+ } catch (e) {
24802
+ if (!isNotAuthorized(e))
24803
+ throw e;
24804
+ this.opts.logger?.warn("[smqtt] the broker refused this client's own id; connecting under the certificate's name, which another client signed in to this account takes over", e);
24805
+ return await this.connectMqtt(creds);
24806
+ }
24807
+ }
24808
+ /**
24809
+ * Connect one secure-MQTT transport under `clientId`, or under the certificate's own name when it is
24810
+ * omitted, and wire its decode and fan-out. See {@link startMqtt} for which id is used and why.
24811
+ *
24812
+ * The fan-out is wired once the connection stands, so an attempt that is discarded — a client id the
24813
+ * broker refuses, a socket that dies mid-handshake — never reports a connection a consumer never had;
24814
+ * the connect it just completed is announced here instead. Errors raised while connecting are held
24815
+ * only to keep an emitter without an `error` listener from throwing, and are reported once the
24816
+ * transport is one a consumer owns.
24817
+ *
24818
+ * The inbound decode is gated by the reporting device's own capabilities, so one line's decoder never
24819
+ * runs against another's traffic, and the DP frame is unwrapped here — the layer that may import the
24820
+ * transport — so a capability reads tags without owning any framing.
24821
+ */
24822
+ async connectMqtt(creds, clientId) {
24823
+ const transport = new SecureMqtt({ credentials: creds, clientId, logger: this.opts.logger });
24824
+ const whileConnecting = [];
24825
+ const hold = (e) => void whileConnecting.push(e);
24826
+ transport.on("error", hold);
24827
+ await transport.connect();
24828
+ transport.off("error", hold);
24765
24829
  transport.on("connect", () => this.emit("connect"));
24766
24830
  transport.on("disconnect", (r) => this.emit("disconnect", r));
24767
24831
  transport.on("message", (m) => {
@@ -24800,7 +24864,9 @@ var EufyMega = class extends EventEmitter9 {
24800
24864
  this.emitSemantic(out.event, out.payload, { edge: true, refresh: out.refresh });
24801
24865
  });
24802
24866
  transport.on("error", (e) => this.reportError(e));
24803
- await transport.connect();
24867
+ this.emit("connect");
24868
+ for (const e of whileConnecting)
24869
+ this.reportError(e);
24804
24870
  return transport;
24805
24871
  }
24806
24872
  /**
@@ -25257,6 +25323,7 @@ export {
25257
25323
  AUDIO_MEMBERS,
25258
25324
  AccessUnitAssembler,
25259
25325
  AiDetectType,
25326
+ AlarmDelayMode,
25260
25327
  ArmingMode,
25261
25328
  BATTERY_MEMBERS,
25262
25329
  BIZ_CHANNEL,
@@ -25443,7 +25510,6 @@ export {
25443
25510
  freshestLanIp,
25444
25511
  genId,
25445
25512
  generateFid,
25446
- generateMqttUuid,
25447
25513
  getCapabilityModule,
25448
25514
  getIdSuffix,
25449
25515
  getImageBaseCode,
@@ -25456,6 +25522,7 @@ export {
25456
25522
  inferName,
25457
25523
  inspectParams,
25458
25524
  isKnownValueKind,
25525
+ isNotAuthorized,
25459
25526
  isPrivateIpv4,
25460
25527
  isSessionValid,
25461
25528
  isV1Image,
@@ -25471,6 +25538,7 @@ export {
25471
25538
  mergeProperties,
25472
25539
  mqttAppName,
25473
25540
  mqttScopeFor,
25541
+ mqttUuidFrom,
25474
25542
  namespaceForCodec,
25475
25543
  needsRealtimeInit,
25476
25544
  noopLogger,