@mega-yfue/eufy-sdk 0.6.0-beta.37 → 0.6.0-beta.39

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/index.js CHANGED
@@ -4800,11 +4800,20 @@ function cellGated(members) {
4800
4800
  });
4801
4801
  return Object.fromEntries(gated);
4802
4802
  }
4803
+ var notSensor = (ctx) => ctx.codec !== "sensor";
4804
+ function notOnSensor(members) {
4805
+ const gated = Object.entries(members).map(([name, m]) => {
4806
+ const own = m.available;
4807
+ const available = own ? (ctx) => notSensor(ctx) && own(ctx) : notSensor;
4808
+ return [name, { ...m, available }];
4809
+ });
4810
+ return Object.fromEntries(gated);
4811
+ }
4803
4812
  function isChargingState(v) {
4804
4813
  const n = Number(v);
4805
4814
  return n !== 0 && n !== 2;
4806
4815
  }
4807
- var BATTERY_MEMBERS = cellGated({
4816
+ var CAMERA_BATTERY_MEMBERS = cellGated({
4808
4817
  /**
4809
4818
  * The headline percentage, and this capability's detection evidence: reporting 1101 is what proves a
4810
4819
  * device is battery-powered, which is also the fact `MediaProvider` reads to decide a stream needs a
@@ -5031,6 +5040,7 @@ var BATTERY_MEMBERS = cellGated({
5031
5040
  description: "Raw GET_CAMERA_INFO value (1103), meaning unevidenced \u2014 read live as the constant 5 on a T8170 at 92% and a T8171 at 27%, so it is NOT a low-battery flag."
5032
5041
  }
5033
5042
  });
5043
+ var BATTERY_MEMBERS = notOnSensor(CAMERA_BATTERY_MEMBERS);
5034
5044
  var BATTERY = {
5035
5045
  capability: "battery",
5036
5046
  description: "Battery level, charging, health, temperature, and solar input.",
@@ -15325,6 +15335,7 @@ __export(codec_exports, {
15325
15335
  encryptP2PData: () => encryptP2PData,
15326
15336
  frameMessage: () => frameMessage,
15327
15337
  gatewayInfoCipherId: () => gatewayInfoCipherId,
15338
+ gatewayInfoRefused: () => gatewayInfoRefused,
15328
15339
  hasHeader: () => hasHeader,
15329
15340
  p2pCommandEncryptionKey: () => p2pCommandEncryptionKey,
15330
15341
  p2pDidToBuffer: () => p2pDidToBuffer,
@@ -15443,6 +15454,10 @@ function decryptP2PData(data, key) {
15443
15454
  function gatewayInfoCipherId(gwPayload) {
15444
15455
  return gwPayload.length >= 2 ? gwPayload.readUInt16LE(0) : 0;
15445
15456
  }
15457
+ var GATEWAY_LEVEL2_REFUSED = -103;
15458
+ function gatewayInfoRefused(gwPayload) {
15459
+ return gwPayload.length >= 4 && gwPayload.readInt32LE(0) === GATEWAY_LEVEL2_REFUSED;
15460
+ }
15446
15461
  function eufyKdf(shared, outLen) {
15447
15462
  const hmac = (k, d) => createHmac2("sha256", k).update(d).digest();
15448
15463
  const label = Buffer.from("ECIES");
@@ -16588,6 +16603,8 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
16588
16603
  liveStartedChannels = /* @__PURE__ */ new Map();
16589
16604
  /** The `cipher_id` this connection's `CMD_GATEWAYINFO` reply named, once its negotiation has read it. */
16590
16605
  gatewayCipherId;
16606
+ /** Whether this connection's latest `CMD_GATEWAYINFO` reply was a refusal — see {@link gatewayInfoRefused}. */
16607
+ gatewayRefused = false;
16591
16608
  /** The `n` of the last `[0x01, n]` header this connection sent in the clear — see {@link nextClearCommandBytes}. */
16592
16609
  clearCommandCount = 0;
16593
16610
  unackedLiveStarts = /* @__PURE__ */ new Map();
@@ -16785,9 +16802,17 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
16785
16802
  * Every outcome settles the wait in {@link awaitLevel2Key}, so a later command is told at once rather than
16786
16803
  * left to time out against a key that is not coming. Settling also re-opens the negotiation, so a later
16787
16804
  * GATEWAYINFO reply in the same connection generation runs it again; a lookup that completes after its
16788
- * generation ended settles nothing.
16805
+ * generation ended settles nothing. A {@link gatewayInfoRefused} reply names no cipher, so it settles at once
16806
+ * with no cloud lookup, and a later reply naming a cipher withdraws the refusal.
16789
16807
  */
16790
16808
  negotiateLevel2Key(gwPayload) {
16809
+ this.gatewayRefused = gatewayInfoRefused(gwPayload);
16810
+ if (this.gatewayRefused) {
16811
+ this.logger.debug(`[p2p] ${this.cfg.stationSn} gateway info refused level-2`);
16812
+ this.trace({ phase: "level2-unavailable", reason: "gateway-refused" });
16813
+ this.settleLevel2();
16814
+ return;
16815
+ }
16791
16816
  this.level2Negotiating = true;
16792
16817
  const generation = this.connectionGeneration;
16793
16818
  const cipherId = gatewayInfoCipherId(gwPayload);
@@ -16910,6 +16935,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
16910
16935
  this.level2Negotiating = false;
16911
16936
  this.level2Reprompted = false;
16912
16937
  this.gatewayCipherId = void 0;
16938
+ this.gatewayRefused = false;
16913
16939
  this.clearCommandCount = 0;
16914
16940
  }
16915
16941
  const socket = dgram.createSocket("udp4");
@@ -17534,6 +17560,13 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
17534
17560
  this.seqNumber = this.seqNumber + 1 & 65535;
17535
17561
  this.send(this.connectAddress, RequestMessageType.DATA, data);
17536
17562
  }
17563
+ /**
17564
+ * Whether this connection's station answered `CMD_GATEWAYINFO` with a refusal ({@link gatewayInfoRefused}) and
17565
+ * no key that frames a set-payload is held.
17566
+ */
17567
+ get refusedLevel2() {
17568
+ return this.gatewayRefused && !this.level2KeyServes("set-payload");
17569
+ }
17537
17570
  /**
17538
17571
  * Send a **level-2 (AES-256-GCM, signCode 8) control payload** to a HomeBase-attached device. The
17539
17572
  * target camera is selected by `channel` (= device_channel) + the `mChannel` envelope — the same
@@ -17544,10 +17577,15 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
17544
17577
  * On a session holding only the AES-128-ECB level-2 key of an RSA cipher, the frame is the level-1
17545
17578
  * layout ({@link sendRawLevel1Bytes}) sealed under that key at encryption type 2: the encryption-type
17546
17579
  * byte names the key the body is sealed with (1 = level-1 key, 2 = the negotiated key).
17580
+ *
17581
+ * With `clearOnRefusal` on a session that {@link refusedLevel2}, the frame goes in the clear in the vendor
17582
+ * app's form for such a device: header bytes `[01 00]`, signCode 0, and a body of `{cmd, payload}` alone.
17583
+ * Without it, a refused session answers `false` and sends nothing.
17547
17584
  */
17548
- sendControlLevel2(cmd, channel, accountId, payload, mValue3 = cmd) {
17549
- if (!this.connectAddress || !this.level2KeyServes("set-payload"))
17585
+ sendControlLevel2(cmd, channel, accountId, payload, mValue3 = cmd, clearOnRefusal = false) {
17586
+ if (!this.connectAddress || !(this.level2KeyServes("set-payload") || clearOnRefusal && this.refusedLevel2)) {
17550
17587
  return false;
17588
+ }
17551
17589
  const value = Buffer.from(JSON.stringify({ account_id: accountId, cmd, mChannel: channel, mValue3, payload }), "utf-8");
17552
17590
  let sealed;
17553
17591
  if (this.level2Key) {
@@ -17555,8 +17593,10 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
17555
17593
  if (!body)
17556
17594
  return false;
17557
17595
  sealed = buildRawCommandPayload(body, channel, 8, [8, 0], 0);
17558
- } else {
17596
+ } else if (this.level2EcbKey) {
17559
17597
  sealed = buildRawCommandPayload(encryptP2PData(paddingP2PData(value), this.level2EcbKey), channel, 2);
17598
+ } else {
17599
+ sealed = buildRawCommandPayload(Buffer.from(JSON.stringify({ cmd, payload }), "utf-8"), channel);
17560
17600
  }
17561
17601
  const data = Buffer.concat([buildCommandHeader(this.seqNumber, CMD_SET_PAYLOAD), sealed]);
17562
17602
  this.seqNumber = this.seqNumber + 1 & 65535;
@@ -18909,169 +18949,6 @@ import { EventEmitter as EventEmitter3 } from "node:events";
18909
18949
 
18910
18950
  // dist/transport/p2p/video.js
18911
18951
  import { createDecipheriv as createDecipheriv6 } from "node:crypto";
18912
- var VIDEO_GCM_AAD = Buffer.from("eufy security");
18913
- var HEADER_LEN = 22;
18914
- var ENVELOPE_OFFSET = 22;
18915
- var ENVELOPE_LEN = 129;
18916
- var GCM_TAG_OFFSET = 151;
18917
- var GCM_TAG_LEN = 16;
18918
- var GCM_NONCE_OFFSET = 167;
18919
- var GCM_NONCE_LEN = 12;
18920
- var BODY_OFFSET = 179;
18921
- function parseVideoFrameHeader(payload) {
18922
- if (payload.length < HEADER_LEN)
18923
- return void 0;
18924
- const flags = payload.readUInt8(4);
18925
- return {
18926
- payloadLength: payload.readUInt32LE(0),
18927
- keyframe: (flags & 1) === 1,
18928
- flags,
18929
- sequence: payload.readUInt16LE(6),
18930
- width: payload.readInt16LE(10),
18931
- height: payload.readInt16LE(12),
18932
- timestamp: payload.readUInt32LE(14)
18933
- };
18934
- }
18935
- var VideoFrameDecoder = class {
18936
- eccPrivateKeyHex;
18937
- mediaKey;
18938
- /** @param eccPrivateKey the camera's 32-byte ECC private key (P-256 scalar). */
18939
- constructor(eccPrivateKey) {
18940
- if (eccPrivateKey.length !== 32)
18941
- throw new Error("eccPrivateKey must be 32 bytes");
18942
- this.eccPrivateKeyHex = eccPrivateKey.toString("hex");
18943
- }
18944
- /** The media key recovered from the most recent keyframe, if any. */
18945
- get currentMediaKey() {
18946
- return this.mediaKey ? Buffer.from(this.mediaKey) : void 0;
18947
- }
18948
- /**
18949
- * Recover the per-stream media key from a keyframe's 129-byte ECIES envelope: ECIES unwrap
18950
- * (ECDH → eufyKDF → AES-128-CBC, with the trailing HMAC verified) → 32-byte AES-256-GCM key.
18951
- */
18952
- unwrapMediaKey(envelope) {
18953
- const key = eciesUnwrap(envelope, this.eccPrivateKeyHex, { verifyHmac: true, pkcs7: true });
18954
- return key && key.length === 32 ? key : void 0;
18955
- }
18956
- /**
18957
- * Decode one video frame. Returns the keyframe flag and decrypted H.264 bytes, or `undefined`
18958
- * if the frame is malformed, the envelope/key is wrong, or GCM authentication fails.
18959
- */
18960
- decodeFrame(payload) {
18961
- const header = parseVideoFrameHeader(payload);
18962
- if (!header)
18963
- return void 0;
18964
- if (header.keyframe) {
18965
- if (payload.length < ENVELOPE_OFFSET + ENVELOPE_LEN)
18966
- return void 0;
18967
- const envelope = payload.subarray(ENVELOPE_OFFSET, ENVELOPE_OFFSET + ENVELOPE_LEN);
18968
- const key = this.unwrapMediaKey(envelope);
18969
- if (!key)
18970
- return void 0;
18971
- this.mediaKey = key;
18972
- }
18973
- const mediaKey = this.mediaKey;
18974
- if (!mediaKey)
18975
- return void 0;
18976
- if (payload.length < BODY_OFFSET)
18977
- return void 0;
18978
- const tag2 = payload.subarray(GCM_TAG_OFFSET, GCM_TAG_OFFSET + GCM_TAG_LEN);
18979
- const nonce = payload.subarray(GCM_NONCE_OFFSET, GCM_NONCE_OFFSET + GCM_NONCE_LEN);
18980
- const body = payload.subarray(BODY_OFFSET);
18981
- try {
18982
- const dec2 = createDecipheriv6("aes-256-gcm", mediaKey, nonce);
18983
- dec2.setAAD(VIDEO_GCM_AAD);
18984
- dec2.setAuthTag(tag2);
18985
- const head = dec2.update(body);
18986
- const tail = dec2.final();
18987
- const h264 = tail.length ? Buffer.concat([head, tail]) : head;
18988
- return { keyframe: header.keyframe, h264, width: header.width, height: header.height };
18989
- } catch {
18990
- return void 0;
18991
- }
18992
- }
18993
- };
18994
- var STATION_CHUNK_BYTES = 64e3;
18995
- var AccessUnitAssembler = class {
18996
- onDropped;
18997
- chunkBytes;
18998
- open;
18999
- droppedUnits = 0;
19000
- /**
19001
- * @param onDropped called for each incomplete unit discarded, with what had arrived, how many frames it
19002
- * arrived in, and how many this assembler has dropped in total — a running count, so no caller keeps
19003
- * its own.
19004
- * @param chunkBytes the payload size the station fills a frame to before continuing a unit in the next. A
19005
- * unit is held while its latest frame declares exactly this. {@link STATION_CHUNK_BYTES} when omitted.
19006
- */
19007
- constructor(onDropped, chunkBytes = STATION_CHUNK_BYTES) {
19008
- this.onDropped = onDropped;
19009
- this.chunkBytes = chunkBytes;
19010
- }
19011
- /**
19012
- * Feed one raw `CMD_VIDEO_FRAME` payload; returns the access units it completed — none while a unit is
19013
- * still being filled, one in the ordinary case.
19014
- *
19015
- * `decode` extracts the payload of a frame, whatever the stream's encryption: it is called for EVERY
19016
- * frame, including a continuation, because a continuation carries its own wrapped key ahead of its share
19017
- * of the payload (measured: 129 bytes beyond what its header declares, exactly as a unit's first frame
19018
- * carries). A continuation without its matching open unit has no Annex-B start code and cannot be emitted
19019
- * as a complete unit, even if its repeated header says keyframe.
19020
- */
19021
- push(payload, decode) {
19022
- const header = parseVideoFrameHeader(payload);
19023
- const body = header && decode(payload);
19024
- if (!header || !body?.length) {
19025
- this.discard();
19026
- return [];
19027
- }
19028
- const full = header.payloadLength === this.chunkBytes;
19029
- const open2 = this.open;
19030
- if (open2 && sameUnit(open2.header, header) && !beginsAccessUnit(body)) {
19031
- open2.chunks.push(body);
19032
- open2.carried += body.length;
19033
- if (full)
19034
- return [];
19035
- this.open = void 0;
19036
- return [unitOf(open2.header, Buffer.concat(open2.chunks))];
19037
- }
19038
- this.discard();
19039
- if (!beginsAccessUnit(body))
19040
- return [];
19041
- if (full) {
19042
- this.open = { header, chunks: [body], carried: body.length };
19043
- return [];
19044
- }
19045
- return [unitOf(header, body)];
19046
- }
19047
- /** Forget an incomplete unit, counting and reporting it. */
19048
- discard() {
19049
- const open2 = this.open;
19050
- if (!open2)
19051
- return;
19052
- this.open = void 0;
19053
- this.droppedUnits++;
19054
- this.onDropped?.({ carried: open2.carried, chunks: open2.chunks.length, count: this.droppedUnits });
19055
- }
19056
- };
19057
- var SPLIT_UNIT_STAMP_SPREAD_MS = 100;
19058
- function sameUnit(open2, next) {
19059
- return next.sequence === open2.sequence && next.timestamp - open2.timestamp >>> 0 <= SPLIT_UNIT_STAMP_SPREAD_MS;
19060
- }
19061
- function beginsAccessUnit(body) {
19062
- if (body.length < 4 || body[0] !== 0 || body[1] !== 0)
19063
- return false;
19064
- return body[2] === 1 || body[2] === 0 && body[3] === 1;
19065
- }
19066
- function unitOf(header, data) {
19067
- return {
19068
- sourceTimestamp: header.timestamp,
19069
- keyframe: header.keyframe,
19070
- width: header.width,
19071
- height: header.height,
19072
- data
19073
- };
19074
- }
19075
18952
 
19076
18953
  // dist/transport/p2p/annexb.js
19077
18954
  var ANNEXB_START = Buffer.from([0, 0, 0, 1]);
@@ -19389,6 +19266,172 @@ function h265Geometry(sps) {
19389
19266
  };
19390
19267
  }
19391
19268
 
19269
+ // dist/transport/p2p/video.js
19270
+ var VIDEO_GCM_AAD = Buffer.from("eufy security");
19271
+ var HEADER_LEN = 22;
19272
+ var ENVELOPE_OFFSET = 22;
19273
+ var ENVELOPE_LEN = 129;
19274
+ var GCM_TAG_OFFSET = 151;
19275
+ var GCM_TAG_LEN = 16;
19276
+ var GCM_NONCE_OFFSET = 167;
19277
+ var GCM_NONCE_LEN = 12;
19278
+ var BODY_OFFSET = 179;
19279
+ function parseVideoFrameHeader(payload) {
19280
+ if (payload.length < HEADER_LEN)
19281
+ return void 0;
19282
+ const flags = payload.readUInt8(4);
19283
+ return {
19284
+ payloadLength: payload.readUInt32LE(0),
19285
+ keyframe: (flags & 1) === 1,
19286
+ flags,
19287
+ sequence: payload.readUInt16LE(6),
19288
+ width: payload.readInt16LE(10),
19289
+ height: payload.readInt16LE(12),
19290
+ timestamp: payload.readUInt32LE(14)
19291
+ };
19292
+ }
19293
+ var VideoFrameDecoder = class {
19294
+ eccPrivateKeyHex;
19295
+ mediaKey;
19296
+ /** @param eccPrivateKey the camera's 32-byte ECC private key (P-256 scalar). */
19297
+ constructor(eccPrivateKey) {
19298
+ if (eccPrivateKey.length !== 32)
19299
+ throw new Error("eccPrivateKey must be 32 bytes");
19300
+ this.eccPrivateKeyHex = eccPrivateKey.toString("hex");
19301
+ }
19302
+ /** The media key recovered from the most recent keyframe, if any. */
19303
+ get currentMediaKey() {
19304
+ return this.mediaKey ? Buffer.from(this.mediaKey) : void 0;
19305
+ }
19306
+ /**
19307
+ * Recover the per-stream media key from a keyframe's 129-byte ECIES envelope: ECIES unwrap
19308
+ * (ECDH → eufyKDF → AES-128-CBC, with the trailing HMAC verified) → 32-byte AES-256-GCM key.
19309
+ */
19310
+ unwrapMediaKey(envelope) {
19311
+ const key = eciesUnwrap(envelope, this.eccPrivateKeyHex, { verifyHmac: true, pkcs7: true });
19312
+ return key && key.length === 32 ? key : void 0;
19313
+ }
19314
+ /**
19315
+ * Decode one video frame. Returns the keyframe flag and decrypted H.264 bytes, or `undefined`
19316
+ * if the frame is malformed, the envelope/key is wrong, or GCM authentication fails.
19317
+ */
19318
+ decodeFrame(payload) {
19319
+ const header = parseVideoFrameHeader(payload);
19320
+ if (!header)
19321
+ return void 0;
19322
+ if (header.keyframe) {
19323
+ if (payload.length < ENVELOPE_OFFSET + ENVELOPE_LEN)
19324
+ return void 0;
19325
+ const envelope = payload.subarray(ENVELOPE_OFFSET, ENVELOPE_OFFSET + ENVELOPE_LEN);
19326
+ const key = this.unwrapMediaKey(envelope);
19327
+ if (!key)
19328
+ return void 0;
19329
+ this.mediaKey = key;
19330
+ }
19331
+ const mediaKey = this.mediaKey;
19332
+ if (!mediaKey)
19333
+ return void 0;
19334
+ if (payload.length < BODY_OFFSET)
19335
+ return void 0;
19336
+ const tag2 = payload.subarray(GCM_TAG_OFFSET, GCM_TAG_OFFSET + GCM_TAG_LEN);
19337
+ const nonce = payload.subarray(GCM_NONCE_OFFSET, GCM_NONCE_OFFSET + GCM_NONCE_LEN);
19338
+ const body = payload.subarray(BODY_OFFSET);
19339
+ try {
19340
+ const dec2 = createDecipheriv6("aes-256-gcm", mediaKey, nonce);
19341
+ dec2.setAAD(VIDEO_GCM_AAD);
19342
+ dec2.setAuthTag(tag2);
19343
+ const head = dec2.update(body);
19344
+ const tail = dec2.final();
19345
+ const h264 = tail.length ? Buffer.concat([head, tail]) : head;
19346
+ return { keyframe: header.keyframe, h264, width: header.width, height: header.height };
19347
+ } catch {
19348
+ return void 0;
19349
+ }
19350
+ }
19351
+ };
19352
+ var STATION_CHUNK_BYTES = 64e3;
19353
+ var AccessUnitAssembler = class {
19354
+ onDropped;
19355
+ chunkBytes;
19356
+ open;
19357
+ droppedUnits = 0;
19358
+ /**
19359
+ * @param onDropped called for each incomplete unit discarded, with what had arrived, how many frames it
19360
+ * arrived in, and how many this assembler has dropped in total — a running count, so no caller keeps
19361
+ * its own.
19362
+ * @param chunkBytes the payload size the station fills a frame to before continuing a unit in the next. A
19363
+ * unit is held while its latest frame declares exactly this. {@link STATION_CHUNK_BYTES} when omitted.
19364
+ */
19365
+ constructor(onDropped, chunkBytes = STATION_CHUNK_BYTES) {
19366
+ this.onDropped = onDropped;
19367
+ this.chunkBytes = chunkBytes;
19368
+ }
19369
+ /**
19370
+ * Feed one raw `CMD_VIDEO_FRAME` payload; returns the access units it completed — none while a unit is
19371
+ * still being filled, one in the ordinary case.
19372
+ *
19373
+ * `decode` extracts the payload of a frame, whatever the stream's encryption: it is called for EVERY
19374
+ * frame, including a continuation, because a continuation carries its own wrapped key ahead of its share
19375
+ * of the payload (measured: 129 bytes beyond what its header declares, exactly as a unit's first frame
19376
+ * carries). A continuation without its matching open unit has no Annex-B start code and cannot be emitted
19377
+ * as a complete unit, even if its repeated header says keyframe.
19378
+ */
19379
+ push(payload, decode) {
19380
+ const header = parseVideoFrameHeader(payload);
19381
+ const body = header && decode(payload);
19382
+ if (!header || !body?.length) {
19383
+ this.discard();
19384
+ return [];
19385
+ }
19386
+ const full = header.payloadLength === this.chunkBytes;
19387
+ const open2 = this.open;
19388
+ if (open2 && sameUnit(open2.header, header) && !beginsAccessUnit(body)) {
19389
+ open2.chunks.push(body);
19390
+ open2.carried += body.length;
19391
+ if (full)
19392
+ return [];
19393
+ this.open = void 0;
19394
+ return [unitOf(open2.header, Buffer.concat(open2.chunks))];
19395
+ }
19396
+ this.discard();
19397
+ if (!beginsAccessUnit(body))
19398
+ return [];
19399
+ if (full) {
19400
+ this.open = { header, chunks: [body], carried: body.length };
19401
+ return [];
19402
+ }
19403
+ return [unitOf(header, body)];
19404
+ }
19405
+ /** Forget an incomplete unit, counting and reporting it. */
19406
+ discard() {
19407
+ const open2 = this.open;
19408
+ if (!open2)
19409
+ return;
19410
+ this.open = void 0;
19411
+ this.droppedUnits++;
19412
+ this.onDropped?.({ carried: open2.carried, chunks: open2.chunks.length, count: this.droppedUnits });
19413
+ }
19414
+ };
19415
+ var SPLIT_UNIT_STAMP_SPREAD_MS = 100;
19416
+ function sameUnit(open2, next) {
19417
+ return next.sequence === open2.sequence && next.timestamp - open2.timestamp >>> 0 <= SPLIT_UNIT_STAMP_SPREAD_MS;
19418
+ }
19419
+ function beginsAccessUnit(body) {
19420
+ if (body.length < 4 || body[0] !== 0 || body[1] !== 0)
19421
+ return false;
19422
+ return body[2] === 1 || body[2] === 0 && body[3] === 1;
19423
+ }
19424
+ function unitOf(header, data) {
19425
+ const codec = header.keyframe ? void 0 : sniffAnnexbCodec(data);
19426
+ return {
19427
+ sourceTimestamp: header.timestamp,
19428
+ keyframe: header.keyframe || codec !== void 0 && hasIdr(data, codec),
19429
+ width: header.width,
19430
+ height: header.height,
19431
+ data
19432
+ };
19433
+ }
19434
+
19392
19435
  // dist/transport/p2p/audio-frame.js
19393
19436
  import { createDecipheriv as createDecipheriv7 } from "node:crypto";
19394
19437
  var CMD_AUDIO_FRAME2 = 1301;
@@ -22419,6 +22462,7 @@ var RTSP_URL_READ_TIMEOUT_MS = 12e3;
22419
22462
  var TALKBACK_CODEC_WAIT_MS = 3e3;
22420
22463
  var OWN_LIVE_SESSION_MODELS = /* @__PURE__ */ new Set(["T8030"]);
22421
22464
  var ELD_TALKBACK_MODELS = /* @__PURE__ */ new Set(["T8410C"]);
22465
+ var CLEAR_FF09_MODELS = /* @__PURE__ */ new Set(["T8506"]);
22422
22466
  function speakerCodec(consumer, talk, timeoutMs) {
22423
22467
  return new Promise((resolve2) => {
22424
22468
  let timer;
@@ -23764,6 +23808,9 @@ var P2PCommandRouter = class _P2PCommandRouter {
23764
23808
  * sources on the replaced session are discarded before closing it (see {@link tearDownStation}).
23765
23809
  * Replaced at most once per resolution, so a station whose replacement is silent too is returned rather
23766
23810
  * than closed again.
23811
+ *
23812
+ * `clearFallback` hands over a session whose station {@link P2PSession.refusedLevel2} instead of throwing, for
23813
+ * a standalone device of a model in {@link CLEAR_FF09_MODELS}.
23767
23814
  */
23768
23815
  async resolveSession(sn, opts = {}, rebuilt = false) {
23769
23816
  const dev = await this.deviceFor(sn);
@@ -23815,8 +23862,10 @@ var P2PCommandRouter = class _P2PCommandRouter {
23815
23862
  if (!ready && session.repromptLevel2Key()) {
23816
23863
  ready = await abortable(session.awaitLevel2Key(LEVEL2_GRACE_MS, "call", sender), opts.signal);
23817
23864
  }
23818
- if (!ready)
23865
+ const sendsClear = opts.clearFallback === true && !homeBaseAttached && CLEAR_FF09_MODELS.has(this.recordFor(sn)?.model ?? "") && session.refusedLevel2;
23866
+ if (!ready && !sendsClear)
23819
23867
  throw new StationKeyUnavailableError(parentSn);
23868
+ return { session, parentSn, channel, accountId, homeBaseAttached, sendsClear };
23820
23869
  }
23821
23870
  return { session, parentSn, channel, accountId, homeBaseAttached };
23822
23871
  }
@@ -23926,7 +23975,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
23926
23975
  });
23927
23976
  return;
23928
23977
  }
23929
- await this.replayLevel2Send(sn, `set-payload cmd ${cmd}`, ({ session, accountId }) => session.sendControlLevel2(cmd, channel, accountId, payload, mValue3 ?? cmd), resolved ?? await this.resolveSession(sn, { waitLevel2: true, level2Sender }));
23978
+ await this.replayLevel2Send(sn, `set-payload cmd ${cmd}`, ({ session, accountId, sendsClear }) => session.sendControlLevel2(cmd, channel, accountId, payload, mValue3 ?? cmd, sendsClear), resolved ?? await this.resolveSession(sn, { waitLevel2: true, level2Sender }));
23930
23979
  }
23931
23980
  /**
23932
23981
  * **`keyed-payload-actuate` intent** — the classic Wi-Fi lock's actuation: the sealed fields from
@@ -24093,7 +24142,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
24093
24142
  * bytes and never the routing channel — that's re-resolved here from the device record.
24094
24143
  */
24095
24144
  async sendFf09Actuate(sn, cmd) {
24096
- const resolved = await this.resolveSession(sn, { waitLevel2: true });
24145
+ const resolved = await this.resolveSession(sn, { waitLevel2: true, clearFallback: true });
24097
24146
  const frame = buildFf09Frame({
24098
24147
  engage: cmd.engage,
24099
24148
  adminUserId: cmd.adminUserId,