@stoprocent/noble
Advanced tools
+23
-3
@@ -48,6 +48,18 @@ /// <reference types="node" /> | ||
| connectAsync(idOrAddress: PeripheralIdOrAddress, options?: ConnectOptions): Promise<Peripheral>; | ||
| /** | ||
| * Pair with a peripheral. Windows only; requires an already-connected | ||
| * peripheral and supports only the ConfirmOnly ("Just Works") ceremony. | ||
| * Rejects with 'Pairing is not supported on this platform' elsewhere. | ||
| */ | ||
| pairAsync(idOrAddress: PeripheralIdOrAddress): Promise<void>; | ||
| startScanning(serviceUUIDs?: string[], allowDuplicates?: boolean, callback?: (error?: Error) => void): void; | ||
| stopScanning(callback?: () => void): void; | ||
| connect(idOrAddress: PeripheralIdOrAddress, options?: ConnectOptions, callback?: (error: Error | undefined, peripheral: Peripheral) => void): void; | ||
| /** | ||
| * Pair with a peripheral. Windows only; requires an already-connected | ||
| * peripheral and supports only the ConfirmOnly ("Just Works") ceremony. | ||
| * Calls back with 'Pairing is not supported on this platform' elsewhere. | ||
| */ | ||
| pair(idOrAddress: PeripheralIdOrAddress, callback?: (error: Error | null) => void): void; | ||
| cancelConnect(idOrAddress: PeripheralIdOrAddress, options?: object): void; | ||
@@ -62,4 +74,5 @@ reset(): void; | ||
| on(event: "discover", listener: (peripheral: Peripheral) => void): this; | ||
| on(event: "pair", listener: (peripheral: Peripheral, error: Error | null) => void): this; | ||
| on(event: string, listener: Function): this; | ||
| once(event: "stateChange", listener: (state: AdapterState) => void): this; | ||
@@ -69,2 +82,3 @@ once(event: "scanStart", listener: () => void): this; | ||
| once(event: "discover", listener: (peripheral: Peripheral) => void): this; | ||
| once(event: "pair", listener: (peripheral: Peripheral, error: Error | null) => void): this; | ||
| once(event: string, listener: Function): this; | ||
@@ -111,2 +125,4 @@ | ||
| connectAsync(): Promise<void>; | ||
| /** Windows only; ConfirmOnly ("Just Works") ceremony only. */ | ||
| pairAsync(): Promise<void>; | ||
| disconnectAsync(): Promise<void>; | ||
@@ -122,2 +138,4 @@ updateRssiAsync(): Promise<number>; | ||
| connect(callback?: (error: Error | undefined) => void): void; | ||
| /** Windows only; ConfirmOnly ("Just Works") ceremony only. */ | ||
| pair(callback?: (error: Error | null) => void): void; | ||
| disconnect(callback?: () => void): void; | ||
@@ -136,2 +154,3 @@ updateRssi(callback?: (error: Error | undefined, rssi: number) => void): void; | ||
| on(event: "connect", listener: (error: Error | undefined) => void): this; | ||
| on(event: "pair", listener: (error: Error | null) => void): this; | ||
| on(event: "disconnect", listener: (reason: DisconnectReason) => void): this; | ||
@@ -142,4 +161,5 @@ on(event: "rssiUpdate", listener: (rssi: number) => void): this; | ||
| on(event: string, listener: Function): this; | ||
| once(event: "connect", listener: (error: Error | undefined) => void): this; | ||
| once(event: "pair", listener: (error: Error | null) => void): this; | ||
| once(event: "disconnect", listener: (reason: DisconnectReason) => void): this; | ||
@@ -146,0 +166,0 @@ once(event: "rssiUpdate", listener: (rssi: number) => void): this; |
@@ -20,2 +20,3 @@ #pragma once | ||
| void Connected(const std::string& uuid, const std::string& error = ""); | ||
| void Paired(const std::string& uuid, bool paired, const std::string& error = ""); | ||
| void Disconnected(const std::string& uuid); | ||
@@ -22,0 +23,0 @@ void MTU(const std::string& uuid, int mtu); |
@@ -157,2 +157,10 @@ #include "Emit.h" | ||
| void Emit::Paired(const std::string& uuid, bool paired, const std::string& error) | ||
| { | ||
| mCallback->call([uuid, paired, error](Napi::Env env, std::vector<napi_value>& args) { | ||
| // emit('pair', deviceUuid, paired, error) | ||
| args = { _s("pair"), _u(uuid), _b(paired), error.empty() ? env.Null() : _e(error) }; | ||
| }); | ||
| } | ||
| void Emit::Disconnected(const std::string& uuid) | ||
@@ -159,0 +167,0 @@ { |
+52
-0
@@ -65,2 +65,3 @@ const debug = require('debug')('noble'); | ||
| this._bindings.on('connect', this._onConnect.bind(this)); | ||
| this._bindings.on('pair', this._onPair.bind(this)); | ||
| this._bindings.on('disconnect', this._onDisconnect.bind(this)); | ||
@@ -424,2 +425,53 @@ this._bindings.on('rssiUpdate', this._onRssiUpdate.bind(this)); | ||
| pair (idOrAddress, callback) { | ||
| const identifier = this._getPeripheralId(idOrAddress); | ||
| if (typeof callback === 'function') { | ||
| this.onceExclusive(`pair:${identifier}`, error => callback(error)); | ||
| } | ||
| if (typeof this._bindings.pair !== 'function') { | ||
| // Only the Windows bindings implement pairing today. Surface a | ||
| // deterministic error via the same event the callback waits on, so | ||
| // callers don't hang waiting for a 'pair' completion that will | ||
| // never come. | ||
| const err = new Error('Pairing is not supported on this platform'); | ||
| const peripheral = this._peripherals.get(identifier); | ||
| if (peripheral) { | ||
| // Mirror _onPair: surface the outcome on the public Noble 'pair' | ||
| // event (peripheral, error) as well as on the peripheral itself, so a | ||
| // noble-level listener is notified of the unsupported-platform failure. | ||
| this.emit('pair', peripheral, err); | ||
| peripheral.emit('pair', err); | ||
| } | ||
| this.emit(`pair:${identifier}`, err); | ||
| return; | ||
| } | ||
| this._bindings.pair(identifier); | ||
| } | ||
| async pairAsync (idOrAddress) { | ||
| return new Promise((resolve, reject) => { | ||
| this.pair(idOrAddress, error => error ? reject(error) : resolve()); | ||
| }); | ||
| } | ||
| _onPair (peripheralId, paired, error) { | ||
| const peripheral = this._peripherals.get(peripheralId); | ||
| const failure = error || (paired ? null : new Error('pairing failed')); | ||
| // Always release the completion callback/promise registered as | ||
| // pair:${peripheralId} by Noble#pair — even when the peripheral is no | ||
| // longer tracked. Otherwise an untracked id leaves the caller hanging. | ||
| this.emit(`pair:${peripheralId}`, failure); | ||
| if (peripheral) { | ||
| // Public Noble-level pair event, mirroring 'discover' (subject first): | ||
| // (peripheral, error). Only fires for a known peripheral; the untracked | ||
| // case is surfaced via 'warning' below. | ||
| this.emit('pair', peripheral, failure); | ||
| peripheral.emit('pair', failure); | ||
| } else { | ||
| this.emit('warning', `unknown peripheral ${peripheralId} pair result received!`); | ||
| } | ||
| } | ||
| cancelConnect (idOrAddress, parameters) { | ||
@@ -426,0 +478,0 @@ // Get the identifier for the peripheral |
+16
-0
@@ -60,2 +60,18 @@ const NobleEventEmitter = require('./noble-event-emitter'); | ||
| pair (callback) { | ||
| // Delegate the callback to Noble#pair so it is released via the internal | ||
| // pair:${id} event. Noble#_onPair always emits pair:${id} (even when this | ||
| // peripheral has been removed from Noble's map during cleanup), whereas it | ||
| // only emits this peripheral's 'pair' event while still tracked. Registering | ||
| // on the peripheral event would therefore leave the callback hanging if | ||
| // pairing completes after the peripheral is gone. | ||
| this._noble.pair(this.id, callback); | ||
| } | ||
| async pairAsync () { | ||
| return new Promise((resolve, reject) => { | ||
| this.pair(error => error ? reject(error) : resolve()); | ||
| }); | ||
| } | ||
| cancelConnect (options) { | ||
@@ -62,0 +78,0 @@ if (this.state === 'connecting') { |
@@ -9,4 +9,8 @@ #include "ble_manager.h" | ||
| #include <winrt/Windows.Devices.Bluetooth.GenericAttributeProfile.h> | ||
| #include <winrt/Windows.Devices.Enumeration.h> | ||
| using winrt::Windows::Devices::Enumeration::DevicePairingProtectionLevel; | ||
| using winrt::Windows::Devices::Enumeration::DevicePairingResult; | ||
| using winrt::Windows::Devices::Enumeration::DevicePairingResultStatus; | ||
| using winrt::Windows::Devices::Bluetooth::BluetoothCacheMode; | ||
@@ -388,2 +392,185 @@ using winrt::Windows::Devices::Bluetooth::BluetoothConnectionStatus; | ||
| bool BLEManager::Pair(const std::string& uuid) | ||
| { | ||
| using winrt::Windows::Devices::Enumeration::DevicePairingKinds; | ||
| auto it = mDeviceMap.find(uuid); | ||
| if (it == mDeviceMap.end() || !it->second.device.has_value()) | ||
| { | ||
| mEmit.Paired(uuid, false, "device not connected"); | ||
| return true; | ||
| } | ||
| BluetoothLEDevice& device = *it->second.device; | ||
| // The complete WinRT pairing sequence runs under one exception boundary. | ||
| // Each of the calls below (device.DeviceInformation().Pairing(), | ||
| // pairing.IsPaired(), pairing.CanPair(), pairing.Custom(), the handler | ||
| // registration, and PairAsync().Completed()) can throw winrt::hresult_error | ||
| // on bad device state, revoked instances, or RPC failure to the enumerator | ||
| // service. A throw from any of them that escapes Pair() would propagate out | ||
| // of the JS binding's QueuedEventRegistration::ForwardByBaton dispatch and | ||
| // tear down the node module — that's the crash we're guarding against. | ||
| // | ||
| // `custom`, `token`, and `handlerRegistered` are declared before the try so | ||
| // both catch blocks can see them. `custom` is null-initialised because it | ||
| // is reassigned by pairing.Custom() inside the try and must be safe to | ||
| // inspect in the catch. `handlerRegistered` gates the revocation so a throw | ||
| // before the handler is attached cannot dereference an invalid token. | ||
| DeviceInformationCustomPairing custom{nullptr}; | ||
| winrt::event_token token{}; | ||
| bool handlerRegistered = false; | ||
| try | ||
| { | ||
| auto pairing = device.DeviceInformation().Pairing(); | ||
| // Already bonded — report success immediately. | ||
| if (pairing.IsPaired()) | ||
| { | ||
| mEmit.Paired(uuid, true); | ||
| return true; | ||
| } | ||
| if (!pairing.CanPair()) | ||
| { | ||
| mEmit.Paired(uuid, false, "device reports it cannot be paired"); | ||
| return true; | ||
| } | ||
| // Use custom pairing so we can auto-accept the ConfirmOnly (Just Works) | ||
| // ceremony without a UI prompt. The actual kind the device requests is | ||
| // logged so we can diagnose failures. | ||
| // | ||
| // The PairingRequested handler must Accept() the args exactly once for | ||
| // ConfirmOnly; for any other kind (DisplayPin / ProvidePassword / | ||
| // ConfirmPinMatch etc.) we simply return without Accept(), which Windows | ||
| // treats as a rejection and the PairAsync completes with the appropriate | ||
| // failure status (RejectedByHandler / AuthenticationNotAllowed). We have | ||
| // no UI to surface a PIN or password prompt from this library. | ||
| custom = pairing.Custom(); | ||
| token = custom.PairingRequested( | ||
| [](winrt::Windows::Devices::Enumeration::DeviceInformationCustomPairing const&, | ||
| winrt::Windows::Devices::Enumeration::DevicePairingRequestedEventArgs const& args) { | ||
| auto kind = args.PairingKind(); | ||
| LOGE("pairing requested, kind=%d", static_cast<int>(kind)); | ||
| if (kind == DevicePairingKinds::ConfirmOnly) | ||
| { | ||
| args.Accept(); | ||
| } | ||
| // Any other kind: do not call Accept() — Windows treats the | ||
| // handler returning as a rejection. | ||
| }); | ||
| handlerRegistered = true; | ||
| auto completed = bind2(this, &BLEManager::OnPaired, uuid, token, custom); | ||
| custom.PairAsync(DevicePairingKinds::ConfirmOnly, DevicePairingProtectionLevel::Encryption) | ||
| .Completed(completed); | ||
| } | ||
| catch (const winrt::hresult_error& e) | ||
| { | ||
| if (handlerRegistered) | ||
| { | ||
| try { custom.PairingRequested(token); } catch (...) {} | ||
| } | ||
| mEmit.Paired(uuid, false, "pairing operation failed: " + winrt::to_string(e.message())); | ||
| } | ||
| catch (const std::exception& e) | ||
| { | ||
| if (handlerRegistered) | ||
| { | ||
| try { custom.PairingRequested(token); } catch (...) {} | ||
| } | ||
| mEmit.Paired(uuid, false, std::string("pairing operation failed: ") + e.what()); | ||
| } | ||
| return true; | ||
| } | ||
| std::string pairingResultStatusToString(DevicePairingResultStatus status) | ||
| { | ||
| switch (status) | ||
| { | ||
| case DevicePairingResultStatus::Paired: return "Paired"; | ||
| case DevicePairingResultStatus::NotReadyToPair: return "NotReadyToPair"; | ||
| case DevicePairingResultStatus::NotPaired: return "NotPaired"; | ||
| case DevicePairingResultStatus::AlreadyPaired: return "AlreadyPaired"; | ||
| case DevicePairingResultStatus::ConnectionRejected: return "ConnectionRejected"; | ||
| case DevicePairingResultStatus::TooManyConnections: return "TooManyConnections"; | ||
| case DevicePairingResultStatus::HardwareFailure: return "HardwareFailure"; | ||
| case DevicePairingResultStatus::AuthenticationTimeout: return "AuthenticationTimeout"; | ||
| case DevicePairingResultStatus::AuthenticationNotAllowed: return "AuthenticationNotAllowed"; | ||
| case DevicePairingResultStatus::AuthenticationFailure: return "AuthenticationFailure"; | ||
| case DevicePairingResultStatus::NoSupportedProfiles: return "NoSupportedProfiles"; | ||
| case DevicePairingResultStatus::ProtectionLevelCouldNotBeMet: | ||
| return "ProtectionLevelCouldNotBeMet"; | ||
| case DevicePairingResultStatus::AccessDenied: return "AccessDenied"; | ||
| case DevicePairingResultStatus::InvalidCeremonyData: return "InvalidCeremonyData"; | ||
| case DevicePairingResultStatus::PairingCanceled: return "PairingCanceled"; | ||
| case DevicePairingResultStatus::OperationAlreadyInProgress: | ||
| return "OperationAlreadyInProgress"; | ||
| case DevicePairingResultStatus::RequiredHandlerNotRegistered: | ||
| return "RequiredHandlerNotRegistered"; | ||
| case DevicePairingResultStatus::RejectedByHandler: return "RejectedByHandler"; | ||
| case DevicePairingResultStatus::RemoteDeviceHasAssociation: | ||
| return "RemoteDeviceHasAssociation"; | ||
| case DevicePairingResultStatus::Failed: return "Failed"; | ||
| default: | ||
| return "Unknown(" + std::to_string(static_cast<int>(status)) + ")"; | ||
| } | ||
| } | ||
| void BLEManager::OnPaired(IAsyncOperation<DevicePairingResult> asyncOp, AsyncStatus status, | ||
| const std::string uuid, | ||
| winrt::event_token token, | ||
| winrt::Windows::Devices::Enumeration::DeviceInformationCustomPairing custom) | ||
| { | ||
| // Revoke the PairingRequested handler now that pairing has settled; without | ||
| // this, a stray callback (e.g. if the device re-prompts) would call Accept/ | ||
| // Reject on already-resolved args and could log confusing messages. Best-effort | ||
| // (guarded like the revocation sites in Pair()): a throw here must not skip | ||
| // the pairing result emission below. This block deliberately stays outside | ||
| // the result-handling try because revocation should always run even when | ||
| // the result itself is unusable. | ||
| try { custom.PairingRequested(token); } catch (...) {} | ||
| // Everything below — status branching, asyncOp.GetResults(), result.Status(), | ||
| // logging that touches the status enum, and the final emission — can throw | ||
| // winrt::hresult_error (revoked asyncOp, marshalling failures) or a standard | ||
| // exception (e.g. formatBluetoothUuid-style misbehaviour surfacing a bad_alloc). | ||
| // A throw escaping OnPaired would propagate through the Completed() handler | ||
| // that JS bindings registered and terminate the node module. Wrap the whole | ||
| // result-handling tail in one try/catch so the consumer always gets a | ||
| // deterministic Paired(uuid, false, ...). | ||
| try | ||
| { | ||
| if (status != AsyncStatus::Completed) | ||
| { | ||
| mEmit.Paired(uuid, false, "pairing operation " + asyncStatusToString(status)); | ||
| return; | ||
| } | ||
| auto result = asyncOp.GetResults(); | ||
| auto resultStatus = result.Status(); | ||
| LOGE("pairing result status=%s", pairingResultStatusToString(resultStatus).c_str()); | ||
| if (resultStatus == DevicePairingResultStatus::Paired || | ||
| resultStatus == DevicePairingResultStatus::AlreadyPaired) | ||
| { | ||
| mEmit.Paired(uuid, true); | ||
| } | ||
| else | ||
| { | ||
| mEmit.Paired(uuid, false, | ||
| "pairing failed with status " + pairingResultStatusToString(resultStatus)); | ||
| } | ||
| } | ||
| catch (const winrt::hresult_error& e) | ||
| { | ||
| mEmit.Paired(uuid, false, | ||
| "pairing result failed: " + winrt::to_string(e.message())); | ||
| } | ||
| catch (const std::exception& e) | ||
| { | ||
| mEmit.Paired(uuid, false, | ||
| std::string("pairing result failed: ") + e.what()); | ||
| } | ||
| } | ||
| bool BLEManager::Disconnect(const std::string& uuid) | ||
@@ -390,0 +577,0 @@ { |
@@ -5,2 +5,3 @@ #pragma once | ||
| #include <winrt/Windows.Devices.Bluetooth.GenericAttributeProfile.h> | ||
| #include <winrt/Windows.Devices.Enumeration.h> | ||
@@ -25,2 +26,3 @@ #include "Emit.h" | ||
| bool Connect(const std::string& uuid); | ||
| bool Pair(const std::string& uuid); | ||
| bool Disconnect(const std::string& uuid); | ||
@@ -48,2 +50,3 @@ bool CancelConnect(const std::string& uuid); | ||
| void OnConnected(IAsyncOperation<BluetoothLEDevice> asyncOp, AsyncStatus status, std::string uuid); | ||
| void OnPaired(IAsyncOperation<winrt::Windows::Devices::Enumeration::DevicePairingResult> asyncOp, AsyncStatus status, std::string uuid, winrt::event_token token, winrt::Windows::Devices::Enumeration::DeviceInformationCustomPairing custom); | ||
| void OnConnectionStatusChanged(BluetoothLEDevice device, winrt::Windows::Foundation::IInspectable inspectable); | ||
@@ -50,0 +53,0 @@ void OnGattSessionCreated(IAsyncOperation<GattSession> asyncOp, AsyncStatus status, std::string uuid); |
@@ -104,2 +104,12 @@ #include "noble_winrt.h" | ||
| // pair(deviceUuid) | ||
| Napi::Value NobleWinrt::Pair(const Napi::CallbackInfo& info) | ||
| { | ||
| CHECK_MANAGER() | ||
| ARG1(String) | ||
| auto uuid = info[0].As<Napi::String>().Utf8Value(); | ||
| manager->Pair(uuid); | ||
| return info.Env().Undefined(); | ||
| } | ||
| // disconnect(deviceUuid) | ||
@@ -319,2 +329,3 @@ Napi::Value NobleWinrt::Disconnect(const Napi::CallbackInfo& info) | ||
| NobleWinrt::InstanceMethod("connect", &NobleWinrt::Connect), | ||
| NobleWinrt::InstanceMethod("pair", &NobleWinrt::Pair), | ||
| NobleWinrt::InstanceMethod("disconnect", &NobleWinrt::Disconnect), | ||
@@ -321,0 +332,0 @@ NobleWinrt::InstanceMethod("cancelConnect", &NobleWinrt::CancelConnect), |
@@ -16,2 +16,3 @@ #pragma once | ||
| Napi::Value Connect(const Napi::CallbackInfo&); | ||
| Napi::Value Pair(const Napi::CallbackInfo&); | ||
| Napi::Value Disconnect(const Napi::CallbackInfo&); | ||
@@ -18,0 +19,0 @@ Napi::Value CancelConnect(const Napi::CallbackInfo&); |
+2
-2
@@ -9,3 +9,3 @@ { | ||
| "description": "A Node.js BLE (Bluetooth Low Energy) central library.", | ||
| "version": "2.5.10", | ||
| "version": "2.6.0", | ||
| "repository": { | ||
@@ -38,3 +38,3 @@ "type": "git", | ||
| "optionalDependencies": { | ||
| "@stoprocent/bluetooth-hci-socket": "^2.2.8" | ||
| "@stoprocent/bluetooth-hci-socket": "^2.3.0" | ||
| }, | ||
@@ -41,0 +41,0 @@ "peerDependencies": { |
+41
-0
@@ -332,2 +332,5 @@ #  | ||
| // Pair with a peripheral by ID or address (Windows only — see "Pairing" below) | ||
| await noble.pairAsync(idOrAddress); | ||
| // Set adapter address (HCI only on supported devices) | ||
@@ -352,2 +355,5 @@ noble.setAddress('00:11:22:33:44:55'); | ||
| // Pair with peripheral (Windows only — see "Pairing" below) | ||
| await peripheral.pairAsync(); | ||
| // Update RSSI | ||
@@ -387,2 +393,37 @@ const rssi = await peripheral.updateRssiAsync(); | ||
| ### Pairing | ||
| ```typescript | ||
| // Pair at the Noble level (by ID or address) or on a Peripheral instance. | ||
| await noble.pairAsync(idOrAddress); | ||
| // or | ||
| await peripheral.pairAsync(); | ||
| // Callback form is also available on both. | ||
| noble.pair(idOrAddress, error => { /* ... */ }); | ||
| peripheral.pair(error => { /* ... */ }); | ||
| // The pairing outcome is emitted on the peripheral, and (subject, error) | ||
| // on Noble, mirroring other events. | ||
| peripheral.on('pair', error => { /* error is null on success */ }); | ||
| noble.on('pair', (peripheral, error) => { /* ... */ }); | ||
| ``` | ||
| **Platform support and limitations — please read before using pairing:** | ||
| - **Windows only.** Pairing is implemented solely by the Windows (WinRT) | ||
| binding. On macOS, Linux, and other bindings `pair`/`pairAsync` does not | ||
| attempt a native call — it rejects/returns a deterministic | ||
| `'Pairing is not supported on this platform'` error so callers do not hang. | ||
| - **A connected device is required.** The peripheral must already be | ||
| discovered and connected (present in Noble's peripheral map) before pairing | ||
| is requested. Pairing an unknown/untracked id fails rather than hanging. | ||
| - **Only the `ConfirmOnly` ("Just Works") ceremony is supported.** The | ||
| handler auto-accepts `ConfirmOnly` requests without a UI prompt. Any other | ||
| ceremony (`DisplayPin`, `ProvidePassword`, `ConfirmPinMatch`, …) is **not** | ||
| accepted — this library has no UI to surface a PIN or password — and Windows | ||
| completes the operation with the corresponding failure status | ||
| (e.g. `RejectedByHandler` / `AuthenticationNotAllowed`), which is reported | ||
| back through the `pair` callback/event. | ||
| ### Service Methods | ||
@@ -389,0 +430,0 @@ |
@@ -30,2 +30,3 @@ const Peripheral = require('../../lib/peripheral'); | ||
| connect: jest.fn(), | ||
| pair: jest.fn(), | ||
| cancelConnect: jest.fn(), | ||
@@ -166,2 +167,71 @@ disconnect: jest.fn(), | ||
| describe('pair', () => { | ||
| test('should delegate to noble, forwarding the callback', () => { | ||
| peripheral.pair(); | ||
| expect(mockNoble.pair).toHaveBeenCalledWith(mockId, undefined); | ||
| expect(mockNoble.pair).toHaveBeenCalledTimes(1); | ||
| }); | ||
| test('should release the callback via Noble#pair (success)', () => { | ||
| const callback = jest.fn(); | ||
| peripheral.pair(callback); | ||
| expect(mockNoble.pair).toHaveBeenCalledWith(mockId, callback); | ||
| expect(mockNoble.pair).toHaveBeenCalledTimes(1); | ||
| // Noble releases the delegated callback through the internal pair:${id} | ||
| // event, so drive the callback it received rather than the peripheral. | ||
| mockNoble.pair.mock.calls[0][1](null); | ||
| expect(callback).toHaveBeenCalledWith(null); | ||
| expect(callback).toHaveBeenCalledTimes(1); | ||
| }); | ||
| test('should release the callback via Noble#pair (failure)', () => { | ||
| const callback = jest.fn(); | ||
| const error = new Error('pairing failed'); | ||
| peripheral.pair(callback); | ||
| mockNoble.pair.mock.calls[0][1](error); | ||
| expect(callback).toHaveBeenCalledWith(error); | ||
| expect(callback).toHaveBeenCalledTimes(1); | ||
| }); | ||
| test('should not hang on the peripheral pair event after removal', () => { | ||
| // Regression: the callback used to be registered on the peripheral 'pair' | ||
| // event, which Noble only emits while the peripheral is tracked. If | ||
| // pairing completes after cleanup (peripheral removed from Noble's map), | ||
| // that registration never fired and the callback hung forever. The | ||
| // callback is now owned by Noble#pair via pair:${id}. | ||
| const callback = jest.fn(); | ||
| peripheral.pair(callback); | ||
| peripheral.emit('pair', new Error('late completion after removal')); | ||
| expect(callback).not.toHaveBeenCalled(); | ||
| expect(mockNoble.pair).toHaveBeenCalledWith(mockId, callback); | ||
| }); | ||
| }); | ||
| describe('pairAsync', () => { | ||
| test('should resolve on success', async () => { | ||
| const promise = peripheral.pairAsync(); | ||
| // Noble releases the delegated callback via pair:${id}; simulate it. | ||
| mockNoble.pair.mock.calls[0][1](null); | ||
| await expect(promise).resolves.toBeUndefined(); | ||
| expect(mockNoble.pair).toHaveBeenCalledWith(mockId, expect.any(Function)); | ||
| expect(mockNoble.pair).toHaveBeenCalledTimes(1); | ||
| }); | ||
| test('should reject on error', async () => { | ||
| const promise = peripheral.pairAsync(); | ||
| mockNoble.pair.mock.calls[0][1](new Error('pairing failed')); | ||
| await expect(promise).rejects.toThrow('pairing failed'); | ||
| }); | ||
| }); | ||
| describe('cancelConnect', () => { | ||
@@ -168,0 +238,0 @@ test('not connecting, should resolve', async () => { |
+150
-0
@@ -645,2 +645,152 @@ const Noble = require('../lib/noble'); | ||
| describe('pair', () => { | ||
| test('should delegate to binding', () => { | ||
| const peripheralUuid = 'aabbccddeeff'; | ||
| mockBindings.pair = jest.fn(); | ||
| noble.pair(peripheralUuid); | ||
| expect(mockBindings.pair).toHaveBeenCalledWith(peripheralUuid); | ||
| expect(mockBindings.pair).toHaveBeenCalledTimes(1); | ||
| }); | ||
| test('should fail synchronously when pairing is unsupported by the bindings', () => { | ||
| const peripheralUuid = 'aabbccddeeff'; | ||
| // mockBindings deliberately has no pair() method (e.g. macOS/Linux). | ||
| const callback = jest.fn(); | ||
| noble.pair(peripheralUuid, callback); | ||
| expect(callback).toHaveBeenCalledWith(expect.any(Error)); | ||
| expect(callback).toHaveBeenCalledTimes(1); | ||
| expect(mockBindings.pair).toBeUndefined(); | ||
| }); | ||
| test('should emit the public pair event on the unsupported-platform path', () => { | ||
| const peripheralUuid = 'aabbccddeeff'; | ||
| const peripheral = { emit: jest.fn() }; | ||
| noble._peripherals.set(peripheralUuid, peripheral); | ||
| // mockBindings deliberately has no pair() method. | ||
| const pairListener = jest.fn(); | ||
| noble.on('pair', pairListener); | ||
| noble.pair(peripheralUuid, () => {}); | ||
| expect(pairListener).toHaveBeenCalledWith(peripheral, expect.any(Error)); | ||
| expect(pairListener).toHaveBeenCalledTimes(1); | ||
| }); | ||
| }); | ||
| describe('pairAsync', () => { | ||
| test('should resolve on success', async () => { | ||
| const peripheralUuid = 'aabbccddeeff'; | ||
| mockBindings.pair = jest.fn(); | ||
| noble._peripherals.set(peripheralUuid, { emit: jest.fn() }); | ||
| const promise = noble.pairAsync(peripheralUuid); | ||
| noble._onPair(peripheralUuid, true); | ||
| await expect(promise).resolves.toBeUndefined(); | ||
| expect(mockBindings.pair).toHaveBeenCalledWith(peripheralUuid); | ||
| }); | ||
| test('should reject on failure', async () => { | ||
| const peripheralUuid = 'aabbccddeeff'; | ||
| mockBindings.pair = jest.fn(); | ||
| noble._peripherals.set(peripheralUuid, { emit: jest.fn() }); | ||
| const promise = noble.pairAsync(peripheralUuid); | ||
| noble._onPair(peripheralUuid, false, new Error('pairing failed')); | ||
| await expect(promise).rejects.toThrow('pairing failed'); | ||
| }); | ||
| test('should reject when pairing is unsupported by the bindings', async () => { | ||
| const peripheralUuid = 'aabbccddeeff'; | ||
| // mockBindings deliberately has no pair() method (e.g. macOS/Linux). | ||
| const promise = noble.pairAsync(peripheralUuid); | ||
| await expect(promise).rejects.toThrow('Pairing is not supported on this platform'); | ||
| expect(mockBindings.pair).toBeUndefined(); | ||
| }); | ||
| }); | ||
| describe('onPair', () => { | ||
| test('should emit pair on existing peripheral (success)', () => { | ||
| const emit = jest.fn(); | ||
| noble._peripherals.set('aabbccddeeff', { emit }); | ||
| const warningCallback = jest.fn(); | ||
| noble.on('warning', warningCallback); | ||
| noble._onPair('aabbccddeeff', true); | ||
| expect(emit).toHaveBeenCalledWith('pair', null); | ||
| expect(warningCallback).not.toHaveBeenCalled(); | ||
| }); | ||
| test('should emit pair failure on existing peripheral', () => { | ||
| const emit = jest.fn(); | ||
| noble._peripherals.set('aabbccddeeff', { emit }); | ||
| const error = new Error('pairing failed'); | ||
| const warningCallback = jest.fn(); | ||
| noble.on('warning', warningCallback); | ||
| noble._onPair('aabbccddeeff', false, error); | ||
| expect(emit).toHaveBeenCalledWith('pair', error); | ||
| expect(warningCallback).not.toHaveBeenCalled(); | ||
| }); | ||
| test('should emit the public Noble pair event on success', () => { | ||
| const peripheral = { emit: jest.fn() }; | ||
| noble._peripherals.set('aabbccddeeff', peripheral); | ||
| const pairListener = jest.fn(); | ||
| noble.on('pair', pairListener); | ||
| noble._onPair('aabbccddeeff', true); | ||
| expect(pairListener).toHaveBeenCalledWith(peripheral, null); | ||
| expect(pairListener).toHaveBeenCalledTimes(1); | ||
| }); | ||
| test('should emit the public Noble pair event on failure', () => { | ||
| const peripheral = { emit: jest.fn() }; | ||
| noble._peripherals.set('aabbccddeeff', peripheral); | ||
| const error = new Error('pairing failed'); | ||
| const pairListener = jest.fn(); | ||
| noble.on('pair', pairListener); | ||
| noble._onPair('aabbccddeeff', false, error); | ||
| expect(pairListener).toHaveBeenCalledWith(peripheral, error); | ||
| expect(pairListener).toHaveBeenCalledTimes(1); | ||
| }); | ||
| test('should release the completion callback even for an untracked peripheral', () => { | ||
| // Regression: Noble#pair registers pair:${id}; if the native side | ||
| // completes pairing for an id that is not in _peripherals, the | ||
| // completion callback must still fire rather than hang forever. | ||
| const peripheralUuid = 'aabbccddeeff'; | ||
| const warningCallback = jest.fn(); | ||
| noble.on('warning', warningCallback); | ||
| // Ensure Noble#pair takes the supported-bindings path so completion waits | ||
| // for _onPair (mirrors native behavior on Windows). | ||
| mockBindings.pair = jest.fn(); | ||
| const completionCallback = jest.fn(); | ||
| noble.pair(peripheralUuid, completionCallback); | ||
| expect(completionCallback).not.toHaveBeenCalled(); | ||
| noble._onPair(peripheralUuid, false, new Error('device not connected')); | ||
| expect(completionCallback).toHaveBeenCalledWith(expect.any(Error)); | ||
| expect(completionCallback).toHaveBeenCalledTimes(1); | ||
| expect(warningCallback).toHaveBeenCalledWith( | ||
| 'unknown peripheral aabbccddeeff pair result received!' | ||
| ); | ||
| }); | ||
| }); | ||
| describe('onDiscover', () => { | ||
@@ -647,0 +797,0 @@ test('should add new peripheral', () => { |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
2507131
3.46%17436
1.45%754
5.75%