@mega-yfue/eufy-sdk 0.2.0-beta.14 → 0.2.0-beta.16
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/core/store.d.ts +18 -1
- package/dist/index.js +449 -193
- package/dist/index.js.map +4 -4
- package/dist/transport/http/decodeImageV2.d.ts +8 -14
- 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 +22 -2
- package/dist/transport/http/solix-client.d.ts +5 -0
- package/dist/transport/media-failure.d.ts +48 -0
- package/dist/transport/stored-image-cache.d.ts +7 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -153,7 +153,7 @@ function tokenNotExpired(tokenExpiresAt, skewSec = 300) {
|
|
|
153
153
|
return true;
|
|
154
154
|
}
|
|
155
155
|
function isSessionValid(s, skewSec = 300) {
|
|
156
|
-
if (!s?.authToken || !s.shareKey || !s.keyIdent)
|
|
156
|
+
if (!s?.authToken || !s.shareKey || !s.keyIdent || !s.accountUserId)
|
|
157
157
|
return false;
|
|
158
158
|
return tokenNotExpired(s.tokenExpiresAt, skewSec);
|
|
159
159
|
}
|
|
@@ -217,18 +217,33 @@ BLOCKED_ADDRESSES.addAddress("::1", "ipv6");
|
|
|
217
217
|
BLOCKED_ADDRESSES.addSubnet("fc00::", 7, "ipv6");
|
|
218
218
|
BLOCKED_ADDRESSES.addSubnet("fe80::", 10, "ipv6");
|
|
219
219
|
var MediaDownloadError = class extends Error {
|
|
220
|
+
mediaFailure;
|
|
221
|
+
status;
|
|
222
|
+
constructor(message, mediaFailure, status) {
|
|
223
|
+
super(message);
|
|
224
|
+
this.name = "MediaDownloadError";
|
|
225
|
+
this.mediaFailure = mediaFailure;
|
|
226
|
+
if (status !== void 0)
|
|
227
|
+
this.status = status;
|
|
228
|
+
}
|
|
220
229
|
};
|
|
230
|
+
function mediaFailureError(message, reason, cause) {
|
|
231
|
+
const error = new MediaDownloadError(message, reason);
|
|
232
|
+
if (cause !== void 0)
|
|
233
|
+
error.cause = cause;
|
|
234
|
+
return error;
|
|
235
|
+
}
|
|
221
236
|
var MediaDownloadAuthenticationError = class extends Error {
|
|
222
237
|
};
|
|
223
|
-
function allowedMediaUrl(value, hostPattern) {
|
|
238
|
+
function allowedMediaUrl(value, hostPattern, reason) {
|
|
224
239
|
let url;
|
|
225
240
|
try {
|
|
226
241
|
url = new URL(value);
|
|
227
242
|
} catch {
|
|
228
|
-
throw new MediaDownloadError("Media download rejected");
|
|
243
|
+
throw new MediaDownloadError("Media download rejected", reason);
|
|
229
244
|
}
|
|
230
245
|
if (url.protocol !== "https:" || url.username !== "" || url.password !== "" || url.port !== "" || isIP(url.hostname) !== 0 || !hostPattern.test(url.hostname)) {
|
|
231
|
-
throw new MediaDownloadError("Media download rejected");
|
|
246
|
+
throw new MediaDownloadError("Media download rejected", reason);
|
|
232
247
|
}
|
|
233
248
|
return url;
|
|
234
249
|
}
|
|
@@ -243,7 +258,7 @@ async function assertPublicResolution(url, resolveHost2, signal) {
|
|
|
243
258
|
try {
|
|
244
259
|
addresses = await Promise.race([resolveHost2(url.hostname), aborted]);
|
|
245
260
|
} catch {
|
|
246
|
-
throw new MediaDownloadError("Media download rejected");
|
|
261
|
+
throw new MediaDownloadError("Media download rejected", "address-not-public");
|
|
247
262
|
} finally {
|
|
248
263
|
signal.removeEventListener("abort", onAbort);
|
|
249
264
|
}
|
|
@@ -251,14 +266,14 @@ async function assertPublicResolution(url, resolveHost2, signal) {
|
|
|
251
266
|
const type = family === 6 ? "ipv6" : family === 4 ? "ipv4" : void 0;
|
|
252
267
|
return type === void 0 || BLOCKED_ADDRESSES.check(address, type);
|
|
253
268
|
})) {
|
|
254
|
-
throw new MediaDownloadError("Media download rejected");
|
|
269
|
+
throw new MediaDownloadError("Media download rejected", "address-not-public");
|
|
255
270
|
}
|
|
256
271
|
}
|
|
257
272
|
var resolveHost = (hostname) => lookup(hostname, { all: true, verbatim: true });
|
|
258
273
|
async function readMediaBody(response) {
|
|
259
274
|
const contentLength = response.headers.get("content-length");
|
|
260
275
|
if (contentLength && /^\d+$/.test(contentLength) && BigInt(contentLength) > BigInt(MEDIA_DOWNLOAD_MAX_BYTES)) {
|
|
261
|
-
throw new MediaDownloadError("Media download rejected");
|
|
276
|
+
throw new MediaDownloadError("Media download rejected", "too-large");
|
|
262
277
|
}
|
|
263
278
|
if (!response.body)
|
|
264
279
|
return Buffer.alloc(0);
|
|
@@ -272,14 +287,14 @@ async function readMediaBody(response) {
|
|
|
272
287
|
bytes2 += value.byteLength;
|
|
273
288
|
if (bytes2 > MEDIA_DOWNLOAD_MAX_BYTES) {
|
|
274
289
|
void reader2.cancel().catch(() => void 0);
|
|
275
|
-
throw new MediaDownloadError("Media download rejected");
|
|
290
|
+
throw new MediaDownloadError("Media download rejected", "too-large");
|
|
276
291
|
}
|
|
277
292
|
chunks.push(value);
|
|
278
293
|
}
|
|
279
294
|
return Buffer.concat(chunks, bytes2);
|
|
280
295
|
}
|
|
281
296
|
async function downloadMediaResource(url, authenticatedHeaders, fetchImpl = fetch, resolver = resolveHost) {
|
|
282
|
-
const original = allowedMediaUrl(url, EUFY_MEDIA_HOST);
|
|
297
|
+
const original = allowedMediaUrl(url, EUFY_MEDIA_HOST, "url-not-allowed");
|
|
283
298
|
const controller = new AbortController();
|
|
284
299
|
const timeout = setTimeout(() => controller.abort(), MEDIA_DOWNLOAD_TIMEOUT_MS);
|
|
285
300
|
try {
|
|
@@ -292,31 +307,33 @@ async function downloadMediaResource(url, authenticatedHeaders, fetchImpl = fetc
|
|
|
292
307
|
if (originalResponse.status >= 300 && originalResponse.status < 400) {
|
|
293
308
|
const location = originalResponse.headers.get("location");
|
|
294
309
|
if (!location)
|
|
295
|
-
throw new MediaDownloadError("Media download rejected");
|
|
296
|
-
const target = allowedMediaUrl(location, EUFY_OBJECT_HOST);
|
|
310
|
+
throw new MediaDownloadError("Media download rejected", "redirect-not-allowed");
|
|
311
|
+
const target = allowedMediaUrl(location, EUFY_OBJECT_HOST, "redirect-not-allowed");
|
|
297
312
|
await assertPublicResolution(target, resolver, controller.signal);
|
|
298
313
|
const redirectedResponse = await fetchImpl(target, { redirect: "manual", signal: controller.signal });
|
|
299
314
|
if (redirectedResponse.status >= 300 && redirectedResponse.status < 400) {
|
|
300
|
-
throw new MediaDownloadError("Media download rejected");
|
|
315
|
+
throw new MediaDownloadError("Media download rejected", "redirect-not-allowed");
|
|
316
|
+
}
|
|
317
|
+
if (redirectedResponse.status !== 200) {
|
|
318
|
+
throw new MediaDownloadError("Media download failed", "http-status", redirectedResponse.status);
|
|
301
319
|
}
|
|
302
|
-
if (redirectedResponse.status !== 200)
|
|
303
|
-
throw new MediaDownloadError("Media download failed");
|
|
304
320
|
return await readMediaBody(redirectedResponse);
|
|
305
321
|
}
|
|
306
322
|
if (originalResponse.status === 401 || originalResponse.status === 403) {
|
|
307
323
|
throw new MediaDownloadAuthenticationError("Media authentication failed");
|
|
308
324
|
}
|
|
309
|
-
if (originalResponse.status !== 200)
|
|
310
|
-
throw new MediaDownloadError("Media download failed");
|
|
325
|
+
if (originalResponse.status !== 200) {
|
|
326
|
+
throw new MediaDownloadError("Media download failed", "http-status", originalResponse.status);
|
|
327
|
+
}
|
|
311
328
|
return await readMediaBody(originalResponse);
|
|
312
329
|
} catch (error) {
|
|
313
330
|
if (controller.signal.aborted)
|
|
314
|
-
throw new MediaDownloadError("Media download timed out");
|
|
331
|
+
throw new MediaDownloadError("Media download timed out", "timeout");
|
|
315
332
|
if (error instanceof MediaDownloadAuthenticationError)
|
|
316
333
|
throw error;
|
|
317
334
|
if (error instanceof MediaDownloadError)
|
|
318
335
|
throw error;
|
|
319
|
-
throw new MediaDownloadError("Media download failed");
|
|
336
|
+
throw new MediaDownloadError("Media download failed", "network");
|
|
320
337
|
} finally {
|
|
321
338
|
clearTimeout(timeout);
|
|
322
339
|
}
|
|
@@ -375,8 +392,228 @@ function randomUserAgent(seed, model) {
|
|
|
375
392
|
// dist/transport/http/decodeImageV1.js
|
|
376
393
|
import { createDecipheriv as createDecipheriv2, createHash as createHash2 } from "node:crypto";
|
|
377
394
|
|
|
395
|
+
// dist/transport/http/jpeg-scan.js
|
|
396
|
+
var DHT = 196;
|
|
397
|
+
var SOS = 218;
|
|
398
|
+
var DRI = 221;
|
|
399
|
+
var RST_FIRST = 208;
|
|
400
|
+
var RST_LAST = 215;
|
|
401
|
+
var LUMA_SAMPLING = { 0: [1, 1], 1: [2, 1], 2: [2, 2] };
|
|
402
|
+
var ScanEnd = class extends Error {
|
|
403
|
+
};
|
|
404
|
+
function readHuffmanTables(segment, into) {
|
|
405
|
+
const end = 2 + (segment[2] << 8 | segment[3]);
|
|
406
|
+
let at = 4;
|
|
407
|
+
while (at < end) {
|
|
408
|
+
const id = segment[at];
|
|
409
|
+
const counts = segment.subarray(at + 1, at + 17);
|
|
410
|
+
let total = 0;
|
|
411
|
+
for (const count2 of counts)
|
|
412
|
+
total += count2;
|
|
413
|
+
const values = segment.slice(at + 17, at + 17 + total);
|
|
414
|
+
const minCode = new Int32Array(17);
|
|
415
|
+
const maxCode = new Int32Array(17).fill(-1);
|
|
416
|
+
const valPtr = new Int32Array(17);
|
|
417
|
+
let code = 0;
|
|
418
|
+
let index = 0;
|
|
419
|
+
for (let length = 1; length <= 16; length++) {
|
|
420
|
+
const count2 = counts[length - 1];
|
|
421
|
+
if (count2 > 0) {
|
|
422
|
+
valPtr[length] = index;
|
|
423
|
+
minCode[length] = code;
|
|
424
|
+
code += count2;
|
|
425
|
+
index += count2;
|
|
426
|
+
maxCode[length] = code - 1;
|
|
427
|
+
}
|
|
428
|
+
code <<= 1;
|
|
429
|
+
}
|
|
430
|
+
into.set(id, { minCode, maxCode, valPtr, values });
|
|
431
|
+
at += 17 + total;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
var BitReader = class {
|
|
435
|
+
/** Byte offset of the next byte to read. */
|
|
436
|
+
position;
|
|
437
|
+
bits = 0;
|
|
438
|
+
count = 0;
|
|
439
|
+
/** Set when the reader stepped over a restart marker — the caller resets its DC predictors. */
|
|
440
|
+
restarted = false;
|
|
441
|
+
data;
|
|
442
|
+
constructor(data, start) {
|
|
443
|
+
this.data = data;
|
|
444
|
+
this.position = start;
|
|
445
|
+
}
|
|
446
|
+
/** The offset the last whole byte ended at — what "how much of the scan did this consume" reads. */
|
|
447
|
+
get consumed() {
|
|
448
|
+
return this.position;
|
|
449
|
+
}
|
|
450
|
+
readBit() {
|
|
451
|
+
if (this.count === 0) {
|
|
452
|
+
if (this.position >= this.data.length)
|
|
453
|
+
throw new ScanEnd("out of data");
|
|
454
|
+
let byte = this.data[this.position++];
|
|
455
|
+
if (byte === 255) {
|
|
456
|
+
const next = this.data[this.position];
|
|
457
|
+
if (next === 0) {
|
|
458
|
+
this.position++;
|
|
459
|
+
} else if (next !== void 0 && next >= RST_FIRST && next <= RST_LAST) {
|
|
460
|
+
this.position++;
|
|
461
|
+
this.restarted = true;
|
|
462
|
+
if (this.position >= this.data.length)
|
|
463
|
+
throw new ScanEnd("out of data");
|
|
464
|
+
byte = this.data[this.position++];
|
|
465
|
+
if (byte === 255)
|
|
466
|
+
throw new ScanEnd("marker after restart");
|
|
467
|
+
} else {
|
|
468
|
+
this.position--;
|
|
469
|
+
throw new ScanEnd("marker");
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
this.bits = byte;
|
|
473
|
+
this.count = 8;
|
|
474
|
+
}
|
|
475
|
+
this.count--;
|
|
476
|
+
return this.bits >> this.count & 1;
|
|
477
|
+
}
|
|
478
|
+
/** Read `length` bits, most significant first. */
|
|
479
|
+
readBits(length) {
|
|
480
|
+
let value = 0;
|
|
481
|
+
for (let i = 0; i < length; i++)
|
|
482
|
+
value = value << 1 | this.readBit();
|
|
483
|
+
return value;
|
|
484
|
+
}
|
|
485
|
+
/** Drop the current byte's remaining bits — what a restart interval boundary does. */
|
|
486
|
+
align() {
|
|
487
|
+
this.count = 0;
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
function decodeHuffman(reader2, table) {
|
|
491
|
+
let code = reader2.readBit();
|
|
492
|
+
for (let length = 1; length <= 16; length++) {
|
|
493
|
+
if (table.maxCode[length] >= 0 && code <= table.maxCode[length]) {
|
|
494
|
+
return table.values[table.valPtr[length] + code - table.minCode[length]];
|
|
495
|
+
}
|
|
496
|
+
code = code << 1 | reader2.readBit();
|
|
497
|
+
}
|
|
498
|
+
throw new ScanEnd("no huffman code of any length");
|
|
499
|
+
}
|
|
500
|
+
function extend(value, length) {
|
|
501
|
+
return value < 1 << length - 1 ? value - (1 << length) + 1 : value;
|
|
502
|
+
}
|
|
503
|
+
function decodeBlock(reader2, component) {
|
|
504
|
+
const dcLength = decodeHuffman(reader2, component.dc);
|
|
505
|
+
if (dcLength > 16)
|
|
506
|
+
throw new ScanEnd("dc magnitude out of range");
|
|
507
|
+
component.pred += dcLength === 0 ? 0 : extend(reader2.readBits(dcLength), dcLength);
|
|
508
|
+
let k = 1;
|
|
509
|
+
while (k < 64) {
|
|
510
|
+
const rs = decodeHuffman(reader2, component.ac);
|
|
511
|
+
const size = rs & 15;
|
|
512
|
+
const run = rs >> 4;
|
|
513
|
+
if (size === 0) {
|
|
514
|
+
if (run !== 15)
|
|
515
|
+
break;
|
|
516
|
+
k += 16;
|
|
517
|
+
} else {
|
|
518
|
+
k += run;
|
|
519
|
+
if (k > 63)
|
|
520
|
+
throw new ScanEnd("ac coefficient index out of range");
|
|
521
|
+
reader2.readBits(size);
|
|
522
|
+
k++;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return component.pred;
|
|
526
|
+
}
|
|
527
|
+
function scanEntropy(tail, subsampling, extraTables) {
|
|
528
|
+
const tables = /* @__PURE__ */ new Map();
|
|
529
|
+
for (const segment of extraTables)
|
|
530
|
+
readHuffmanTables(segment, tables);
|
|
531
|
+
let at = 0;
|
|
532
|
+
let restartInterval = 0;
|
|
533
|
+
let scanStart = -1;
|
|
534
|
+
let assignments = [];
|
|
535
|
+
while (at + 3 < tail.length) {
|
|
536
|
+
if (tail[at] !== 255)
|
|
537
|
+
return null;
|
|
538
|
+
const marker = tail[at + 1];
|
|
539
|
+
const length = tail[at + 2] << 8 | tail[at + 3];
|
|
540
|
+
if (marker === DHT) {
|
|
541
|
+
readHuffmanTables(tail.subarray(at, at + 2 + length), tables);
|
|
542
|
+
} else if (marker === DRI) {
|
|
543
|
+
restartInterval = tail[at + 4] << 8 | tail[at + 5];
|
|
544
|
+
} else if (marker === SOS) {
|
|
545
|
+
const count2 = tail[at + 4];
|
|
546
|
+
assignments = [];
|
|
547
|
+
for (let i = 0; i < count2; i++) {
|
|
548
|
+
const id = tail[at + 5 + i * 2];
|
|
549
|
+
const tableByte = tail[at + 6 + i * 2];
|
|
550
|
+
assignments.push({ id, dc: tableByte >> 4, ac: tableByte & 15 });
|
|
551
|
+
}
|
|
552
|
+
scanStart = at + 2 + length;
|
|
553
|
+
break;
|
|
554
|
+
}
|
|
555
|
+
at += 2 + length;
|
|
556
|
+
}
|
|
557
|
+
if (scanStart < 0 || assignments.length !== 3)
|
|
558
|
+
return null;
|
|
559
|
+
const [lumaH, lumaV] = LUMA_SAMPLING[subsampling] ?? LUMA_SAMPLING[0];
|
|
560
|
+
const components = [];
|
|
561
|
+
for (const [index, assignment] of assignments.entries()) {
|
|
562
|
+
const dc = tables.get(assignment.dc);
|
|
563
|
+
const ac = tables.get(16 | assignment.ac);
|
|
564
|
+
if (!dc || !ac)
|
|
565
|
+
return null;
|
|
566
|
+
components.push({ h: index === 0 ? lumaH : 1, v: index === 0 ? lumaV : 1, dc, ac, pred: 0 });
|
|
567
|
+
}
|
|
568
|
+
const reader2 = new BitReader(tail, scanStart);
|
|
569
|
+
let luma = new Int32Array(1024);
|
|
570
|
+
const lumaBlocks = lumaH * lumaV;
|
|
571
|
+
let mcus = 0;
|
|
572
|
+
let consumed = reader2.consumed;
|
|
573
|
+
let complete = false;
|
|
574
|
+
try {
|
|
575
|
+
for (; ; ) {
|
|
576
|
+
if (restartInterval > 0 && mcus > 0 && mcus % restartInterval === 0) {
|
|
577
|
+
reader2.align();
|
|
578
|
+
for (const component of components)
|
|
579
|
+
component.pred = 0;
|
|
580
|
+
}
|
|
581
|
+
if (reader2.restarted) {
|
|
582
|
+
reader2.restarted = false;
|
|
583
|
+
for (const component of components)
|
|
584
|
+
component.pred = 0;
|
|
585
|
+
}
|
|
586
|
+
let lumaSum = 0;
|
|
587
|
+
for (const component of components) {
|
|
588
|
+
for (let block = 0; block < component.h * component.v; block++) {
|
|
589
|
+
const dc = decodeBlock(reader2, component);
|
|
590
|
+
if (component === components[0])
|
|
591
|
+
lumaSum += dc;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
if (mcus === luma.length) {
|
|
595
|
+
const grown = new Int32Array(luma.length * 2);
|
|
596
|
+
grown.set(luma);
|
|
597
|
+
luma = grown;
|
|
598
|
+
}
|
|
599
|
+
luma[mcus] = Math.round(lumaSum / lumaBlocks);
|
|
600
|
+
mcus++;
|
|
601
|
+
consumed = reader2.consumed;
|
|
602
|
+
if (consumed >= tail.length) {
|
|
603
|
+
complete = true;
|
|
604
|
+
break;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
} catch (error) {
|
|
608
|
+
if (!(error instanceof ScanEnd))
|
|
609
|
+
throw error;
|
|
610
|
+
complete = tail.length - consumed <= 2;
|
|
611
|
+
}
|
|
612
|
+
return { mcus, luma: luma.subarray(0, mcus), complete };
|
|
613
|
+
}
|
|
614
|
+
|
|
378
615
|
// dist/transport/http/decodeImageV2.js
|
|
379
|
-
import { decode as jpegDecode
|
|
616
|
+
import { decode as jpegDecode } from "jpeg-js";
|
|
380
617
|
var V2_PREFIX = "v2_eufysecurity:";
|
|
381
618
|
var DC_CHROMA = Buffer.from([255, 196, 0, 31, 1]);
|
|
382
619
|
var ZIGZAG = [
|
|
@@ -582,35 +819,6 @@ var DHT_AC_LUMA = Buffer.from("ffc400b5100002010303020403050504040000017d0102030
|
|
|
582
819
|
var SOI = Buffer.from([255, 216]);
|
|
583
820
|
var APP0 = Buffer.from("ffe000104a46494600010100000100010000", "hex");
|
|
584
821
|
var MCU = { 0: [8, 8], 1: [16, 8], 2: [16, 16] };
|
|
585
|
-
var LADDER = [
|
|
586
|
-
[160, 90],
|
|
587
|
-
[240, 135],
|
|
588
|
-
[256, 144],
|
|
589
|
-
[320, 180],
|
|
590
|
-
[384, 216],
|
|
591
|
-
[400, 225],
|
|
592
|
-
[480, 270],
|
|
593
|
-
[512, 288],
|
|
594
|
-
[576, 324],
|
|
595
|
-
[640, 360],
|
|
596
|
-
[704, 396],
|
|
597
|
-
[768, 432],
|
|
598
|
-
[848, 480],
|
|
599
|
-
[960, 540],
|
|
600
|
-
[1024, 576],
|
|
601
|
-
[1280, 720],
|
|
602
|
-
[1600, 900],
|
|
603
|
-
[1920, 1080],
|
|
604
|
-
[176, 144],
|
|
605
|
-
[320, 240],
|
|
606
|
-
[352, 288],
|
|
607
|
-
[480, 360],
|
|
608
|
-
[640, 480],
|
|
609
|
-
[800, 600],
|
|
610
|
-
[1024, 768],
|
|
611
|
-
[256, 480],
|
|
612
|
-
[320, 384]
|
|
613
|
-
];
|
|
614
822
|
function scaleQuant(base, quality) {
|
|
615
823
|
const factor = quality < 50 ? Math.floor(5e3 / quality) : 200 - quality * 2;
|
|
616
824
|
return base.map((v) => Math.min(255, Math.max(1, Math.floor((v * factor + 50) / 100))));
|
|
@@ -647,7 +855,7 @@ function sofSegment(width, height, subsampling) {
|
|
|
647
855
|
1
|
|
648
856
|
]);
|
|
649
857
|
}
|
|
650
|
-
function buildHeader(width, height, subsampling, quality
|
|
858
|
+
function buildHeader(width, height, subsampling, quality) {
|
|
651
859
|
return Buffer.concat([
|
|
652
860
|
SOI,
|
|
653
861
|
APP0,
|
|
@@ -658,99 +866,107 @@ function buildHeader(width, height, subsampling, quality = 85) {
|
|
|
658
866
|
DHT_AC_LUMA
|
|
659
867
|
]);
|
|
660
868
|
}
|
|
661
|
-
|
|
869
|
+
var PROBE_MEMORY_MB = 24;
|
|
870
|
+
function decodeSpliced(header, tail) {
|
|
662
871
|
try {
|
|
663
|
-
|
|
664
|
-
return jpegDecode(jpeg, { useTArray: true, maxMemoryUsageInMB: 128 });
|
|
872
|
+
return jpegDecode(Buffer.concat([header, tail]), { useTArray: true, maxMemoryUsageInMB: PROBE_MEMORY_MB });
|
|
665
873
|
} catch {
|
|
666
874
|
return null;
|
|
667
875
|
}
|
|
668
876
|
}
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
function rowShear(img, rows) {
|
|
684
|
-
const { width, data } = img;
|
|
685
|
-
let sum = 0;
|
|
686
|
-
let count2 = 0;
|
|
687
|
-
for (let y = 1; y < rows; y++) {
|
|
688
|
-
for (let x = 0; x < width; x += 4) {
|
|
689
|
-
sum += Math.abs(data[(y * width + x) * 4] - data[((y - 1) * width + x) * 4]);
|
|
690
|
-
count2++;
|
|
691
|
-
}
|
|
877
|
+
var REFERENCE_QUALITY = 85;
|
|
878
|
+
var MAX_STRETCH = 8;
|
|
879
|
+
var CUTOFF_PERCENT = 0.5;
|
|
880
|
+
function trimmedRange(data, channel, total, cutoff) {
|
|
881
|
+
const hist = new Array(256).fill(0);
|
|
882
|
+
for (let i = 0; i < total; i++)
|
|
883
|
+
hist[data[i * 4 + channel]]++;
|
|
884
|
+
let remaining = Math.floor(total * cutoff / 100);
|
|
885
|
+
let lo = 0;
|
|
886
|
+
while (lo < 255 && remaining > 0) {
|
|
887
|
+
if (remaining < hist[lo])
|
|
888
|
+
break;
|
|
889
|
+
remaining -= hist[lo];
|
|
890
|
+
lo++;
|
|
692
891
|
}
|
|
693
|
-
|
|
892
|
+
remaining = Math.floor(total * cutoff / 100);
|
|
893
|
+
let hi = 255;
|
|
894
|
+
while (hi > 0 && remaining > 0) {
|
|
895
|
+
if (remaining < hist[hi])
|
|
896
|
+
break;
|
|
897
|
+
remaining -= hist[hi];
|
|
898
|
+
hi--;
|
|
899
|
+
}
|
|
900
|
+
return { lo, hi };
|
|
694
901
|
}
|
|
695
|
-
function
|
|
696
|
-
const total = width * height;
|
|
902
|
+
function contrastScale(img, cutoff = CUTOFF_PERCENT) {
|
|
903
|
+
const total = img.width * img.height;
|
|
904
|
+
if (total === 0)
|
|
905
|
+
return 1;
|
|
906
|
+
let stretch = Number.POSITIVE_INFINITY;
|
|
697
907
|
for (let channel = 0; channel < 3; channel++) {
|
|
698
|
-
const
|
|
699
|
-
for (let i = 0; i < total; i++)
|
|
700
|
-
hist[data[i * 4 + channel]]++;
|
|
701
|
-
let remaining = Math.floor(total * cutoff / 100);
|
|
702
|
-
for (let bin = 0; bin < 256 && remaining > 0; bin++) {
|
|
703
|
-
if (remaining > hist[bin]) {
|
|
704
|
-
remaining -= hist[bin];
|
|
705
|
-
hist[bin] = 0;
|
|
706
|
-
} else {
|
|
707
|
-
hist[bin] -= remaining;
|
|
708
|
-
remaining = 0;
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
remaining = Math.floor(total * cutoff / 100);
|
|
712
|
-
for (let bin = 255; bin >= 0 && remaining > 0; bin--) {
|
|
713
|
-
if (remaining > hist[bin]) {
|
|
714
|
-
remaining -= hist[bin];
|
|
715
|
-
hist[bin] = 0;
|
|
716
|
-
} else {
|
|
717
|
-
hist[bin] -= remaining;
|
|
718
|
-
remaining = 0;
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
let lo = 0;
|
|
722
|
-
while (lo < 256 && hist[lo] === 0)
|
|
723
|
-
lo++;
|
|
724
|
-
let hi = 255;
|
|
725
|
-
while (hi >= 0 && hist[hi] === 0)
|
|
726
|
-
hi--;
|
|
908
|
+
const { lo, hi } = trimmedRange(img.data, channel, total, cutoff);
|
|
727
909
|
if (hi <= lo)
|
|
728
910
|
continue;
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
}
|
|
738
|
-
function
|
|
739
|
-
const
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
911
|
+
if (lo < 128)
|
|
912
|
+
stretch = Math.min(stretch, 128 / (128 - lo));
|
|
913
|
+
if (hi > 128)
|
|
914
|
+
stretch = Math.min(stretch, 127 / (hi - 128));
|
|
915
|
+
}
|
|
916
|
+
if (!Number.isFinite(stretch))
|
|
917
|
+
return 1;
|
|
918
|
+
return Math.min(MAX_STRETCH, Math.max(1, stretch));
|
|
919
|
+
}
|
|
920
|
+
function qualityForStretch(stretch) {
|
|
921
|
+
const referenceFactor = REFERENCE_QUALITY < 50 ? Math.floor(5e3 / REFERENCE_QUALITY) : 200 - REFERENCE_QUALITY * 2;
|
|
922
|
+
const factor = referenceFactor * stretch;
|
|
923
|
+
const quality = factor <= 100 ? (200 - factor) / 2 : 5e3 / factor;
|
|
924
|
+
return Math.min(99, Math.max(1, Math.round(quality)));
|
|
925
|
+
}
|
|
926
|
+
var MAX_EDGE = 4096;
|
|
927
|
+
var MIN_ASPECT = 0.4;
|
|
928
|
+
var MAX_ASPECT = 4;
|
|
929
|
+
function mcuRowShear(luma, mcuWidth, mcuHeight) {
|
|
930
|
+
if (mcuHeight < 2)
|
|
931
|
+
return Number.POSITIVE_INFINITY;
|
|
932
|
+
let sum = 0;
|
|
933
|
+
for (let y = 1; y < mcuHeight; y++) {
|
|
934
|
+
for (let x = 0; x < mcuWidth; x++)
|
|
935
|
+
sum += Math.abs(luma[y * mcuWidth + x] - luma[(y - 1) * mcuWidth + x]);
|
|
936
|
+
}
|
|
937
|
+
return sum / (mcuWidth * (mcuHeight - 1));
|
|
938
|
+
}
|
|
939
|
+
function bestGeometry(luma, mcus, subsampling) {
|
|
940
|
+
const [mcuPixelWidth, mcuPixelHeight] = MCU[subsampling];
|
|
941
|
+
let best = null;
|
|
942
|
+
for (let mcuWidth = 1; mcuWidth <= mcus; mcuWidth++) {
|
|
943
|
+
if (mcus % mcuWidth !== 0)
|
|
944
|
+
continue;
|
|
945
|
+
const mcuHeight = mcus / mcuWidth;
|
|
946
|
+
const width = mcuWidth * mcuPixelWidth;
|
|
947
|
+
const height = mcuHeight * mcuPixelHeight;
|
|
948
|
+
if (width > MAX_EDGE || height > MAX_EDGE)
|
|
949
|
+
continue;
|
|
950
|
+
const aspect = width / height;
|
|
951
|
+
if (aspect < MIN_ASPECT || aspect > MAX_ASPECT)
|
|
952
|
+
continue;
|
|
953
|
+
const shear = mcuRowShear(luma, mcuWidth, mcuHeight);
|
|
954
|
+
if (!best || shear < best.shear)
|
|
955
|
+
best = { subsampling, width, height, shear };
|
|
956
|
+
}
|
|
957
|
+
return best;
|
|
958
|
+
}
|
|
959
|
+
function findGeometry(tail) {
|
|
960
|
+
let best = null;
|
|
961
|
+
for (const subsampling of [0, 1, 2]) {
|
|
962
|
+
const scan = scanEntropy(tail, subsampling, [DHT_DC_LUMA, DHT_AC_LUMA]);
|
|
963
|
+
if (!scan?.complete || scan.mcus < 1)
|
|
964
|
+
continue;
|
|
965
|
+
const geometry = bestGeometry(scan.luma, scan.mcus, subsampling);
|
|
966
|
+
if (geometry && (!best || geometry.shear < best.shear))
|
|
967
|
+
best = geometry;
|
|
968
|
+
}
|
|
969
|
+
return best;
|
|
754
970
|
}
|
|
755
971
|
function isV2Image(data) {
|
|
756
972
|
return data.length >= V2_PREFIX.length && data.subarray(0, V2_PREFIX.length).toString("latin1") === V2_PREFIX;
|
|
@@ -762,53 +978,17 @@ function decodeImageV2(data) {
|
|
|
762
978
|
if (cut < 0)
|
|
763
979
|
return null;
|
|
764
980
|
const tail = data.subarray(cut);
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
let filled = null;
|
|
768
|
-
for (const [width2, height2] of ladderByMcuCount(subsampling2)) {
|
|
769
|
-
const img2 = decodeCandidate(tail, width2, height2, subsampling2);
|
|
770
|
-
if (!img2)
|
|
771
|
-
break;
|
|
772
|
-
if (!filled || width2 * height2 > filled.width * filled.height)
|
|
773
|
-
filled = { width: width2, height: height2, img: img2 };
|
|
774
|
-
}
|
|
775
|
-
if (!filled)
|
|
776
|
-
continue;
|
|
777
|
-
const spread = colorSpread(filled.img);
|
|
778
|
-
if (!best || spread < best.spread)
|
|
779
|
-
best = { spread, subsampling: subsampling2, width: filled.width, height: filled.height };
|
|
780
|
-
}
|
|
781
|
-
if (!best)
|
|
981
|
+
const geometry = findGeometry(tail);
|
|
982
|
+
if (!geometry)
|
|
782
983
|
return null;
|
|
783
|
-
const {
|
|
784
|
-
const
|
|
785
|
-
const
|
|
786
|
-
|
|
787
|
-
let width = coarseWidth;
|
|
788
|
-
let height = coarseHeight;
|
|
789
|
-
let bestShear = Number.POSITIVE_INFINITY;
|
|
790
|
-
const lowWidth = Math.max(mcuWidth * 4, Math.round(coarseWidth * 0.75 / mcuWidth) * mcuWidth);
|
|
791
|
-
const highWidth = Math.round(coarseWidth * 1.25 / mcuWidth) * mcuWidth;
|
|
792
|
-
for (let candidateWidth = lowWidth; candidateWidth <= highWidth; candidateWidth += mcuWidth) {
|
|
793
|
-
const candidateHeight = Math.floor(totalMcus / (candidateWidth / mcuWidth)) * mcuHeight;
|
|
794
|
-
if (candidateHeight < mcuHeight)
|
|
795
|
-
continue;
|
|
796
|
-
const img2 = decodeCandidate(tail, candidateWidth, candidateHeight, subsampling);
|
|
797
|
-
if (!img2)
|
|
798
|
-
continue;
|
|
799
|
-
const shear = rowShear(img2, candidateHeight);
|
|
800
|
-
if (shear < bestShear) {
|
|
801
|
-
bestShear = shear;
|
|
802
|
-
width = candidateWidth;
|
|
803
|
-
height = candidateHeight;
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
height = maxHeight(tail, width, subsampling, height);
|
|
807
|
-
const img = decodeCandidate(tail, width, height, subsampling);
|
|
808
|
-
if (!img)
|
|
984
|
+
const { width, height, subsampling } = geometry;
|
|
985
|
+
const reference = buildHeader(width, height, subsampling, REFERENCE_QUALITY);
|
|
986
|
+
const probe = decodeSpliced(reference, tail);
|
|
987
|
+
if (!probe)
|
|
809
988
|
return null;
|
|
810
|
-
|
|
811
|
-
|
|
989
|
+
const stretch = contrastScale(probe);
|
|
990
|
+
const header = stretch > 1.01 ? buildHeader(width, height, subsampling, qualityForStretch(stretch)) : reference;
|
|
991
|
+
return Buffer.concat([header, tail]);
|
|
812
992
|
}
|
|
813
993
|
|
|
814
994
|
// dist/transport/http/decodeImageV1.js
|
|
@@ -955,7 +1135,7 @@ var REAUTH_HOLD_OFF_CAP_MS = 30 * 6e4;
|
|
|
955
1135
|
var REAUTH_STABLE_MS = 10 * 6e4;
|
|
956
1136
|
var CONTENDED_SESSION_HINT = "another client may be signed in with the same account and device identity, and each login displaces the other's session; give each client its own openudid";
|
|
957
1137
|
function tokenRejected(code, msg) {
|
|
958
|
-
return code === EufyCloudErrorCode.SESSION_KICKED || /user_id is empty|invalid
|
|
1138
|
+
return code === EufyCloudErrorCode.SESSION_KICKED || /user_id is empty|invalid[^,]*\btoken\b|\btoken\b[^,]*(expired|error|not exist)|kicked|(?:\btoken\b|\bsession\b)[^,]*does not exist|unauthor/i.test(msg ?? "");
|
|
959
1139
|
}
|
|
960
1140
|
function withoutTokenEcho(text2) {
|
|
961
1141
|
return text2.replace(/token\s*[=:]\s*"?[A-Za-z0-9._-]{8,}"?/gi, "token = <redacted>");
|
|
@@ -967,6 +1147,13 @@ var MegaHttpClient = class {
|
|
|
967
1147
|
sessionKey;
|
|
968
1148
|
/** Per-host ECDH session keys for non-mega gateways (e.g. eufylife) keyed by host. */
|
|
969
1149
|
sessionKeys = /* @__PURE__ */ new Map();
|
|
1150
|
+
/**
|
|
1151
|
+
* The held credential. `userId` is the login reply's `ap_cloud_user_id` where it has one — the Anker
|
|
1152
|
+
* Passport cloud's id — while `accountUserId` is the eufy account's own `user_id`.
|
|
1153
|
+
*
|
|
1154
|
+
* The `gtoken` header is hashed from `accountUserId`: that is the id the gateway recomputes the header
|
|
1155
|
+
* from, rejecting a disagreement with `"gtoken not equal userid error"`.
|
|
1156
|
+
*/
|
|
970
1157
|
auth_;
|
|
971
1158
|
/** captcha_id of an in-flight challenge, held between login() and solveCaptcha(). */
|
|
972
1159
|
pendingCaptchaId;
|
|
@@ -1032,7 +1219,12 @@ var MegaHttpClient = class {
|
|
|
1032
1219
|
if (!isSessionValid(saved) || !saved)
|
|
1033
1220
|
return void 0;
|
|
1034
1221
|
this.region = saved.region;
|
|
1035
|
-
this.auth_ = {
|
|
1222
|
+
this.auth_ = {
|
|
1223
|
+
userId: saved.userId,
|
|
1224
|
+
accountUserId: saved.accountUserId,
|
|
1225
|
+
authToken: saved.authToken,
|
|
1226
|
+
geoKey: saved.geoKey
|
|
1227
|
+
};
|
|
1036
1228
|
this.tokenExpiresAt = saved.tokenExpiresAt;
|
|
1037
1229
|
this.sessionKey = {
|
|
1038
1230
|
keyIdent: saved.keyIdent,
|
|
@@ -1095,7 +1287,16 @@ var MegaHttpClient = class {
|
|
|
1095
1287
|
};
|
|
1096
1288
|
}
|
|
1097
1289
|
/**
|
|
1098
|
-
* The
|
|
1290
|
+
* The id `gtoken` is hashed from — the account's own `user_id`, which is what the gateway recomputes the
|
|
1291
|
+
* header from. One place so the two header paths cannot drift on which of the session's ids that is.
|
|
1292
|
+
*
|
|
1293
|
+
* Call only where `auth_` is already established; every header path guards it.
|
|
1294
|
+
*/
|
|
1295
|
+
gtokenUserId() {
|
|
1296
|
+
return this.auth_.accountUserId ?? this.auth_.userId;
|
|
1297
|
+
}
|
|
1298
|
+
/**
|
|
1299
|
+
* The account-credential headers every authed call carries — `x-auth-token` + `gtoken`.
|
|
1099
1300
|
* One place so the signed path, the key-exchange and the bearer path can't drift on what "authed" means.
|
|
1100
1301
|
*/
|
|
1101
1302
|
authTokenHeaders() {
|
|
@@ -1104,7 +1305,7 @@ var MegaHttpClient = class {
|
|
|
1104
1305
|
return {
|
|
1105
1306
|
"x-auth-token": this.auth_.authToken,
|
|
1106
1307
|
authorization: this.auth_.authToken,
|
|
1107
|
-
gtoken: gtoken(this.
|
|
1308
|
+
gtoken: gtoken(this.gtokenUserId())
|
|
1108
1309
|
};
|
|
1109
1310
|
}
|
|
1110
1311
|
/**
|
|
@@ -1408,7 +1609,7 @@ var MegaHttpClient = class {
|
|
|
1408
1609
|
try {
|
|
1409
1610
|
return await downloadMediaResource(url, {
|
|
1410
1611
|
"x-auth-token": this.auth_.authToken,
|
|
1411
|
-
gtoken: gtoken(this.
|
|
1612
|
+
gtoken: gtoken(this.gtokenUserId()),
|
|
1412
1613
|
"app-name": "eufy_mega",
|
|
1413
1614
|
"model-type": "PHONE",
|
|
1414
1615
|
"user-agent": this.mediaUserAgent
|
|
@@ -1420,9 +1621,20 @@ var MegaHttpClient = class {
|
|
|
1420
1621
|
throw error;
|
|
1421
1622
|
}
|
|
1422
1623
|
}
|
|
1423
|
-
/**
|
|
1624
|
+
/**
|
|
1625
|
+
* Download push image bytes and decrypt a recognized v1 wrapper when its device key input is available.
|
|
1626
|
+
*
|
|
1627
|
+
* A decoder throw is tagged `decode-failed`: to anything downstream, the difference between "the
|
|
1628
|
+
* bytes never arrived" and "the bytes arrived and the wrapper would not decrypt" is the difference
|
|
1629
|
+
* between a network problem and a key problem, and one of them is this SDK's to fix.
|
|
1630
|
+
*/
|
|
1424
1631
|
async downloadImage(url, p2pDid) {
|
|
1425
|
-
|
|
1632
|
+
const data = await this.downloadMedia(url);
|
|
1633
|
+
try {
|
|
1634
|
+
return normalizePushImage(data, p2pDid);
|
|
1635
|
+
} catch (error) {
|
|
1636
|
+
throw mediaFailureError("Push image could not be decoded", "decode-failed", error);
|
|
1637
|
+
}
|
|
1426
1638
|
}
|
|
1427
1639
|
/** The security-app data host for this region (face recognition, media, etc.). */
|
|
1428
1640
|
securityAppHost() {
|
|
@@ -1699,17 +1911,19 @@ var MegaHttpClient = class {
|
|
|
1699
1911
|
{
|
|
1700
1912
|
const cap = Object.keys(res).filter((k) => /captcha|answer|picture|image|fa_/i.test(k));
|
|
1701
1913
|
this.logger.debug("[mega] login resp keys:", Object.keys(res).join(","));
|
|
1914
|
+
this.logger.debug("[mega] ids agree:", res.ap_cloud_user_id === res.user_id);
|
|
1702
1915
|
if (cap.length)
|
|
1703
1916
|
this.logger.debug("[mega] captcha/fa:", JSON.stringify(Object.fromEntries(cap.map((k) => [k, res[k]]))));
|
|
1704
1917
|
}
|
|
1705
1918
|
const userId = res.ap_cloud_user_id ?? res.user_id ?? res.userId;
|
|
1919
|
+
const accountUserId = res.user_id ?? res.userId;
|
|
1706
1920
|
const authToken = res.auth_token ?? res.token;
|
|
1707
1921
|
if (!userId || !authToken)
|
|
1708
1922
|
throw new Error(`login returned no session: ${JSON.stringify(res).slice(0, 200)}`);
|
|
1709
1923
|
const faInfo = res.fa_info ?? {};
|
|
1710
1924
|
const needs2fa = !isVerify && !!faInfo.info;
|
|
1711
1925
|
if (needs2fa) {
|
|
1712
|
-
this.auth_ = { userId, authToken, geoKey: res.geo_key };
|
|
1926
|
+
this.auth_ = { userId, accountUserId, authToken, geoKey: res.geo_key };
|
|
1713
1927
|
this.pendingCaptchaId = void 0;
|
|
1714
1928
|
this.pending2fa = true;
|
|
1715
1929
|
await this.sendVerifyCode(messageType);
|
|
@@ -1717,7 +1931,7 @@ var MegaHttpClient = class {
|
|
|
1717
1931
|
}
|
|
1718
1932
|
this.pendingCaptchaId = void 0;
|
|
1719
1933
|
this.pending2fa = false;
|
|
1720
|
-
this.auth_ = { userId, authToken, geoKey: res.geo_key };
|
|
1934
|
+
this.auth_ = { userId, accountUserId, authToken, geoKey: res.geo_key };
|
|
1721
1935
|
this.tokenExpiresAt = Number(res.token_expires_at ?? 0) || 0;
|
|
1722
1936
|
this.sessionKey = void 0;
|
|
1723
1937
|
await this.ensureSessionKey();
|
|
@@ -1730,6 +1944,7 @@ var MegaHttpClient = class {
|
|
|
1730
1944
|
return;
|
|
1731
1945
|
this.store.save({
|
|
1732
1946
|
userId: this.auth_.userId,
|
|
1947
|
+
accountUserId: this.auth_.accountUserId ?? this.auth_.userId,
|
|
1733
1948
|
authToken: this.auth_.authToken,
|
|
1734
1949
|
geoKey: this.auth_.geoKey,
|
|
1735
1950
|
region: this.region,
|
|
@@ -12880,7 +13095,7 @@ function lz4BlockDecompress(block, size) {
|
|
|
12880
13095
|
const end = block.length;
|
|
12881
13096
|
let pos = 0;
|
|
12882
13097
|
let written = 0;
|
|
12883
|
-
const
|
|
13098
|
+
const extend2 = (base) => {
|
|
12884
13099
|
let len = base;
|
|
12885
13100
|
if (base !== 15)
|
|
12886
13101
|
return len;
|
|
@@ -12895,7 +13110,7 @@ function lz4BlockDecompress(block, size) {
|
|
|
12895
13110
|
};
|
|
12896
13111
|
while (pos < end) {
|
|
12897
13112
|
const token = block[pos++];
|
|
12898
|
-
const literals =
|
|
13113
|
+
const literals = extend2(token >> 4);
|
|
12899
13114
|
if (literals === void 0)
|
|
12900
13115
|
return void 0;
|
|
12901
13116
|
if (pos + literals > end || written + literals > size)
|
|
@@ -12910,7 +13125,7 @@ function lz4BlockDecompress(block, size) {
|
|
|
12910
13125
|
pos += 2;
|
|
12911
13126
|
if (offset === 0 || offset > written)
|
|
12912
13127
|
return void 0;
|
|
12913
|
-
const match =
|
|
13128
|
+
const match = extend2(token & 15);
|
|
12914
13129
|
if (match === void 0)
|
|
12915
13130
|
return void 0;
|
|
12916
13131
|
const length = match + 4;
|
|
@@ -23367,8 +23582,21 @@ var FileFcmStore = class {
|
|
|
23367
23582
|
}
|
|
23368
23583
|
};
|
|
23369
23584
|
|
|
23585
|
+
// dist/transport/media-failure.js
|
|
23586
|
+
var MEDIA_FAILURE_REASONS = [
|
|
23587
|
+
"url-not-allowed",
|
|
23588
|
+
"address-not-public",
|
|
23589
|
+
"redirect-not-allowed",
|
|
23590
|
+
"http-status",
|
|
23591
|
+
"too-large",
|
|
23592
|
+
"timeout",
|
|
23593
|
+
"network",
|
|
23594
|
+
"decode-failed"
|
|
23595
|
+
];
|
|
23596
|
+
|
|
23370
23597
|
// dist/transport/stored-image-cache.js
|
|
23371
23598
|
var MAX_JPEG_BYTES = 10 * 1024 * 1024;
|
|
23599
|
+
var MAX_REMEMBERED_URLS = 64;
|
|
23372
23600
|
var DIAGNOSTIC_INTERVAL_MS = 6e4;
|
|
23373
23601
|
var StoredImageCache = class {
|
|
23374
23602
|
downloader;
|
|
@@ -23385,7 +23613,13 @@ var StoredImageCache = class {
|
|
|
23385
23613
|
this.clock = clock;
|
|
23386
23614
|
this.isLifecycleError = isLifecycleError;
|
|
23387
23615
|
}
|
|
23388
|
-
/**
|
|
23616
|
+
/**
|
|
23617
|
+
* Observe a normalized thumbnail URL and start acquisition eagerly.
|
|
23618
|
+
*
|
|
23619
|
+
* A URL already inside this device's window of recent attempts is ignored, so one event arriving as
|
|
23620
|
+
* several pushes downloads one thumbnail. The window is a `Set`, which iterates in insertion order,
|
|
23621
|
+
* so the entry evicted once it is full is the oldest attempt.
|
|
23622
|
+
*/
|
|
23389
23623
|
observe(deviceKey, url) {
|
|
23390
23624
|
let state = this.devices.get(deviceKey);
|
|
23391
23625
|
if (!state) {
|
|
@@ -23400,6 +23634,12 @@ var StoredImageCache = class {
|
|
|
23400
23634
|
if (state.seenUrls.has(url))
|
|
23401
23635
|
return;
|
|
23402
23636
|
state.seenUrls.add(url);
|
|
23637
|
+
while (state.seenUrls.size > MAX_REMEMBERED_URLS) {
|
|
23638
|
+
const oldest = state.seenUrls.values().next();
|
|
23639
|
+
if (oldest.done)
|
|
23640
|
+
break;
|
|
23641
|
+
state.seenUrls.delete(oldest.value);
|
|
23642
|
+
}
|
|
23403
23643
|
state.queued = {
|
|
23404
23644
|
deviceKey,
|
|
23405
23645
|
url,
|
|
@@ -23449,7 +23689,7 @@ var StoredImageCache = class {
|
|
|
23449
23689
|
} else if (image === void 0) {
|
|
23450
23690
|
state.lifecycleError = void 0;
|
|
23451
23691
|
state.reason = "download-failed";
|
|
23452
|
-
this.diagnose(state, candidate, "download-failed");
|
|
23692
|
+
this.diagnose(state, candidate, "download-failed", error);
|
|
23453
23693
|
} else if (!this.isValidJpeg(image)) {
|
|
23454
23694
|
state.lifecycleError = void 0;
|
|
23455
23695
|
state.reason = "invalid-image";
|
|
@@ -23465,7 +23705,7 @@ var StoredImageCache = class {
|
|
|
23465
23705
|
isValidJpeg(image) {
|
|
23466
23706
|
return Buffer.isBuffer(image) && image.length > 0 && image.length <= MAX_JPEG_BYTES && image.length >= 5 && image[0] === 255 && image[1] === 216 && image[2] === 255 && image[image.length - 2] === 255 && image[image.length - 1] === 217;
|
|
23467
23707
|
}
|
|
23468
|
-
diagnose(state, candidate, failure) {
|
|
23708
|
+
diagnose(state, candidate, failure, error) {
|
|
23469
23709
|
const now2 = this.clock();
|
|
23470
23710
|
const last = state.loggedFailures.get(failure);
|
|
23471
23711
|
if (last !== void 0 && now2 - last < DIAGNOSTIC_INTERVAL_MS)
|
|
@@ -23473,11 +23713,23 @@ var StoredImageCache = class {
|
|
|
23473
23713
|
state.loggedFailures.set(failure, now2);
|
|
23474
23714
|
this.logger.warn("[stored-snapshot-cache] candidate failed", {
|
|
23475
23715
|
class: failure,
|
|
23716
|
+
...mediaFailureTag(error),
|
|
23476
23717
|
observedAt: candidate.observedAt,
|
|
23477
23718
|
retained: state.retained !== void 0
|
|
23478
23719
|
});
|
|
23479
23720
|
}
|
|
23480
23721
|
};
|
|
23722
|
+
function mediaFailureTag(error) {
|
|
23723
|
+
if (typeof error !== "object" || error === null)
|
|
23724
|
+
return {};
|
|
23725
|
+
const { mediaFailure, status } = error;
|
|
23726
|
+
if (typeof mediaFailure !== "string")
|
|
23727
|
+
return {};
|
|
23728
|
+
const cause = MEDIA_FAILURE_REASONS.find((reason) => reason === mediaFailure);
|
|
23729
|
+
if (!cause)
|
|
23730
|
+
return {};
|
|
23731
|
+
return Number.isInteger(status) && status >= 100 && status <= 599 ? { cause, status } : { cause };
|
|
23732
|
+
}
|
|
23481
23733
|
|
|
23482
23734
|
// dist/client/device-registry.js
|
|
23483
23735
|
function resolvedStationSn(raw, sn) {
|
|
@@ -26178,6 +26430,11 @@ var SolixClient = class {
|
|
|
26178
26430
|
* Turn a decrypted `/passport/login` payload into an `ok`/`2fa` result, establishing the session on
|
|
26179
26431
|
* `ok`. The passport marks a pending 2FA with a non-empty `fa_info.info`, and empties it once the code
|
|
26180
26432
|
* has been satisfied.
|
|
26433
|
+
*
|
|
26434
|
+
* `gtoken` is hashed from `ap_cloud_user_id` where the reply carries one, `user_id` otherwise. Whether
|
|
26435
|
+
* this gateway recomputes the header from `user_id` specifically — as the mega gateway does, rejecting a
|
|
26436
|
+
* disagreement with `"gtoken not equal userid error"` — is unverified here: no Solix response has been
|
|
26437
|
+
* observed refusing the header, which is consistent with the two ids agreeing on the accounts seen.
|
|
26181
26438
|
*/
|
|
26182
26439
|
classifyLogin(data, isVerify) {
|
|
26183
26440
|
const userId = data.ap_cloud_user_id ?? data.user_id;
|
|
@@ -26747,7 +27004,6 @@ export {
|
|
|
26747
27004
|
aesKey,
|
|
26748
27005
|
asBool,
|
|
26749
27006
|
assertNever,
|
|
26750
|
-
autoContrast,
|
|
26751
27007
|
bizChannelName,
|
|
26752
27008
|
buildActions,
|
|
26753
27009
|
buildAppShapedClientId,
|