@onekeyfe/hwk-desktop-noble-ble 1.2.3-alpha.4 → 1.2.5-alpha.0

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.
@@ -36,4 +36,4 @@ export {
36
36
  THIRD_PARTY_BLE_DEVICE_TTL_MS,
37
37
  THIRD_PARTY_BLE_POWER_ON_TIMEOUT_MS
38
38
  };
39
- //# sourceMappingURL=chunk-RNZYQRXV.mjs.map
39
+ //# sourceMappingURL=chunk-A5AENDC2.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/constants.ts"],"sourcesContent":["/**\n * Renderer/main IPC channels shared by every third-party BLE vendor; prefixed to\n * avoid clashing with the OneKey handler (`$onekey-noble-ble-*`) in one app.\n */\nexport const THIRD_PARTY_BLE_CHANNELS = {\n scan: '$onekey-3p-ble-scan',\n stopScan: '$onekey-3p-ble-stop-scan',\n connect: '$onekey-3p-ble-connect',\n disconnect: '$onekey-3p-ble-disconnect',\n write: '$onekey-3p-ble-write',\n subscribe: '$onekey-3p-ble-subscribe',\n unsubscribe: '$onekey-3p-ble-unsubscribe',\n availability: '$onekey-3p-ble-availability',\n getDevice: '$onekey-3p-ble-get-device',\n readRssi: '$onekey-3p-ble-read-rssi',\n cancelPairing: '$onekey-3p-ble-cancel-pairing',\n /** push event: main → renderer with assembled BLE notification payload */\n notification: '$onekey-3p-ble-notification',\n /** push event: main → renderer when device disconnects unexpectedly */\n disconnected: '$onekey-3p-ble-disconnected',\n} as const;\n\nexport type ThirdPartyBleChannel =\n (typeof THIRD_PARTY_BLE_CHANNELS)[keyof typeof THIRD_PARTY_BLE_CHANNELS];\n\n/** Reconnect scan window for `connect(id)`. */\nexport const THIRD_PARTY_BLE_SCAN_DURATION_MS = 5_000;\n\n/** Continuous scan auto-stops this long after the last poll (reset, not accumulated). */\nexport const THIRD_PARTY_BLE_SCAN_IDLE_STOP_MS = 10_000;\n\n/** Drop a device from the snapshot if it hasn't re-advertised within this window. */\nexport const THIRD_PARTY_BLE_DEVICE_TTL_MS = 5_000;\n\n/** Time we'll wait for noble to reach `poweredOn` before failing init. */\nexport const THIRD_PARTY_BLE_POWER_ON_TIMEOUT_MS = 10_000;\n"],"mappings":";;;;;;;;AAIO,IAAM,2BAA2B;AAAA,EACtC,MAAM;AAAA,EACN,UAAU;AAAA,EACV,SAAS;AAAA,EACT,YAAY;AAAA,EACZ,OAAO;AAAA,EACP,WAAW;AAAA,EACX,aAAa;AAAA,EACb,cAAc;AAAA,EACd,WAAW;AAAA,EACX,UAAU;AAAA,EACV,eAAe;AAAA;AAAA,EAEf,cAAc;AAAA;AAAA,EAEd,cAAc;AAChB;AAMO,IAAM,mCAAmC;AAGzC,IAAM,oCAAoC;AAG1C,IAAM,gCAAgC;AAGtC,IAAM,sCAAsC;","names":[]}
@@ -1,10 +1,6 @@
1
1
  /**
2
- * IPC channel names exchanged between renderer and main process, shared by
3
- * every third-party vendor that talks BLE on desktop — Ledger reaches the same
4
- * handler through `desktopApi.thirdPartyBle`, it does not register its own.
5
- * Namespaced under `$onekey-3p-ble-…` to avoid clashing with the
6
- * existing OneKey BLE handler (`$onekey-noble-ble-…`) when both run in
7
- * the same Electron app.
2
+ * Renderer/main IPC channels shared by every third-party BLE vendor; prefixed to
3
+ * avoid clashing with the OneKey handler (`$onekey-noble-ble-*`) in one app.
8
4
  */
