@mega-yfue/eufy-sdk 0.2.0-beta.3 → 0.2.0-beta.30
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/device-registry.d.ts +14 -28
- package/dist/client/eufy-mega.d.ts +16 -11
- package/dist/client/types.d.ts +6 -2
- package/dist/core/contracts.d.ts +26 -30
- package/dist/core/crypto.d.ts +10 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/logger.d.ts +5 -3
- package/dist/core/solix-types.d.ts +121 -0
- package/dist/core/store.d.ts +38 -10
- package/dist/index.js +2929 -505
- package/dist/index.js.map +4 -4
- package/dist/model/capabilities/access.d.ts +22 -3
- package/dist/model/capabilities/arming.d.ts +33 -27
- package/dist/model/capabilities/battery.d.ts +32 -4
- package/dist/model/capabilities/contact.d.ts +4 -0
- package/dist/model/capabilities/display.d.ts +85 -0
- package/dist/model/capabilities/doorbell.d.ts +24 -14
- package/dist/model/capabilities/index.d.ts +19 -4
- package/dist/model/capabilities/lock.d.ts +15 -12
- package/dist/model/capabilities/ptz.d.ts +6 -2
- package/dist/model/capabilities/solix.d.ts +173 -0
- package/dist/model/capabilities/types.d.ts +69 -12
- package/dist/model/capabilities/vacuum-clean.d.ts +133 -25
- package/dist/model/classify.d.ts +3 -1
- package/dist/model/device-family.d.ts +2 -1
- package/dist/model/device-types.d.ts +1 -0
- package/dist/model/device.d.ts +15 -0
- package/dist/model/index.d.ts +5 -0
- package/dist/model/param-dictionary.d.ts +24 -0
- package/dist/model/param-namespace.d.ts +1 -1
- package/dist/model/solix-catalog.d.ts +25 -0
- package/dist/model/solix-device.d.ts +137 -0
- package/dist/model/solix-family.d.ts +31 -0
- package/dist/model/solix-site.d.ts +70 -0
- package/dist/model/types.d.ts +5 -5
- package/dist/transport/ff09.d.ts +7 -0
- package/dist/transport/http/decodeImageV2.d.ts +8 -14
- package/dist/transport/http/index.d.ts +1 -0
- package/dist/transport/http/jpeg-scan.d.ts +59 -0
- package/dist/transport/http/media-download.d.ts +3 -0
- package/dist/transport/http/mega-client.d.ts +89 -9
- package/dist/transport/http/solix-client.d.ts +270 -0
- package/dist/transport/http/solix-constants.d.ts +56 -0
- package/dist/transport/media-failure.d.ts +48 -0
- package/dist/transport/mqtt/index.d.ts +3 -0
- package/dist/transport/mqtt/secure-mqtt.d.ts +14 -1
- package/dist/transport/mqtt/solix-mqtt.d.ts +360 -0
- package/dist/transport/mqtt/topics.d.ts +30 -0
- package/dist/transport/p2p/command-router.d.ts +131 -19
- package/dist/transport/p2p/live-stream.d.ts +5 -4
- package/dist/transport/p2p/live-trace.d.ts +48 -5
- package/dist/transport/p2p/media.d.ts +11 -0
- package/dist/transport/p2p/p2p-session.d.ts +9 -0
- package/dist/transport/p2p/session-manager.d.ts +57 -23
- package/dist/transport/p2p/shared-live-source.d.ts +10 -1
- package/dist/transport/p2p/station-channels.d.ts +54 -0
- package/dist/transport/stored-image-cache.d.ts +7 -1
- package/package.json +7 -6
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { type SolixCapability, type SolixEnergyMeterReads } from "./capabilities/solix.js";
|
|
2
|
+
import { type SolixProductFamily } from "./solix-family.js";
|
|
3
|
+
import type { SolixDeviceRecord, SolixProductCategory, SolixSiteScene } from "../core/solix-types.js";
|
|
4
|
+
/**
|
|
5
|
+
* The device record shape, re-exported from the model surface. It lives in `core/solix-types` so the
|
|
6
|
+
* transport client can return it without crossing the transport↔model line.
|
|
7
|
+
*/
|
|
8
|
+
export type { SolixDeviceRecord } from "../core/solix-types.js";
|
|
9
|
+
export interface SolixIdentity {
|
|
10
|
+
serial: string;
|
|
11
|
+
productCode: string;
|
|
12
|
+
/** Friendly name — the catalog marketing name if resolvable, else the record's alias/name. */
|
|
13
|
+
name: string;
|
|
14
|
+
/** Anker catalog category (e.g. "Accessory", "Portable Power Station"), if resolvable. */
|
|
15
|
+
category?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Normalized product family — the "what kind of thing is this" answer (power station, Solarbank,
|
|
18
|
+
* smart meter, …), decorrelated from the marketing `category` string. See {@link SolixDevice.family}.
|
|
19
|
+
*/
|
|
20
|
+
family: SolixProductFamily;
|
|
21
|
+
}
|
|
22
|
+
export interface SolixConnectivity {
|
|
23
|
+
online: boolean;
|
|
24
|
+
rssi?: number;
|
|
25
|
+
ssid?: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The `dev.energyMeter()` handle: the members-derived reads ({@link SolixEnergyMeterReads} —
|
|
29
|
+
* `meterVoltageL1` is present only once a frame carrying its tag has landed). Every not-yet-named meter
|
|
30
|
+
* quantity is read from {@link SolixDevice.telemetry} under its `channel_<hex tag>` key instead, which a
|
|
31
|
+
* static members table cannot enumerate.
|
|
32
|
+
*/
|
|
33
|
+
export type SolixEnergyMeter = SolixEnergyMeterReads;
|
|
34
|
+
/** Options for {@link SolixDevice}. */
|
|
35
|
+
export interface SolixDeviceOptions {
|
|
36
|
+
/** Catalog categories (from `SolixClient.getProductCatalog()`) — used to resolve name + category. */
|
|
37
|
+
catalog?: SolixProductCategory[];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* A discovered Solix device with resolved category + capabilities. Feed live telemetry with
|
|
41
|
+
* {@link applyReading} (from {@link SolixMqtt}'s `reading` events) to populate value accessors.
|
|
42
|
+
*/
|
|
43
|
+
export declare class SolixDevice {
|
|
44
|
+
readonly serial: string;
|
|
45
|
+
readonly productCode: string;
|
|
46
|
+
readonly record: SolixDeviceRecord;
|
|
47
|
+
private readonly caps;
|
|
48
|
+
private readonly identity_;
|
|
49
|
+
private values;
|
|
50
|
+
constructor(record: SolixDeviceRecord, opts?: SolixDeviceOptions);
|
|
51
|
+
/**
|
|
52
|
+
* The device's {@link SolixProductFamily} — the classification a caller branches on to SORT devices
|
|
53
|
+
* (a site's power stations vs its meters), the Solix analogue of eufy's `isHomeBase()`. Distinct from
|
|
54
|
+
* {@link has}, which answers what the device can DO; family answers what KIND of device it is.
|
|
55
|
+
*/
|
|
56
|
+
get family(): SolixProductFamily;
|
|
57
|
+
/** All capabilities this device carries. */
|
|
58
|
+
get capabilities(): SolixCapability[];
|
|
59
|
+
/** Whether the device carries a capability — the only correct way to branch on behaviour. */
|
|
60
|
+
has(capability: SolixCapability): boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Merge a live telemetry reading (a `SolixMqtt` `reading` event) so accessors reflect it. Takes the
|
|
63
|
+
* WHOLE reading, not just its values, and drops one addressed to a different device: the documented
|
|
64
|
+
* wiring is `mqtt.on("reading", r => device.applyReading(r))`, and one MQTT stream carries every
|
|
65
|
+
* watched meter on the account — so without this filter two meters would cross-feed each other's floats.
|
|
66
|
+
* A reading with no `deviceSn` (a hand-built one) is accepted as-is.
|
|
67
|
+
*/
|
|
68
|
+
applyReading(reading: {
|
|
69
|
+
deviceSn?: string;
|
|
70
|
+
values: Record<string, number>;
|
|
71
|
+
}): void;
|
|
72
|
+
/** All decoded float telemetry channels from the latest applied reading (raw, `channel_<tag>` keys). */
|
|
73
|
+
telemetry(): Record<string, number>;
|
|
74
|
+
identity(): SolixIdentity;
|
|
75
|
+
firmware(): {
|
|
76
|
+
version: string;
|
|
77
|
+
} | undefined;
|
|
78
|
+
connectivity(): SolixConnectivity | undefined;
|
|
79
|
+
/**
|
|
80
|
+
* The members-derived `energyMeter` reads, or `undefined` when the device has no meter. Each getter is
|
|
81
|
+
* installed only for a tag this device has actually reported, and reads `this.values` LIVE — a handle
|
|
82
|
+
* held across an {@link applyReading} reflects the newer values. Getter INSTALLATION is fixed at the
|
|
83
|
+
* time of this call, so re-call it to pick up a tag first seen since.
|
|
84
|
+
*/
|
|
85
|
+
energyMeter(): SolixEnergyMeter | undefined;
|
|
86
|
+
/**
|
|
87
|
+
* The {@link MemberDeps} the members engine needs: a read closure over the live values store, and the
|
|
88
|
+
* evidence gate (`ctx.paramIds`) rebuilt from the ff09 tags this device has reported. No `codec` — the
|
|
89
|
+
* codecs are eufy transport families and a Solix device belongs to none of them. The sink is a no-op:
|
|
90
|
+
* Solix telemetry is read-only, no member here dispatches a command.
|
|
91
|
+
*/
|
|
92
|
+
private meterDeps;
|
|
93
|
+
/** The ff09 tags this device has reported, derived from the decoder's `channel_<hex>` keys. */
|
|
94
|
+
private seenTags;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The minimum a client must offer to be discovered against — the two wire reads {@link discoverSolixDevices}
|
|
98
|
+
* composes. Typed STRUCTURALLY (not as `SolixClient`) so the model layer never imports the transport
|
|
99
|
+
* client: the hard `transport ⊥ model` rule forbids it, and a structural shape needs no import.
|
|
100
|
+
*/
|
|
101
|
+
export interface SolixDeviceReader {
|
|
102
|
+
getDevices(): Promise<SolixDeviceRecord[]>;
|
|
103
|
+
getProductCatalog(): Promise<SolixProductCategory[]>;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Resolve the product catalog for a discovery: a caller-supplied `opts.catalog` short-circuits the wire
|
|
107
|
+
* read, and a failed `getProductCatalog()` degrades to no categories (names/families just go unresolved,
|
|
108
|
+
* never a thrown discovery). Shared by {@link discoverSolixDevices} and `discoverSolixSites` so that
|
|
109
|
+
* "a failed catalog is non-fatal" decision lives in exactly one place.
|
|
110
|
+
*/
|
|
111
|
+
export declare function resolveSolixCatalog(client: SolixDeviceReader, opts: {
|
|
112
|
+
catalog?: SolixProductCategory[];
|
|
113
|
+
}): Promise<SolixProductCategory[]>;
|
|
114
|
+
/**
|
|
115
|
+
* Discover an account's Solix devices as capability-driven {@link SolixDevice} objects — the wire+model
|
|
116
|
+
* composition (a transport read + the product catalog) that used to be `SolixClient.discoverDevices()`.
|
|
117
|
+
* It lives in the model layer because it builds `SolixDevice`; the wire client (now `transport/http`)
|
|
118
|
+
* cannot, and passing it structurally keeps the layers decorrelated. Feed live telemetry to each result
|
|
119
|
+
* via `SolixDevice.applyReading`.
|
|
120
|
+
*/
|
|
121
|
+
export declare function discoverSolixDevices(client: SolixDeviceReader, opts?: {
|
|
122
|
+
catalog?: SolixProductCategory[];
|
|
123
|
+
}): Promise<SolixDevice[]>;
|
|
124
|
+
/**
|
|
125
|
+
* Reduce a site "scene" snapshot to per-device telemetry readings — the BACKSTOP counterpart to the
|
|
126
|
+
* realtime `ff09` decode. It emits the fields the scene reliably carries that the fast MQTT frame does
|
|
127
|
+
* NOT: `batteryTemperature` (the realtime frame's BMS blob is empty, so `solixReadings` withholds it),
|
|
128
|
+
* `batterySoc` (a cross-check/seed for the `0xa3` SOC), and `expansionPacks` — the count of ATTACHED
|
|
129
|
+
* add-on battery packs (0 on a standalone main unit). Each reading is shaped exactly like a `SolixMqtt`
|
|
130
|
+
* `reading` event — `{ deviceSn, values }` — so a caller can feed it straight into
|
|
131
|
+
* {@link SolixDevice.applyReading} and broadcast it on the same path as a live frame. Entries with no
|
|
132
|
+
* usable value are dropped.
|
|
133
|
+
*/
|
|
134
|
+
export declare function solarbankSceneReadings(scene: SolixSiteScene): {
|
|
135
|
+
deviceSn: string;
|
|
136
|
+
values: Record<string, number>;
|
|
137
|
+
}[];
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A Solix device's product family — the normalized "what kind of thing is this" answer, decorrelated
|
|
3
|
+
* from the Anker catalog's marketing category strings. `unknown` when neither the product code nor the
|
|
4
|
+
* category identifies a family (never a guess).
|
|
5
|
+
*/
|
|
6
|
+
export type SolixProductFamily = "powerStation" | "solarbank" | "smartMeter" | "powerBank" | "cooler" | "evCharger" | "charger" | "unknown";
|
|
7
|
+
/** The pure evidence a family decision reads: the product code, and the catalog category when resolved. */
|
|
8
|
+
export interface SolixFamilyInput {
|
|
9
|
+
/** SKU / model code, e.g. `A1782`, `AE103`, `AE1X0`. */
|
|
10
|
+
product_code: string;
|
|
11
|
+
/** Anker catalog category, when a catalog resolved one (e.g. `"Portable Power Station"`). */
|
|
12
|
+
category?: string;
|
|
13
|
+
}
|
|
14
|
+
/** Grid-tie Solarbank / plug-in home battery family (by product code, catalog-independent). */
|
|
15
|
+
export declare const isSolixSolarbank: (input: SolixFamilyInput) => boolean;
|
|
16
|
+
/** Smart energy meter / grid-CT family (by product code, catalog-independent). */
|
|
17
|
+
export declare const isSolixSmartMeter: (input: SolixFamilyInput) => boolean;
|
|
18
|
+
/** Portable Power Station (SOLIX F-series) family — resolved from the catalog category. */
|
|
19
|
+
export declare const isSolixPowerStation: (input: SolixFamilyInput) => boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Resolve a Solix device's {@link SolixProductFamily} by consulting the family predicates first (they are
|
|
22
|
+
* catalog-independent, so a device classifies even before a catalog is fetched), then the catalog
|
|
23
|
+
* category for the families that have no prefix set yet. Returns `"unknown"` when neither identifies one —
|
|
24
|
+
* never a guess, mirroring `device-family.ts` returning `false` on an unknown device type.
|
|
25
|
+
*
|
|
26
|
+
* The meter is checked before the Solarbank: both can present a battery-ish catalog category, but the
|
|
27
|
+
* meter's `AE1X0` prefix is unambiguous and a meter is never a Solarbank. The Solarbank case reuses
|
|
28
|
+
* {@link isSolixSolarbank} (prefix OR the home-battery category) rather than re-testing the prefix here,
|
|
29
|
+
* so the two never drift.
|
|
30
|
+
*/
|
|
31
|
+
export declare function solixProductFamily(input: SolixFamilyInput): SolixProductFamily;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A capability-driven model for an Anker Solix **site** — the account's home energy system, the "My
|
|
3
|
+
* Home" the app shows. A site is the Solix analogue of a eufy HomeBase in its GROUPING role: it is the
|
|
4
|
+
* system that member devices belong to, so it is what a caller reaches for to ask "what's in this
|
|
5
|
+
* system" and to sort the members by family ({@link SolixSite.powerStations} / {@link solarbanks} /
|
|
6
|
+
* {@link smartMeters}) or by capability ({@link SolixSite.withCapability}).
|
|
7
|
+
*
|
|
8
|
+
* A site is a *grouping*, not a device: it carries no telemetry of its own. Its members are full
|
|
9
|
+
* {@link SolixDevice} models (each with its own capabilities + live telemetry), resolved by
|
|
10
|
+
* {@link discoverSolixSites} from the site record's member list joined to the account's device records.
|
|
11
|
+
* The realtime SYSTEM aggregate the app draws (instantaneous battery SoC + solar + grid flow) is not
|
|
12
|
+
* modelled here: it has no confirmed, stable read surface for the current device generation, and this
|
|
13
|
+
* layer does not fabricate a getter for a value it cannot ground — a caller reads each member device's
|
|
14
|
+
* telemetry instead.
|
|
15
|
+
*/
|
|
16
|
+
import { SolixDevice, type SolixDeviceReader } from "./solix-device.js";
|
|
17
|
+
import type { SolixProductFamily } from "./solix-family.js";
|
|
18
|
+
import type { SolixCapability } from "./capabilities/solix.js";
|
|
19
|
+
import type { SolixProductCategory, SolixSiteRecord } from "../core/solix-types.js";
|
|
20
|
+
/**
|
|
21
|
+
* The minimum a client must offer to discover an account's SITES against — {@link SolixDeviceReader}
|
|
22
|
+
* (the device + catalog reads {@link discoverSolixDevices} already composes) plus the site read.
|
|
23
|
+
* Structural (not `SolixClient`) so the model layer never imports the transport client: the hard
|
|
24
|
+
* `transport ⊥ model` rule forbids it, and a structural shape needs no import.
|
|
25
|
+
*/
|
|
26
|
+
export interface SolixSiteReader extends SolixDeviceReader {
|
|
27
|
+
getSites(): Promise<SolixSiteRecord[]>;
|
|
28
|
+
}
|
|
29
|
+
/** Options for {@link SolixSite} / {@link discoverSolixSites} — the same catalog the device model takes. */
|
|
30
|
+
export interface SolixSiteOptions {
|
|
31
|
+
catalog?: SolixProductCategory[];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A discovered Solix site with its member devices resolved to {@link SolixDevice} models. Build one
|
|
35
|
+
* directly from a record + members, or discover an account's sites with {@link discoverSolixSites}.
|
|
36
|
+
*/
|
|
37
|
+
export declare class SolixSite {
|
|
38
|
+
readonly id: string;
|
|
39
|
+
/** Friendly site name (e.g. "My Home"), or the site id when the record carries none. */
|
|
40
|
+
readonly name: string;
|
|
41
|
+
/** Anker's site-type discriminator (e.g. 20 for a Solarbank-anchored home system), when present. */
|
|
42
|
+
readonly powerSiteType?: number;
|
|
43
|
+
readonly record: SolixSiteRecord;
|
|
44
|
+
readonly devices: SolixDevice[];
|
|
45
|
+
constructor(record: SolixSiteRecord, devices: SolixDevice[]);
|
|
46
|
+
/** The member device with this serial, if it belongs to the site. */
|
|
47
|
+
device(serial: string): SolixDevice | undefined;
|
|
48
|
+
/** Member devices of a given product {@link SolixProductFamily} — the grouping accessor. */
|
|
49
|
+
withFamily(family: SolixProductFamily): SolixDevice[];
|
|
50
|
+
/** Member devices that carry a given capability (e.g. every `battery` in the system). */
|
|
51
|
+
withCapability(capability: SolixCapability): SolixDevice[];
|
|
52
|
+
/** The Solarbank / plug-in home-battery members of the system. */
|
|
53
|
+
solarbanks(): SolixDevice[];
|
|
54
|
+
/** The smart-meter (grid-CT) members of the system. */
|
|
55
|
+
smartMeters(): SolixDevice[];
|
|
56
|
+
/** The portable power-station members of the system. */
|
|
57
|
+
powerStations(): SolixDevice[];
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Discover an account's Solix sites as {@link SolixSite} groupings of capability-driven
|
|
61
|
+
* {@link SolixDevice} members — the site analogue of {@link discoverSolixDevices}. Composes three wire
|
|
62
|
+
* reads (the sites, the account's device records, the product catalog) entirely model-side, so the
|
|
63
|
+
* transport client is passed structurally and the layers stay decorrelated.
|
|
64
|
+
*
|
|
65
|
+
* A site member is resolved to its FULL device record from `getDevices()` when the account lists one
|
|
66
|
+
* (so the member carries firmware/connectivity + telemetry identity); a member the flat device list
|
|
67
|
+
* omits is built from the site entry alone (serial + product code + name), so the system is never
|
|
68
|
+
* missing a device it declares.
|
|
69
|
+
*/
|
|
70
|
+
export declare function discoverSolixSites(client: SolixSiteReader, opts?: SolixSiteOptions): Promise<SolixSite[]>;
|
package/dist/model/types.d.ts
CHANGED
|
@@ -28,10 +28,10 @@
|
|
|
28
28
|
*
|
|
29
29
|
* `display` is the T87Ax Smart Display line — its own codec because its `device_type` collides with
|
|
30
30
|
* the security residual range (confirmed live, 2026-09-04: it connects over secure MQTT with no
|
|
31
|
-
* `p2p_did`, never P2P).
|
|
32
|
-
*
|
|
33
|
-
* targets it
|
|
34
|
-
*
|
|
31
|
+
* `p2p_did`, never P2P). It owns its own param namespace (ids 8001-8006) and its own product line, so
|
|
32
|
+
* no other line's capability can attach to it — see `namespaceForCodec` and `CODEC_LINE`. The `display`
|
|
33
|
+
* capability targets it and reads the charge; no screen/audio/assistant param has been observed, so
|
|
34
|
+
* those are absent rather than deferred.
|
|
35
35
|
*/
|
|
36
36
|
export type Codec = "station" | "camera" | "sensor" | "lock" | "keypad" | "vacuum" | "mower" | "light" | "printer" | "display";
|
|
37
37
|
/**
|
|
@@ -39,7 +39,7 @@ export type Codec = "station" | "camera" | "sensor" | "lock" | "keypad" | "vacuu
|
|
|
39
39
|
* it maps to a {@link CapabilityModule} that owns its property schema. Extend this union as
|
|
40
40
|
* new capabilities are modelled — adding one never requires a subclass.
|
|
41
41
|
*/
|
|
42
|
-
export type Capability = "video" | "snapshot" | "motion" | "person_detection" | "battery" | "light" | "ptz" | "doorbell" | "contact" | "leak" | "smoke" | "co" | "siren" | "lock" | "keypad" | "arming" | "storage" | "rtsp" | "camera" | "audio" | "vacuum_clean" | "vacuum_dock" | "suction" | "locate" | "smart_light" | "info";
|
|
42
|
+
export type Capability = "video" | "snapshot" | "motion" | "person_detection" | "battery" | "light" | "ptz" | "doorbell" | "contact" | "leak" | "smoke" | "co" | "siren" | "lock" | "keypad" | "arming" | "storage" | "rtsp" | "camera" | "audio" | "vacuum_clean" | "vacuum_dock" | "suction" | "locate" | "smart_light" | "display" | "info";
|
|
43
43
|
/** Value type of a property. */
|
|
44
44
|
export type PropertyValueType = "bool" | "number" | "string" | "enum";
|
|
45
45
|
/**
|
package/dist/transport/ff09.d.ts
CHANGED
|
@@ -397,6 +397,13 @@ export interface Ff09SettingsResponse {
|
|
|
397
397
|
* padding past the last real field.
|
|
398
398
|
*/
|
|
399
399
|
export declare function parseFf09SettingsResponse(plain: Buffer): Ff09SettingsResponse;
|
|
400
|
+
/**
|
|
401
|
+
* Walk a bounded `tag|len|value` TLV region into a `tag → bytes` map. Stops at a `0x00` tag (only ever
|
|
402
|
+
* trailing zero-padding, never a real field — real tags start at `0xa1`) and refuses a field whose
|
|
403
|
+
* declared length would overrun `end`, so a corrupt length can't read past the region (e.g. into a
|
|
404
|
+
* trailing checksum). Shared by {@link parseFf09SettingsResponse} and the Solix param decoder.
|
|
405
|
+
*/
|
|
406
|
+
export declare function walkFf09Tlv(buf: Buffer, start: number, end: number): Map<number, Buffer>;
|
|
400
407
|
/** Read a little-endian u16 out of a TLV field buffer (throws on a missing/short field — a caller-side bug, not a wire ambiguity). */
|
|
401
408
|
export declare function readFf09U16LE(field: Buffer | undefined, name: string): number;
|
|
402
409
|
/** Read a single byte out of a TLV field buffer (throws on a missing field — same convention as {@link readFf09U16LE}). */
|
|
@@ -1,19 +1,13 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Per-channel auto-contrast, a byte-exact port of PIL `ImageOps.autocontrast` (cutoff 0.5%). The lost
|
|
3
|
-
* quant tables leave the reconstruction low-contrast ("foggy"); stretching each channel's clipped range
|
|
4
|
-
* to full scale restores a natural-looking image. Mutates `data` (RGBA) in place.
|
|
5
|
-
*
|
|
6
|
-
* Parity notes vs PIL: the cutoff count is `n * cutoff // 100` (integer floor); it is trimmed off each
|
|
7
|
-
* end by zeroing whole histogram bins until the count is spent; the range is then the first/last
|
|
8
|
-
* non-empty bins; and the LUT truncates toward zero (`int()`), NOT rounds — rounding would shift pixels.
|
|
9
|
-
*/
|
|
10
|
-
export declare function autoContrast(data: Uint8Array, width: number, height: number, cutoff?: number): void;
|
|
11
1
|
/** True if the blob is a v2 `v2_eufysecurity:` push thumbnail. */
|
|
12
2
|
export declare function isV2Image(data: Buffer): boolean;
|
|
13
3
|
/**
|
|
14
|
-
* Decode a v2 blob to a plain JPEG buffer by reconstructing its header, or null if it isn't v2
|
|
15
|
-
* plaintext scan can't be located
|
|
16
|
-
*
|
|
17
|
-
*
|
|
4
|
+
* Decode a v2 blob to a plain JPEG buffer by reconstructing its header, or null if it isn't v2, the
|
|
5
|
+
* plaintext scan can't be located, or no frame shape explains it.
|
|
6
|
+
*
|
|
7
|
+
* Three steps and no pixel rewrite: the frame shape is read out of the entropy scan,
|
|
8
|
+
* one probe decode both proves the spliced JPEG decodes and measures how far the substitute quant
|
|
9
|
+
* tables fall short of the camera's, and the answer is the same tail under a header carrying the
|
|
10
|
+
* corrected tables. See the module doc for the keyless-splice rationale and for why neither the search
|
|
11
|
+
* nor the correction decodes candidate frames or re-encodes the picture.
|
|
18
12
|
*/
|
|
19
13
|
export declare function decodeImageV2(data: Buffer): Buffer | null;
|
|
@@ -3,3 +3,4 @@ export { randomPhoneModel, randomUserAgent } from "./phone-model.js";
|
|
|
3
3
|
export * from "./decodeImageV1.js";
|
|
4
4
|
export * from "./decodeImageV2.js";
|
|
5
5
|
export { listLightEffects, listAiSceneRecommendations, type LightEffectSummary } from "./light-catalog.js";
|
|
6
|
+
export { SolixClient, type SolixClientOptions, type SolixLoginResult, type SolixSession, type SolixPersisted, type SolixSessionStore, } from "./solix-client.js";
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A baseline-JPEG entropy scanner that decodes no pixels.
|
|
3
|
+
*
|
|
4
|
+
* The v2 thumbnail decoder next door has to discover a frame geometry its blob does not state, and the
|
|
5
|
+
* only evidence is the plaintext entropy-coded scan: how many MCUs it carries, and whether it carries
|
|
6
|
+
* them under a given chroma subsampling. A JPEG decoder answers that — it throws on a frame the scan
|
|
7
|
+
* does not fill — and charges a full set of component and output buffers for each question.
|
|
8
|
+
*
|
|
9
|
+
* That bill is fatal for a caller under a hard memory cap. Each `jpeg-js` decode churns roughly a
|
|
10
|
+
* megabyte of typed arrays, glibc keeps the arenas rather than handing them back, and a search asking
|
|
11
|
+
* the question of every candidate geometry costs tens of megabytes per thumbnail — permanently, and
|
|
12
|
+
* more than an embedded host gives an app in total.
|
|
13
|
+
*
|
|
14
|
+
* This module asks the same question by walking the Huffman-coded coefficients and throwing them away:
|
|
15
|
+
* no IDCT, no component planes, no output image. What it keeps is one number per MCU — the DC
|
|
16
|
+
* coefficient of its luma block(s), i.e. that block's average brightness — which is all the geometry
|
|
17
|
+
* search needs to tell a sheared row apart from a continuous one. Allocation is a single `Int32Array`
|
|
18
|
+
* of MCU count, and the walk is linear in the scan's length.
|
|
19
|
+
*
|
|
20
|
+
* Baseline sequential only (SOF0), which is what the v2 tail is: no progressive refinement, no
|
|
21
|
+
* arithmetic coding. Restart markers are tolerated — the scan resynchronises and resets its DC
|
|
22
|
+
* predictors, exactly as a decoder would.
|
|
23
|
+
*
|
|
24
|
+
* @module transport/http/jpeg-scan
|
|
25
|
+
*/
|
|
26
|
+
/** What the scan carried, under one subsampling hypothesis. */
|
|
27
|
+
export interface EntropyScan {
|
|
28
|
+
/** Complete MCUs decoded before the data ran out. */
|
|
29
|
+
mcus: number;
|
|
30
|
+
/**
|
|
31
|
+
* Mean luma DC per MCU, in scan order — a thumbnail of the picture at MCU resolution.
|
|
32
|
+
*
|
|
33
|
+
* Quantized units (the quant tables are lost with the v2 head), so the values are a scale of their
|
|
34
|
+
* own. Differences between neighbours are what the geometry search reads, and those survive.
|
|
35
|
+
*/
|
|
36
|
+
luma: Int32Array;
|
|
37
|
+
/**
|
|
38
|
+
* Whether the scan ended where a whole MCU ended, with nothing but the EOI marker left.
|
|
39
|
+
*
|
|
40
|
+
* The discriminator between hypotheses: a wrong one reads a block with the wrong Huffman table,
|
|
41
|
+
* diverges, and either dies mid-MCU or stops with data still ahead of it. Only the subsampling the
|
|
42
|
+
* encoder used walks the scan to its last byte on an MCU boundary.
|
|
43
|
+
*/
|
|
44
|
+
complete: boolean;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Walk the plaintext tail's scan under one subsampling hypothesis.
|
|
48
|
+
*
|
|
49
|
+
* `extraTables` carries the DHT segments that are NOT in the tail — for a v2 thumbnail, the standard
|
|
50
|
+
* luma tables the reconstructed header supplies, since only the chroma ones survive in plaintext.
|
|
51
|
+
* Returns null when the tail is not a baseline scan at all (no SOS, missing tables).
|
|
52
|
+
*
|
|
53
|
+
* The walk stops at the first byte it cannot read as this hypothesis's next block, and the result is
|
|
54
|
+
* `complete` only when that happened on an MCU boundary with nothing but a marker left. The DC array
|
|
55
|
+
* grows as the scan turns out to be long rather than being sized from the scan's byte length: a wrong
|
|
56
|
+
* hypothesis dies after a handful of MCUs, and provisioning three whole-scan arrays to discover that is
|
|
57
|
+
* the kind of allocation this module exists to avoid.
|
|
58
|
+
*/
|
|
59
|
+
export declare function scanEntropy(tail: Uint8Array, subsampling: number, extraTables: readonly Uint8Array[]): EntropyScan | null;
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import type { MediaFailureReason } from "../media-failure.js";
|
|
1
2
|
type ResolveHost = (hostname: string) => Promise<readonly {
|
|
2
3
|
address: string;
|
|
3
4
|
family: number;
|
|
4
5
|
}[]>;
|
|
6
|
+
/** Tag a failure OUTSIDE the transfer itself — the push-image decoder refusing bytes that did arrive. @internal */
|
|
7
|
+
export declare function mediaFailureError(message: string, reason: MediaFailureReason, cause?: unknown): Error;
|
|
5
8
|
/** Signals rejection of the active Eufy session without exposing response content. @internal */
|
|
6
9
|
export declare class MediaDownloadAuthenticationError extends Error {
|
|
7
10
|
}
|
|
@@ -32,6 +32,14 @@ export interface MegaClientConfig {
|
|
|
32
32
|
* an explicit value pins a fixed one. Not the account identity — that's `phoneModel`.
|
|
33
33
|
*/
|
|
34
34
|
mediaUserAgent?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Acting name written into the commands that carry an actor field — guard mode and HomeBase alarm
|
|
37
|
+
* output (`user_name`), a lock's acting username. Trimmed, and blank counts as unset: the default
|
|
38
|
+
* is the login email's local-part (the whole string when it has no `@`). Attribution only — the
|
|
39
|
+
* device stores it for its own activity record, and no captured frame shows it being validated
|
|
40
|
+
* against the account.
|
|
41
|
+
*/
|
|
42
|
+
accountName?: string;
|
|
35
43
|
/** Persist + reuse the session (token + session key) across runs. Default: in-memory. */
|
|
36
44
|
store?: SessionStore;
|
|
37
45
|
/** Diagnostics sink. Omit for silence; pass a `Logger` (or `new ConsoleLogger()`) to see logs. */
|
|
@@ -63,9 +71,34 @@ export declare class MegaApiError extends Error {
|
|
|
63
71
|
* device-list params carry the same `{param_type, param_value, update_time}` and are not owner-gated.
|
|
64
72
|
*/
|
|
65
73
|
export declare const OWNER_ONLY_CODE = 20004;
|
|
66
|
-
/**
|
|
74
|
+
/**
|
|
75
|
+
* Thrown when a persisted/expired session is rejected (401). Re-login to recover.
|
|
76
|
+
*
|
|
77
|
+
* It carries the rate the client has already worked out for replacing a rejected token, because the
|
|
78
|
+
* rejection is where that rate stops being the client's alone: a login driven from here spends the same
|
|
79
|
+
* session the client's own recovery would have, and repeated logins are what makes an account start
|
|
80
|
+
* demanding captchas. {@link retryAfterMs} is how long the next replacement is barred for, and
|
|
81
|
+
* {@link contended} whether this rejection landed inside that bar — the shape repeated displacement has.
|
|
82
|
+
*/
|
|
67
83
|
export declare class SessionExpiredError extends Error {
|
|
68
|
-
|
|
84
|
+
/**
|
|
85
|
+
* How long the next session replacement is barred for, in milliseconds; `0` when nothing bars one now.
|
|
86
|
+
*
|
|
87
|
+
* The remainder of the client's own hold-off, which doubles per consecutive replacement and is capped —
|
|
88
|
+
* and which every replacement extends, whether the client spent it or a login made on this error did.
|
|
89
|
+
*/
|
|
90
|
+
readonly retryAfterMs: number;
|
|
91
|
+
/**
|
|
92
|
+
* Whether this rejection landed inside that bar — a token replaced recently and rejected again since.
|
|
93
|
+
*
|
|
94
|
+
* It says the session is being DISPLACED rather than expiring: something else is signing in on this
|
|
95
|
+
* account, and replacing the token again only trades one login for another.
|
|
96
|
+
*/
|
|
97
|
+
readonly contended: boolean;
|
|
98
|
+
constructor(message: string, opts?: {
|
|
99
|
+
retryAfterMs?: number;
|
|
100
|
+
contended?: boolean;
|
|
101
|
+
});
|
|
69
102
|
}
|
|
70
103
|
/**
|
|
71
104
|
* eufy cloud gateway error codes — the numeric `code` carried in a response envelope alongside the
|
|
@@ -179,6 +212,13 @@ export declare class MegaHttpClient {
|
|
|
179
212
|
private sessionKey?;
|
|
180
213
|
/** Per-host ECDH session keys for non-mega gateways (e.g. eufylife) keyed by host. */
|
|
181
214
|
private readonly sessionKeys;
|
|
215
|
+
/**
|
|
216
|
+
* The held credential. `userId` is the login reply's `ap_cloud_user_id` where it has one — the Anker
|
|
217
|
+
* Passport cloud's id — while `accountUserId` is the eufy account's own `user_id`.
|
|
218
|
+
*
|
|
219
|
+
* The `gtoken` header is hashed from `accountUserId`: that is the id the gateway recomputes the header
|
|
220
|
+
* from, rejecting a disagreement with `"gtoken not equal userid error"`.
|
|
221
|
+
*/
|
|
182
222
|
private auth_?;
|
|
183
223
|
/** captcha_id of an in-flight challenge, held between login() and solveCaptcha(). */
|
|
184
224
|
private pendingCaptchaId?;
|
|
@@ -208,9 +248,11 @@ export declare class MegaHttpClient {
|
|
|
208
248
|
private loggingIn;
|
|
209
249
|
/** The one in-flight re-login every call rejected on the same dead token waits on. */
|
|
210
250
|
private reauthAttempt?;
|
|
211
|
-
/** Replacements since the held session last proved stable, and when the last one ran — see {@link
|
|
251
|
+
/** Replacements since the held session last proved stable, and when the last one ran — see {@link holdOffRemainingMs}. */
|
|
212
252
|
private recoveries;
|
|
213
253
|
private lastRecoveryAt;
|
|
254
|
+
/** A token of ours has been rejected and not yet replaced — see {@link noteTokenReplacement}. */
|
|
255
|
+
private rejectedTokenPending;
|
|
214
256
|
constructor(cfg: MegaClientConfig);
|
|
215
257
|
/**
|
|
216
258
|
* Install the session the store holds, if it holds a usable one: the token + its bound ECDH key, skipping
|
|
@@ -231,10 +273,15 @@ export declare class MegaHttpClient {
|
|
|
231
273
|
/** The active region shard (e.g. `"eu-pr"`, `"us-pr"`), set after {@link login} or a region override. */
|
|
232
274
|
get regionShard(): RegionShard;
|
|
233
275
|
/**
|
|
234
|
-
* The
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
* if it has no `@`.
|
|
276
|
+
* The name commands attribute themselves to — {@link MegaClientConfig.accountName} when the config
|
|
277
|
+
* pins one (trimmed; blank counts as unset), otherwise the logged-in account's display name, which
|
|
278
|
+
* is the login email's local-part (e.g. `someone+tag` for `someone+tag@example.com`) and falls back
|
|
279
|
+
* to the whole email if it has no `@`.
|
|
280
|
+
*
|
|
281
|
+
* The local-part is the string the app writes into the ff09 command's acting "username" field
|
|
282
|
+
* (verified against a captured T8531 unlock frame), so it is the faithful default. An override is a
|
|
283
|
+
* different LABEL for the same account, not a different identity: the session authenticates on the
|
|
284
|
+
* token and the device record's member ids, neither of which this touches.
|
|
238
285
|
*/
|
|
239
286
|
get accountName(): string;
|
|
240
287
|
/**
|
|
@@ -246,7 +293,14 @@ export declare class MegaHttpClient {
|
|
|
246
293
|
*/
|
|
247
294
|
private baseHeaders;
|
|
248
295
|
/**
|
|
249
|
-
* The
|
|
296
|
+
* The id `gtoken` is hashed from — the account's own `user_id`, which is what the gateway recomputes the
|
|
297
|
+
* header from. One place so the two header paths cannot drift on which of the session's ids that is.
|
|
298
|
+
*
|
|
299
|
+
* Call only where `auth_` is already established; every header path guards it.
|
|
300
|
+
*/
|
|
301
|
+
private gtokenUserId;
|
|
302
|
+
/**
|
|
303
|
+
* The account-credential headers every authed call carries — `x-auth-token` + `gtoken`.
|
|
250
304
|
* One place so the signed path, the key-exchange and the bearer path can't drift on what "authed" means.
|
|
251
305
|
*/
|
|
252
306
|
private authTokenHeaders;
|
|
@@ -353,7 +407,13 @@ export declare class MegaHttpClient {
|
|
|
353
407
|
registerPushToken(token: string): Promise<void>;
|
|
354
408
|
/** Download raw bytes from a push-media URL using the active account session. */
|
|
355
409
|
downloadMedia(url: string): Promise<Buffer>;
|
|
356
|
-
/**
|
|
410
|
+
/**
|
|
411
|
+
* Download push image bytes and decrypt a recognized v1 wrapper when its device key input is available.
|
|
412
|
+
*
|
|
413
|
+
* A decoder throw is tagged `decode-failed`: to anything downstream, the difference between "the
|
|
414
|
+
* bytes never arrived" and "the bytes arrived and the wrapper would not decrypt" is the difference
|
|
415
|
+
* between a network problem and a key problem, and one of them is this SDK's to fix.
|
|
416
|
+
*/
|
|
357
417
|
downloadImage(url: string, p2pDid?: string): Promise<Buffer>;
|
|
358
418
|
/** The security-app data host for this region (face recognition, media, etc.). */
|
|
359
419
|
private securityAppHost;
|
|
@@ -467,6 +527,26 @@ export declare class MegaHttpClient {
|
|
|
467
527
|
* and a caller is told the honest reason instead of being served a fight.
|
|
468
528
|
*/
|
|
469
529
|
private recoveryDue;
|
|
530
|
+
/**
|
|
531
|
+
* How much longer a token replacement must wait, in milliseconds; `0` when one may run now.
|
|
532
|
+
*
|
|
533
|
+
* The wait doubles per consecutive replacement and is capped, and it is what {@link recoveryDue} gates
|
|
534
|
+
* this client's own recovery on — and what {@link SessionExpiredError.retryAfterMs} hands a host that
|
|
535
|
+
* drives its own. One function so the two cannot disagree about the rate, which they would have to for
|
|
536
|
+
* a host to be told it may retry while this client is still holding off.
|
|
537
|
+
*/
|
|
538
|
+
private holdOffRemainingMs;
|
|
539
|
+
/**
|
|
540
|
+
* Count one token replacement against the hold-off, and clear the rejection it answered.
|
|
541
|
+
*
|
|
542
|
+
* Every replacement passes through here, wherever it was spent from: {@link recoverRejectedSession}, and
|
|
543
|
+
* a {@link login} that follows a rejection this client surfaced. A hold-off that counted only its own
|
|
544
|
+
* would be no bound at all — the wait would sit at its first value however many sessions had been spent,
|
|
545
|
+
* and {@link SessionExpiredError.retryAfterMs} would report a minute while logins ran every few seconds.
|
|
546
|
+
* Which of the two counted a given replacement is the flag: the recovery path clears it before logging
|
|
547
|
+
* in, so the login cannot count the same one again.
|
|
548
|
+
*/
|
|
549
|
+
private noteTokenReplacement;
|
|
470
550
|
/**
|
|
471
551
|
* Note that the held session is working. A replacement that keeps serving calls for long enough is not
|
|
472
552
|
* contention, so the hold-off is forgotten and the next genuine expiry recovers immediately.
|