@mega-yfue/eufy-sdk 0.2.0-beta.14 → 0.2.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.
@@ -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 or the
15
- * plaintext scan can't be located. The search first chooses subsampling and coarse geometry, derives
16
- * the fixed MCU count, refines width by row shear, and pins the exact fill height before applying
17
- * auto-contrast and re-encoding. See the module doc for the keyless-splice rationale.
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;
@@ -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
  }
@@ -353,7 +353,13 @@ export declare class MegaHttpClient {
353
353
  registerPushToken(token: string): Promise<void>;
354
354
  /** Download raw bytes from a push-media URL using the active account session. */
355
355
  downloadMedia(url: string): Promise<Buffer>;
356
- /** Download push image bytes and decrypt a recognized v1 wrapper when its device key input is available. */
356
+ /**
357
+ * Download push image bytes and decrypt a recognized v1 wrapper when its device key input is available.
358
+ *
359
+ * A decoder throw is tagged `decode-failed`: to anything downstream, the difference between "the
360
+ * bytes never arrived" and "the bytes arrived and the wrapper would not decrypt" is the difference
361
+ * between a network problem and a key problem, and one of them is this SDK's to fix.
362
+ */
357
363
  downloadImage(url: string, p2pDid?: string): Promise<Buffer>;
358
364
  /** The security-app data host for this region (face recognition, media, etc.). */
359
365
  private securityAppHost;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Why acquiring a push thumbnail produced no bytes.
3
+ *
4
+ * Transport vocabulary, and it stays inside `transport/`: the modules that raise it and the cache that
5
+ * reads it are both here, and neither the device model nor the client names it. Nothing crosses the
6
+ * capability boundary, so nothing belongs in the shared floor.
7
+ *
8
+ * @module transport/media-failure
9
+ */
10
+ /**
11
+ * The terms an acquisition failure is reported in — a CLOSED vocabulary, and that is the point.
12
+ *
13
+ * `download-failed` says a candidate did not become an image; it does not say whether the URL was
14
+ * refused before a packet moved, the host answered 404, the transfer timed out, or the bytes arrived
15
+ * and would not decode. Those need entirely different fixes, so the distinction has to survive to
16
+ * whatever is reading the log.
17
+ *
18
+ * It is a fixed vocabulary rather than an error message because the thing that fails holds a signed
19
+ * media URL and a response body, and neither may reach a log line. Every member here is a term this
20
+ * file defines; a downloader's own wording never is.
21
+ *
22
+ * - `url-not-allowed` — the candidate URL failed the media allowlist (scheme, credentials, port, a
23
+ * literal address, or a host the SDK does not download from). Nothing was requested.
24
+ * - `address-not-public` — the host resolved to nothing, or to an address that is not public.
25
+ * - `redirect-not-allowed` — the media host redirected somewhere the object-store allowlist refuses,
26
+ * redirected without a target, or redirected twice.
27
+ * - `http-status` — the host answered, with a status other than 200. Carried alongside as `status`.
28
+ * - `too-large` — the body exceeded the download bound, declared or observed.
29
+ * - `timeout` — the whole attempt, DNS included, outlived its window.
30
+ * - `network` — the request itself failed: connect, TLS, a reset mid-body.
31
+ * - `decode-failed` — the bytes arrived and the push-image decoder refused them. A property of the
32
+ * image, not of the network.
33
+ */
34
+ export type MediaFailureReason = "url-not-allowed" | "address-not-public" | "redirect-not-allowed" | "http-status" | "too-large" | "timeout" | "network" | "decode-failed";
35
+ /** Every {@link MediaFailureReason}, so a tag that crossed an injected boundary can be checked against it. */
36
+ export declare const MEDIA_FAILURE_REASONS: readonly MediaFailureReason[];
37
+ /**
38
+ * The tag a media-acquisition failure carries for diagnostics.
39
+ *
40
+ * A property rather than a base class: the cache that logs it takes its downloader by injection and
41
+ * must not import the transport that throws — so it reads this shape off an unknown error and checks
42
+ * the term against {@link MEDIA_FAILURE_REASONS} before it goes anywhere near a log.
43
+ */
44
+ export interface MediaFailure {
45
+ readonly mediaFailure: MediaFailureReason;
46
+ /** The HTTP status, when {@link mediaFailure} is `http-status`. */
47
+ readonly status?: number;
48
+ }
@@ -10,7 +10,13 @@ export declare class StoredImageCache {
10
10
  private activeDownloads;
11
11
  private generation;
12
12
  constructor(downloader: (url: string, deviceKey: string) => Promise<Buffer>, logger: Logger, clock?: () => number, isLifecycleError?: (error: unknown) => boolean);
13
- /** Observe a normalized thumbnail URL and start acquisition eagerly. */
13
+ /**
14
+ * Observe a normalized thumbnail URL and start acquisition eagerly.
15
+ *
16
+ * A URL already inside this device's window of recent attempts is ignored, so one event arriving as
17
+ * several pushes downloads one thumbnail. The window is a `Set`, which iterates in insertion order,
18
+ * so the entry evicted once it is full is the oldest attempt.
19
+ */
14
20
  observe(deviceKey: string, url: string): void;
15
21
  /** Return retained bytes without starting or awaiting network work. */
16
22
  snapshotStored(deviceKey: string): Promise<Buffer>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mega-yfue/eufy-sdk",
3
- "version": "0.2.0-beta.14",
3
+ "version": "0.2.0-beta.15",
4
4
  "description": "One typed TypeScript client for the Anker eufy v6 cloud — capability-driven devices, realtime events over P2P/MQTT/push, and live media",
5
5
  "license": "Apache-2.0",
6
6
  "author": "mega-yfue",