9
5
  declare const THIRD_PARTY_BLE_CHANNELS: {
10
6
  readonly scan: "$onekey-3p-ble-scan";
@@ -1,10 +1,6 @@
1
1
  /**
2
- * IPC channel names exchanged between renderer and main process, shared by
3
- * every third-party vendor that talks BLE on desktop — Ledger reaches the same
4
- * handler through `desktopApi.thirdPartyBle`, it does not register its own.
5
- * Namespaced under `$onekey-3p-ble-…` to avoid clashing with the
6
- * existing OneKey BLE handler (`$onekey-noble-ble-…`) when both run in
7
- * the same Electron app.
2
+ * Renderer/main IPC channels shared by every third-party BLE vendor; prefixed to
3
+ * avoid clashing with the OneKey handler (`$onekey-noble-ble-*`) in one app.
8
4
  */
9
5
  declare const THIRD_PARTY_BLE_CHANNELS: {
10
6
  readonly scan: "$onekey-3p-ble-scan";
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/constants.ts"],"sourcesContent":["/**\n * IPC channel names exchanged between renderer and main process, shared by\n * every third-party vendor that talks BLE on desktop — Ledger reaches the same\n * handler through `desktopApi.thirdPartyBle`, it does not register its own.\n * Namespaced under `$onekey-3p-ble-…` to avoid clashing with the\n * existing OneKey BLE handler (`$onekey-noble-ble-…`) when both run in\n * the same Electron app.\n */\nexport const THIRD_PARTY_BLE_CHANNELS = {\n scan: '$onekey-3p-ble-scan',\n stopScan: '$onekey-3p-ble-stop-scan',\n connect: '$onekey-3p-ble-connect',\n disconnect: '$onekey-3p-ble-disconnect',\n write: '$onekey-3p-ble-write',\n subscribe: '$onekey-3p-ble-subscribe',\n unsubscribe: '$onekey-3p-ble-unsubscribe',\n availability: '$onekey-3p-ble-availability',\n getDevice: '$onekey-3p-ble-get-device',\n readRssi: '$onekey-3p-ble-read-rssi',\n cancelPairing: '$onekey-3p-ble-cancel-pairing',\n /** push event: main → renderer with assembled BLE notification payload */\n notification: '$onekey-3p-ble-notification',\n /** push event: main → renderer when device disconnects unexpectedly */\n disconnected: '$onekey-3p-ble-disconnected',\n} as const;\n\nexport type ThirdPartyBleChannel =\n (typeof THIRD_PARTY_BLE_CHANNELS)[keyof typeof THIRD_PARTY_BLE_CHANNELS];\n\n/** Reconnect scan window for `connect(id)`. */\nexport const THIRD_PARTY_BLE_SCAN_DURATION_MS = 5_000;\n\n/** Continuous scan auto-stops this long after the last poll (reset, not accumulated). */\nexport const THIRD_PARTY_BLE_SCAN_IDLE_STOP_MS = 10_000;\n\n/** Drop a device from the snapshot if it hasn't re-advertised within this window. */\nexport const THIRD_PARTY_BLE_DEVICE_TTL_MS = 5_000;\n\n/** Time we'll wait for noble to reach `poweredOn` before failing init. */\nexport const THIRD_PARTY_BLE_POWER_ON_TIMEOUT_MS = 10_000;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQO,IAAM,2BAA2B;AAAA,EACtC,MAAM;AAAA,EACN,UAAU;AAAA,EACV,SAAS;AAAA,EACT,YAAY;AAAA,EACZ,OAAO;AAAA,EACP,WAAW;AAAA,EACX,aAAa;AAAA,EACb,cAAc;AAAA,EACd,WAAW;AAAA,EACX,UAAU;AAAA,EACV,eAAe;AAAA;AAAA,EAEf,cAAc;AAAA;AAAA,EAEd,cAAc;AAChB;AAMO,IAAM,mCAAmC;AAGzC,IAAM,oCAAoC;AAG1C,IAAM,gCAAgC;AAGtC,IAAM,sCAAsC;","names":[]}
1
+ {"version":3,"sources":["../src/constants.ts"],"sourcesContent":["/**\n * Renderer/main IPC channels shared by every third-party BLE vendor; prefixed to\n * avoid clashing with the OneKey handler (`$onekey-noble-ble-*`) in one app.\n */\nexport const THIRD_PARTY_BLE_CHANNELS = {\n scan: '$onekey-3p-ble-scan',\n stopScan: '$onekey-3p-ble-stop-scan',\n connect: '$onekey-3p-ble-connect',\n disconnect: '$onekey-3p-ble-disconnect',\n write: '$onekey-3p-ble-write',\n subscribe: '$onekey-3p-ble-subscribe',\n unsubscribe: '$onekey-3p-ble-unsubscribe',\n availability: '$onekey-3p-ble-availability',\n getDevice: '$onekey-3p-ble-get-device',\n readRssi: '$onekey-3p-ble-read-rssi',\n cancelPairing: '$onekey-3p-ble-cancel-pairing',\n /** push event: main → renderer with assembled BLE notification payload */\n notification: '$onekey-3p-ble-notification',\n /** push event: main → renderer when device disconnects unexpectedly */\n disconnected: '$onekey-3p-ble-disconnected',\n} as const;\n\nexport type ThirdPartyBleChannel =\n (typeof THIRD_PARTY_BLE_CHANNELS)[keyof typeof THIRD_PARTY_BLE_CHANNELS];\n\n/** Reconnect scan window for `connect(id)`. */\nexport const THIRD_PARTY_BLE_SCAN_DURATION_MS = 5_000;\n\n/** Continuous scan auto-stops this long after the last poll (reset, not accumulated). */\nexport const THIRD_PARTY_BLE_SCAN_IDLE_STOP_MS = 10_000;\n\n/** Drop a device from the snapshot if it hasn't re-advertised within this window. */\nexport const THIRD_PARTY_BLE_DEVICE_TTL_MS = 5_000;\n\n/** Time we'll wait for noble to reach `poweredOn` before failing init. */\nexport const THIRD_PARTY_BLE_POWER_ON_TIMEOUT_MS = 10_000;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAIO,IAAM,2BAA2B;AAAA,EACtC,MAAM;AAAA,EACN,UAAU;AAAA,EACV,SAAS;AAAA,EACT,YAAY;AAAA,EACZ,OAAO;AAAA,EACP,WAAW;AAAA,EACX,aAAa;AAAA,EACb,cAAc;AAAA,EACd,WAAW;AAAA,EACX,UAAU;AAAA,EACV,eAAe;AAAA;AAAA,EAEf,cAAc;AAAA;AAAA,EAEd,cAAc;AAChB;AAMO,IAAM,mCAAmC;AAGzC,IAAM,oCAAoC;AAG1C,IAAM,gCAAgC;AAGtC,IAAM,sCAAsC;","names":[]}
@@ -4,7 +4,7 @@ import {
4
4
  THIRD_PARTY_BLE_POWER_ON_TIMEOUT_MS,
5
5
  THIRD_PARTY_BLE_SCAN_DURATION_MS,
6
6
  THIRD_PARTY_BLE_SCAN_IDLE_STOP_MS
7
- } from "./chunk-RNZYQRXV.mjs";
7
+ } from "./chunk-A5AENDC2.mjs";
8
8
  export {
9
9
  THIRD_PARTY_BLE_CHANNELS,
10
10
  THIRD_PARTY_BLE_DEVICE_TTL_MS,
package/dist/index.d.mts CHANGED
@@ -12,12 +12,7 @@ type BleDebugLogger = (entry: BleDebugLogEntry) => void;
12
12
  /** Replace sensitive values at any depth; everything else is forwarded verbatim. */
13
13
  declare function redactBleDebugLogData(data?: Record<string, unknown>): Record<string, unknown> | undefined;
14
14
 
15
- /**
16
- * Shape of the API the renderer process talks to. In a real Electron app
17
- * this is normally exposed via `contextBridge.exposeInMainWorld('desktopApi',
18
- * { trezorBle: ... })`, but the transport accepts the bridge directly so
19
- * non-Electron hosts (and unit tests) can plug in their own implementation.
20
- */
15
+ /** Device info the main process returns to the renderer over IPC. */
21
16
  interface ThirdPartyBleDeviceInfo {
22
17
  /** Stable id (noble peripheral.id) used as connectId. */
23
18
  id: string;
@@ -53,7 +48,7 @@ interface ThirdPartyBleAvailability {
53
48
  type ThirdPartyBleScanOptions = ElectronBleScanOptions;
54
49
  interface ThirdPartyBleApi {
55
50
  scan(options?: ThirdPartyBleScanOptions): Promise<ThirdPartyBleDeviceInfo[]>;
56
- stopScan(): Promise<void>;
51
+ stopScan(vendor?: string): Promise<void>;
57
52
  connect(id: string, options: ElectronBleConnectOptions): Promise<{
58
53
  id: string;
59
54
  name?: string;
@@ -62,19 +57,18 @@ interface ThirdPartyBleApi {
62
57
  /** Subscribe to the BLE notify characteristic for `id`. */
63
58
  subscribe(id: string): Promise<void>;
64
59
  unsubscribe(id: string): Promise<void>;
65
- /**
66
- * Write an already-framed payload. The main process applies the framing rule
67
- * recorded at connect time — splitting and zero-padding for `padded`, or
68
- * writing the buffer as given for `raw`.
69
- */
60
+ /** Write a framed payload; main chunks and zero-pads `padded`, writes `raw` as-is. */
70
61
  write(id: string, hexData: string): Promise<void>;
71
62
  checkAvailability(): Promise<ThirdPartyBleAvailability>;
72
63
  /** Look up a previously-scanned device by id without re-scanning. */
73
64
  getDevice(id: string): Promise<ThirdPartyBleDeviceInfo | null>;
74
65
  /** Read current RSSI (dBm) of a *connected* peripheral. */
75
66
  readRssi(id: string): Promise<number>;
76
- /** Stop scan + disconnect every in-flight connection. */
77
- cancelPairing(): Promise<void>;
67
+ /** Cancel only the named vendor/device; omitting the argument cancels everything. */
68
+ cancelPairing(options?: {
69
+ vendor: string;
70
+ id?: string;
71
+ }): Promise<void>;
78
72
  /** Register a listener for incoming BLE notifications. Returns an unsubscribe fn. */
79
73
  onNotification(handler: (id: string, hexData: string) => void): () => void;
80
74
  /** Register a listener for unexpected disconnects. Returns an unsubscribe fn. */
@@ -93,11 +87,8 @@ interface NobleLike {
93
87
  stopScanningAsync(): Promise<void>;
94
88
  stop?(): void;
95
89
  /**
96
- * Connect by id/address with NO scan. Both native backends support this and
97
- * emit a `discover` for the peripheral as a side effect: Windows synthesizes
98
- * one for an unknown address (`BLEManager::Connect`, lib/win/src/ble_manager.cc)
99
- * and macOS resolves it via `retrievePeripheralsWithIdentifiers`
100
- * (lib/mac/src/ble_manager.mm). Optional so a stub noble can omit it.
90
+ * Connect by id with no scan; both backends emit `discover` as a side effect
91
+ * (win `BLEManager::Connect`, mac `retrievePeripheralsWithIdentifiers`).
101
92
  */
102
93
  connectAsync?(idOrAddress: string): Promise<NoblePeripheralLike | undefined>;
103
94
  cancelConnect?(idOrAddress: string): void;
@@ -149,12 +140,7 @@ interface NobleBleHandlerOptions {
149
140
  connectTimeoutMs?: number;
150
141
  logger?: BleDebugLogger;
151
142
  }
152
- /**
153
- * Core BLE logic, decoupled from Electron's IPC layer so it can be unit
154
- * tested with a fake noble. Mirrors the OneKey `noble-ble-handler.ts`
155
- * pattern (a single class that owns the peripheral cache + disconnect
156
- * callbacks), but trimmed to the minimum surface we expose.
157
- */
143
+ /** Noble BLE logic decoupled from Electron IPC, so tests can use a fake noble. */
158
144
  declare class NobleBleHandler {
159
145
  private _noble;
160
146
  private readonly _factory;
@@ -165,6 +151,8 @@ declare class NobleBleHandler {
165
151
  private readonly _connected;
166
152
  private _discoverHandler?;
167
153
  private _scanning;
154
+ private readonly _scanOwners;
155
+ private _scanTransition;
168
156
  private _idleStopTimer?;
169
157
  private _onNotification?;
170
158
  private _onDeviceDisconnected?;
@@ -178,121 +166,57 @@ declare class NobleBleHandler {
178
166
  private readonly _connectAttempts;
179
167
  private _nativeReleased;
180
168
  private _lastNobleRecoverAt?;
181
- /** The connect currently in flight, so cancelPairing can abandon it. */
182
- private _activeConnect?;
183
169
  constructor(options?: NobleBleHandlerOptions);
184
170
  setNotificationListener(handler: (id: string, hexData: string) => void): void;
185
171
  setDisconnectedListener(handler: (id: string) => void): void;
186
172
  init(): Promise<void>;
187
173
  checkAvailability(): Promise<ThirdPartyBleAvailability>;
188
174
  /**
189
- * Lazy-start a continuous scan and return the current snapshot immediately.
190
- *
191
- * Scans UNFILTERED and applies the caller's match criteria in `_snapshot()` instead. A
192
- * service-UUID filter cannot be used here: noble's Windows backend applies it
193
- * per RECEIVED PACKET (`BLEManager::OnScanResult`, lib/win/src/ble_manager.cc),
194
- * and a Safe 7's ADV packet carries only its name — the service UUID lives in
195
- * the scan response, which arrives as a separate, irregularly-timed event. So
196
- * a filtered scan drops every ADV packet and the device appears to be
197
- * undiscoverable for minutes at a time while it is plainly on air. (OneKey's
198
- * own devices do advertise their service UUID, which is why the same filter is
199
- * safe in `hd-transport-electron` and was copied here by mistake.)
175
+ * Lazy-start an unfiltered scan and return the current snapshot. noble-win filters per
176
+ * packet, and a Safe 7 ADV carries only its name (UUID is in the scan response).
200
177
  */
201
178
  scan(options?: ElectronBleScanOptions): Promise<ThirdPartyBleDeviceInfo[]>;
202
179
  /**
203
- * Current in-range devices for the asking vendor, dropping any that aged past
204
- * the liveness TTL. The match test replaces the service-UUID scan filter we cannot use
205
- * (see `scan`): it matches the name from the ADV packet, or the service UUID
206
- * once a scan response has merged into the same peripheral.
207
- *
208
- * Note the TTL only prunes what the CALLER sees. `_discovered` is a cache, not
209
- * the source of truth for reachability — a device missing from here can still
210
- * be connected to by id (`_directConnect`).
180
+ * In-range devices matching the caller, minus TTL-expired ones. `_discovered`
181
+ * is only a cache; a missing device can still be reached by `_directConnect`.
211
182
  */
212
183
  private _snapshot;
213
184
  /** A noble instance with its own bindings, so a stuck one can be replaced. */
214
185
  private _createFreshNoble;
215
186
  /**
216
- * Rebuild noble when its adapter state is stuck. Re-enumerating the Windows
217
- * BLE stack (pairing, or removing the device from OS settings) can catch
218
- * noble's RadioWatcher mid-churn: it latches `unsupported` and never
219
- * re-evaluates, so every later scan fails until the process restarts. Fresh
220
- * bindings restart that watcher, which is the in-process equivalent of the
221
- * app restart that is otherwise the only cure.
187
+ * Rebuild noble when Windows BLE re-enumeration latched its RadioWatcher at
188
+ * `unsupported`; fresh bindings restart the watcher without an app restart.
222
189
  */
223
190
  private _recoverNobleIfStuck;
224
191
  private _armIdleStop;
225
192
  private _clearIdleStop;
193
+ private _setScanning;
226
194
  /** Stop scanning but keep the discovered cache (used before connect). */
227
195
  private _pauseScan;
228
196
  /** Stop scanning and forget discovered devices (idle timeout / teardown). */
229
197
  private _stopContinuousScan;
230
- /**
231
- * Stops the process-wide scan no matter which vendor asked. Scan *results*
232
- * are filtered per vendor, but the radio is not: one vendor's stopScan ends
233
- * the other's discovery too. That is safe only because the host mounts one
234
- * vendor's flow at a time; queues are per adapter, not shared. Anything
235
- * that breaks that — a background presence probe, say — needs per-vendor
236
- * refcounting here first.
237
- */
238
- stopScan(): Promise<void>;
198
+ /** A vendor releasing discovery must not stop another vendor's scan. */
199
+ stopScan(vendor?: string): Promise<void>;
239
200
  /**
240
201
  * Look up a previously-scanned device by id (no extra BLE traffic).
241
202
  * Returns null if the device hasn't been seen by a recent scan.
242
203
  */
243
204
  getDevice(id: string): ThirdPartyBleDeviceInfo | null;
244
- /**
245
- * Read the current RSSI (in dBm) for a connected peripheral. Requires
246
- * the device to be connected — noble can't read RSSI off a scan-only
247
- * peripheral. Falls back to the cached scan-time rssi when the noble
248
- * peripheral doesn't expose updateRssiAsync.
249
- */
205
+ /** Live RSSI (dBm) of a connected peripheral, else the cached scan-time value. */
250
206
  readRssi(id: string): Promise<number>;
207
+ /** Abandon matching pairing attempts without touching another owner's links. */
208
+ cancelPairing(options?: {
209
+ vendor: string;
210
+ id?: string;
211
+ }): Promise<void>;
251
212
  /**
252
- * Abort the in-flight pairing flow: abandon a connect that is still running,
253
- * stop scanning, disconnect every peripheral the host currently has open.
254
- * Caller is responsible for surfacing the cancellation to the upper UI layer.
255
- *
256
- * Abandoning the connect is what actually ends the flow. Pairing happens
257
- * inside connectAsync and the entry only reaches _connected after service
258
- * discovery, so the loop below never sees the device being paired — without
259
- * the abandon the caller waits out the full connect timeout, which is sized
260
- * to the OS pairing window and so feels like a hang.
261
- */
262
- cancelPairing(): Promise<void>;
263
- /**
264
- * Scan for a specific peripheral id and resolve THE MOMENT it's discovered,
265
- * stopping the scan immediately (don't wait out the full window). The fast
266
- * reconnect path for a stored connectId when the device IS advertising.
267
- *
268
- * This used to be the only reconnect path, on two assumptions that are both
269
- * false: that noble cannot connect by id without a scan (it can — see
270
- * `_directConnect`), and that "the device advertises continuously" (a bonded
271
- * Safe 7 does not — it holds the link and goes silent). Callers must fall
272
- * back to `_directConnect` when this returns undefined.
213
+ * Scan until `id` is discovered, releasing only its own scan ownership. A
214
+ * bonded Safe 7 goes silent, so callers fall back to `_directConnect`.
273
215
  */
274
216
  private _scanUntilFound;
275
217
  /**
276
- * Connect by id with no scan and no advertisement.
277
- *
278
- * This is the ONLY path that reaches a device which is connected but silent.
279
- * A linked peripheral stops advertising while it HOLDS A LINK (standard BLE; a Safe 7's
280
- * screen says "wait connection") — field-verified: bonding/THP handshake
281
- * alone does NOT silence it, holding the connection does. So while a link is
282
- * up, no amount of scanning will rediscover it — `_scanUntilFound` alone
283
- * dead-ends with "device not found" on a device that is sitting right there,
284
- * connected and reachable.
285
- *
286
- * noble supports this: `noble.connectAsync(id)` needs no prior `discover`,
287
- * because both native backends materialize the peripheral themselves (Windows
288
- * synthesizes one for an unknown address, macOS retrieves it by identifier)
289
- * and then emit a `discover`, which our own handler turns back into a
290
- * `_discovered` entry. OneKey's own noble handler calls this "direct
291
- * connection mode"; Trezor Suite's equivalent is asking the adapter for its
292
- * peripheral list instead of keeping a cache.
293
- *
294
- * Returns undefined (not throw) so the caller reports the normal
295
- * "device not found" rather than a confusing noble-internal error.
218
+ * Connect by id without advertisement: the only route to a linked, silent device.
219
+ * Returns undefined instead of throwing so the caller reports "device not found".
296
220
  */
297
221
  private _directConnect;
298
222
  private _safeDisconnect;
@@ -308,9 +232,8 @@ declare class NobleBleHandler {
308
232
  /** Retire a renderer's handler without stopping a process-wide native manager. */
309
233
  dispose(): Promise<void>;
310
234
  /**
311
- * Terminal native release, including instances replaced by adapter recovery.
312
- * A host sharing Noble must defer stop() until all transports have disposed,
313
- * and deduplicate instances passed to releaseNoble across those transports.
235
+ * Terminal native release, including recovered instances. A host sharing noble
236
+ * must defer stop() until every transport disposed, and dedupe the instances.
314
237
  */
315
238
  disposeForAppQuit(releaseNoble?: (instance: {
316
239
  stop?(): void;
@@ -346,11 +269,8 @@ interface ThirdPartyBleSupportHandle {
346
269
  }) => void): Promise<void>;
347
270
  }
348
271
  /**
349
- * Wire a `NobleBleHandler` to Electron's IPC so the renderer can drive BLE
350
- * via `window.desktopApi.trezorBle`. Call once from the main process after
351
- * `BrowserWindow` is ready.
352
- *
353
- * Call dispose() when retiring a renderer and disposeForAppQuit() before Node teardown.
272
+ * Wire a `NobleBleHandler` to Electron IPC once `BrowserWindow` is ready. Call dispose()
273
+ * when retiring a renderer and disposeForAppQuit() before Node teardown.
354
274
  */
355
275
  declare function initThirdPartyBleSupport(webContents: WebContentsLike, options?: InitThirdPartyBleSupportOptions): ThirdPartyBleSupportHandle;
356
276
 
package/dist/index.d.ts CHANGED
@@ -12,12 +12,7 @@ type BleDebugLogger = (entry: BleDebugLogEntry) => void;
12
12
  /** Replace sensitive values at any depth; everything else is forwarded verbatim. */
13
13
  declare function redactBleDebugLogData(data?: Record<string, unknown>): Record<string, unknown> | undefined;
14
14
 
15
- /**
16
- * Shape of the API the renderer process talks to. In a real Electron app
17
- * this is normally exposed via `contextBridge.exposeInMainWorld('desktopApi',
18
- * { trezorBle: ... })`, but the transport accepts the bridge directly so
19
- * non-Electron hosts (and unit tests) can plug in their own implementation.
20
- */
15
+ /** Device info the main process returns to the renderer over IPC. */
21
16
  interface ThirdPartyBleDeviceInfo {
22
17
  /** Stable id (noble peripheral.id) used as connectId. */
23
18
  id: string;
@@ -53,7 +48,7 @@ interface ThirdPartyBleAvailability {
53
48
  type ThirdPartyBleScanOptions = ElectronBleScanOptions;
54
49
  interface ThirdPartyBleApi {
55
50
  scan(options?: ThirdPartyBleScanOptions): Promise<ThirdPartyBleDeviceInfo[]>;
56
- stopScan(): Promise<void>;
51
+ stopScan(vendor?: string): Promise<void>;
57
52
  connect(id: string, options: ElectronBleConnectOptions): Promise<{
58
53
  id: string;
59
54
  name?: string;
@@ -62,19 +57,18 @@ interface ThirdPartyBleApi {
62
57
  /** Subscribe to the BLE notify characteristic for `id`. */
63
58
  subscribe(id: string): Promise<void>;
64
59
  unsubscribe(id: string): Promise<void>;
65
- /**
66
- * Write an already-framed payload. The main process applies the framing rule
67
- * recorded at connect time — splitting and zero-padding for `padded`, or
68
- * writing the buffer as given for `raw`.
69
- */
60
+ /** Write a framed payload; main chunks and zero-pads `padded`, writes `raw` as-is. */
70
61
  write(id: string, hexData: string): Promise<void>;
71
62
  checkAvailability(): Promise<ThirdPartyBleAvailability>;
72
63
  /** Look up a previously-scanned device by id without re-scanning. */
73
64
  getDevice(id: string): Promise<ThirdPartyBleDeviceInfo | null>;
74
65
  /** Read current RSSI (dBm) of a *connected* peripheral. */
75
66
  readRssi(id: string): Promise<number>;
76
- /** Stop scan + disconnect every in-flight connection. */
77
- cancelPairing(): Promise<void>;
67
+ /** Cancel only the named vendor/device; omitting the argument cancels everything. */
68
+ cancelPairing(options?: {
69
+ vendor: string;
70
+ id?: string;
71
+ }): Promise<void>;
78
72
  /** Register a listener for incoming BLE notifications. Returns an unsubscribe fn. */
79
73
  onNotification(handler: (id: string, hexData: string) => void): () => void;
80
74
  /** Register a listener for unexpected disconnects. Returns an unsubscribe fn. */
@@ -93,11 +87,8 @@ interface NobleLike {
93
87
  stopScanningAsync(): Promise<void>;
94
88
  stop?(): void;
95
89
  /**
96
- * Connect by id/address with NO scan. Both native backends support this and
97
- * emit a `discover` for the peripheral as a side effect: Windows synthesizes
98
- * one for an unknown address (`BLEManager::Connect`, lib/win/src/ble_manager.cc)
99
- * and macOS resolves it via `retrievePeripheralsWithIdentifiers`
100
- * (lib/mac/src/ble_manager.mm). Optional so a stub noble can omit it.
90
+ * Connect by id with no scan; both backends emit `discover` as a side effect
91
+ * (win `BLEManager::Connect`, mac `retrievePeripheralsWithIdentifiers`).
101
92
  */
102
93
  connectAsync?(idOrAddress: string): Promise<NoblePeripheralLike | undefined>;
103
94
  cancelConnect?(idOrAddress: string): void;
@@ -149,12 +140,7 @@ interface NobleBleHandlerOptions {
149
140
  connectTimeoutMs?: number;
150
141
  logger?: BleDebugLogger;
151
142
  }
152
- /**
153
- * Core BLE logic, decoupled from Electron's IPC layer so it can be unit
154
- * tested with a fake noble. Mirrors the OneKey `noble-ble-handler.ts`
155
- * pattern (a single class that owns the peripheral cache + disconnect
156
- * callbacks), but trimmed to the minimum surface we expose.
157
- */
143
+ /** Noble BLE logic decoupled from Electron IPC, so tests can use a fake noble. */
158
144
  declare class NobleBleHandler {
159
145
  private _noble;
160
146
  private readonly _factory;
@@ -165,6 +151,8 @@ declare class NobleBleHandler {
165
151
  private readonly _connected;
166
152
  private _discoverHandler?;
167
153
  private _scanning;
154
+ private readonly _scanOwners;
155
+ private _scanTransition;
168
156
  private _idleStopTimer?;
169
157
  private _onNotification?;
170
158
  private _onDeviceDisconnected?;
@@ -178,121 +166,57 @@ declare class NobleBleHandler {
178
166
  private readonly _connectAttempts;
179
167
  private _nativeReleased;
180
168
  private _lastNobleRecoverAt?;
181
- /** The connect currently in flight, so cancelPairing can abandon it. */
182
- private _activeConnect?;
183
169
  constructor(options?: NobleBleHandlerOptions);
184
170
  setNotificationListener(handler: (id: string, hexData: string) => void): void;
185
171
  setDisconnectedListener(handler: (id: string) => void): void;
186
172
  init(): Promise<void>;
187
173
  checkAvailability(): Promise<ThirdPartyBleAvailability>;
188
174
  /**
189
- * Lazy-start a continuous scan and return the current snapshot immediately.
190
- *
191
- * Scans UNFILTERED and applies the caller's match criteria in `_snapshot()` instead. A
192
- * service-UUID filter cannot be used here: noble's Windows backend applies it
193
- * per RECEIVED PACKET (`BLEManager::OnScanResult`, lib/win/src/ble_manager.cc),
194
- * and a Safe 7's ADV packet carries only its name — the service UUID lives in
195
- * the scan response, which arrives as a separate, irregularly-timed event. So
196
- * a filtered scan drops every ADV packet and the device appears to be
197
- * undiscoverable for minutes at a time while it is plainly on air. (OneKey's
198
- * own devices do advertise their service UUID, which is why the same filter is
199
- * safe in `hd-transport-electron` and was copied here by mistake.)
175
+ * Lazy-start an unfiltered scan and return the current snapshot. noble-win filters per
176
+ * packet, and a Safe 7 ADV carries only its name (UUID is in the scan response).
200
177
  */
201
178
  scan(options?: ElectronBleScanOptions): Promise<ThirdPartyBleDeviceInfo[]>;
202
179
  /**
203
- * Current in-range devices for the asking vendor, dropping any that aged past
204
- * the liveness TTL. The match test replaces the service-UUID scan filter we cannot use
205
- * (see `scan`): it matches the name from the ADV packet, or the service UUID
206
- * once a scan response has merged into the same peripheral.
207
- *
208
- * Note the TTL only prunes what the CALLER sees. `_discovered` is a cache, not
209
- * the source of truth for reachability — a device missing from here can still
210
- * be connected to by id (`_directConnect`).
180
+ * In-range devices matching the caller, minus TTL-expired ones. `_discovered`
181
+ * is only a cache; a missing device can still be reached by `_directConnect`.
211
182
  */
212
183
  private _snapshot;
213
184
  /** A noble instance with its own bindings, so a stuck one can be replaced. */
214
185
  private _createFreshNoble;
215
186
  /**
216
- * Rebuild noble when its adapter state is stuck. Re-enumerating the Windows
217
- * BLE stack (pairing, or removing the device from OS settings) can catch
218
- * noble's RadioWatcher mid-churn: it latches `unsupported` and never
219
- * re-evaluates, so every later scan fails until the process restarts. Fresh
220
- * bindings restart that watcher, which is the in-process equivalent of the
221
- * app restart that is otherwise the only cure.
187
+ * Rebuild noble when Windows BLE re-enumeration latched its RadioWatcher at
188
+ * `unsupported`; fresh bindings restart the watcher without an app restart.
222
189
  */
223
190
  private _recoverNobleIfStuck;
224
191
  private _armIdleStop;
225
192
  private _clearIdleStop;
193
+ private _setScanning;
226
194
  /** Stop scanning but keep the discovered cache (used before connect). */
227
195
  private _pauseScan;
228
196
  /** Stop scanning and forget discovered devices (idle timeout / teardown). */
229
197
  private _stopContinuousScan;
230
- /**
231
- * Stops the process-wide scan no matter which vendor asked. Scan *results*
232
- * are filtered per vendor, but the radio is not: one vendor's stopScan ends
233
- * the other's discovery too. That is safe only because the host mounts one
234
- * vendor's flow at a time; queues are per adapter, not shared. Anything
235
- * that breaks that — a background presence probe, say — needs per-vendor
236
- * refcounting here first.
237
- */
238
- stopScan(): Promise<void>;
198
+ /** A vendor releasing discovery must not stop another vendor's scan. */
199
+ stopScan(vendor?: string): Promise<void>;
239
200
  /**
240
201
  * Look up a previously-scanned device by id (no extra BLE traffic).
241
202
  * Returns null if the device hasn't been seen by a recent scan.
242
203
  */
243
204
  getDevice(id: string): ThirdPartyBleDeviceInfo | null;
244
- /**
245
- * Read the current RSSI (in dBm) for a connected peripheral. Requires
246
- * the device to be connected — noble can't read RSSI off a scan-only
247
- * peripheral. Falls back to the cached scan-time rssi when the noble
248
- * peripheral doesn't expose updateRssiAsync.
249
- */
205
+ /** Live RSSI (dBm) of a connected peripheral, else the cached scan-time value. */
250
206
  readRssi(id: string): Promise<number>;
207
+ /** Abandon matching pairing attempts without touching another owner's links. */
208
+ cancelPairing(options?: {
209
+ vendor: string;
210
+ id?: string;
211
+ }): Promise<void>;
251
212
  /**
252
- * Abort the in-flight pairing flow: abandon a connect that is still running,
253
- * stop scanning, disconnect every peripheral the host currently has open.
254
- * Caller is responsible for surfacing the cancellation to the upper UI layer.
255
- *
256
- * Abandoning the connect is what actually ends the flow. Pairing happens
257
- * inside connectAsync and the entry only reaches _connected after service
258
- * discovery, so the loop below never sees the device being paired — without
259
- * the abandon the caller waits out the full connect timeout, which is sized
260
- * to the OS pairing window and so feels like a hang.
261
- */
262
- cancelPairing(): Promise<void>;
263
- /**
264
- * Scan for a specific peripheral id and resolve THE MOMENT it's discovered,
265
- * stopping the scan immediately (don't wait out the full window). The fast
266
- * reconnect path for a stored connectId when the device IS advertising.
267
- *
268
- * This used to be the only reconnect path, on two assumptions that are both
269
- * false: that noble cannot connect by id without a scan (it can — see
270
- * `_directConnect`), and that "the device advertises continuously" (a bonded
271
- * Safe 7 does not — it holds the link and goes silent). Callers must fall
272
- * back to `_directConnect` when this returns undefined.
213
+ * Scan until `id` is discovered, releasing only its own scan ownership. A
214
+ * bonded Safe 7 goes silent, so callers fall back to `_directConnect`.
273
215
  */
274
216
  private _scanUntilFound;
275
217
  /**
276
- * Connect by id with no scan and no advertisement.
277
- *
278
- * This is the ONLY path that reaches a device which is connected but silent.
279
- * A linked peripheral stops advertising while it HOLDS A LINK (standard BLE; a Safe 7's
280
- * screen says "wait connection") — field-verified: bonding/THP handshake
281
- * alone does NOT silence it, holding the connection does. So while a link is
282
- * up, no amount of scanning will rediscover it — `_scanUntilFound` alone
283
- * dead-ends with "device not found" on a device that is sitting right there,
284
- * connected and reachable.
285
- *
286
- * noble supports this: `noble.connectAsync(id)` needs no prior `discover`,
287
- * because both native backends materialize the peripheral themselves (Windows
288
- * synthesizes one for an unknown address, macOS retrieves it by identifier)
289
- * and then emit a `discover`, which our own handler turns back into a
290
- * `_discovered` entry. OneKey's own noble handler calls this "direct
291
- * connection mode"; Trezor Suite's equivalent is asking the adapter for its
292
- * peripheral list instead of keeping a cache.
293
- *
294
- * Returns undefined (not throw) so the caller reports the normal
295
- * "device not found" rather than a confusing noble-internal error.
218
+ * Connect by id without advertisement: the only route to a linked, silent device.
219
+ * Returns undefined instead of throwing so the caller reports "device not found".
296
220
  */
297
221
  private _directConnect;
298
222
  private _safeDisconnect;
@@ -308,9 +232,8 @@ declare class NobleBleHandler {
308
232
  /** Retire a renderer's handler without stopping a process-wide native manager. */
309
233
  dispose(): Promise<void>;
310
234
  /**
311
- * Terminal native release, including instances replaced by adapter recovery.
312
- * A host sharing Noble must defer stop() until all transports have disposed,
313
- * and deduplicate instances passed to releaseNoble across those transports.
235
+ * Terminal native release, including recovered instances. A host sharing noble
236
+ * must defer stop() until every transport disposed, and dedupe the instances.
314
237
  */
315
238
  disposeForAppQuit(releaseNoble?: (instance: {
316
239
  stop?(): void;
@@ -346,11 +269,8 @@ interface ThirdPartyBleSupportHandle {
346
269
  }) => void): Promise<void>;
347
270
  }
348
271
  /**
349
- * Wire a `NobleBleHandler` to Electron's IPC so the renderer can drive BLE
350
- * via `window.desktopApi.trezorBle`. Call once from the main process after
351
- * `BrowserWindow` is ready.
352
- *
353
- * Call dispose() when retiring a renderer and disposeForAppQuit() before Node teardown.
272
+ * Wire a `NobleBleHandler` to Electron IPC once `BrowserWindow` is ready. Call dispose()
273
+ * when retiring a renderer and disposeForAppQuit() before Node teardown.
354
274
  */
355
275
  declare function initThirdPartyBleSupport(webContents: WebContentsLike, options?: InitThirdPartyBleSupportOptions): ThirdPartyBleSupportHandle;
356
276