@stoprocent/noble
Advanced tools
| // Integer values mirror WinRT's DevicePairingKinds enum (Windows.Devices.Enumeration). | ||
| // Only the Windows binding uses these; other bindings surface a deterministic | ||
| // "Pairing is not supported on this platform" error when pairing is requested. | ||
| module.exports = { | ||
| None: 0, | ||
| ConfirmOnly: 0x00000008, | ||
| DisplayPin: 0x00000010, | ||
| ConfirmPinMatch: 0x00000020, | ||
| ProvidePin: 0x00000040, | ||
| ProvidePassword: 0x00000080, | ||
| ConfirmPassword: 0x00000100, | ||
| }; |
| // Integer values mirror WinRT's DevicePairingProtectionLevel enum | ||
| // (Windows.Devices.Enumeration). Only the Windows binding uses these; | ||
| // other bindings' `pair()` surfaces "Pairing is not supported on this | ||
| // platform". | ||
| module.exports = { | ||
| Default: 0, | ||
| None: 1, | ||
| Encryption: 2, | ||
| EncryptionAndAuthentication: 3, | ||
| }; |
| const UINT32_MAX = 0xFFFFFFFF; | ||
| module.exports = function isUint32 (value) { | ||
| return Number.isInteger(value) && value >= 0 && value <= UINT32_MAX; | ||
| }; |
+49
-16
@@ -28,2 +28,32 @@ /// <reference types="node" /> | ||
| /** | ||
| * Mirrors WinRT's `DevicePairingKinds` (Windows.Devices.Enumeration). | ||
| * Bitfield: pass a single kind or OR several together to advertise | ||
| * which ceremonies the caller knows how to handle. | ||
| * | ||
| * Only the Windows binding honors this; `pair()` on hci-socket / dbus / | ||
| * mac surfaces "Pairing is not supported on this platform" regardless | ||
| * of the value. | ||
| */ | ||
| export enum DevicePairingKinds { | ||
| None = 0, | ||
| ConfirmOnly = 0x00000008, | ||
| DisplayPin = 0x00000010, | ||
| ConfirmPinMatch = 0x00000020, | ||
| ProvidePin = 0x00000040, | ||
| ProvidePassword = 0x00000080, | ||
| ConfirmPassword = 0x00000100, | ||
| } | ||
| /** | ||
| * Mirrors WinRT's `DevicePairingProtectionLevel`. | ||
| * Used only by the Windows binding. | ||
| */ | ||
| export enum DevicePairingProtectionLevel { | ||
| Default = 0, | ||
| None = 1, | ||
| Encryption = 2, | ||
| EncryptionAndAuthentication = 3, | ||
| } | ||
| export interface ConnectOptions { | ||
@@ -49,8 +79,3 @@ addressType?: PeripheralAddressType; | ||
| 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>; | ||
| pairAsync(idOrAddress: PeripheralIdOrAddress, kind?: DevicePairingKinds, protectionLevel?: DevicePairingProtectionLevel): Promise<void>; | ||
@@ -60,8 +85,2 @@ startScanning(serviceUUIDs?: string[], allowDuplicates?: boolean, callback?: (error?: Error) => 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; | ||
@@ -71,2 +90,16 @@ reset(): void; | ||
| setAddress(address: string): void; | ||
| /** | ||
| * Pair with a peripheral. `kind` defaults to | ||
| * `DevicePairingKinds.ConfirmOnly` and `protectionLevel` defaults to | ||
| * `DevicePairingProtectionLevel.Encryption`. On Windows, `ConfirmOnly`, | ||
| * `DisplayPin`, and `ConfirmPinMatch` use the OS pairing dialog. PIN / | ||
| * password kinds (`ProvidePin`, `ProvidePassword`, `ConfirmPassword`) are | ||
| * rejected because this library does not collect secrets to pass into | ||
| * WinRT. On non-Windows platforms the call surfaces a "Pairing is not | ||
| * supported" error. | ||
| */ | ||
| pair(idOrAddress: PeripheralIdOrAddress, callback: (error: Error | undefined) => void): void; | ||
| pair(idOrAddress: PeripheralIdOrAddress, kind?: DevicePairingKinds, callback?: (error: Error | undefined) => void): void; | ||
| pair(idOrAddress: PeripheralIdOrAddress, kind: DevicePairingKinds, protectionLevel: DevicePairingProtectionLevel, callback?: (error: Error | undefined) => void): void; | ||
@@ -126,4 +159,3 @@ on(event: "stateChange", listener: (state: AdapterState) => void): this; | ||
| connectAsync(): Promise<void>; | ||
| /** Windows only; ConfirmOnly ("Just Works") ceremony only. */ | ||
| pairAsync(): Promise<void>; | ||
| pairAsync(kind?: DevicePairingKinds, protectionLevel?: DevicePairingProtectionLevel): Promise<void>; | ||
| disconnectAsync(): Promise<void>; | ||
@@ -139,4 +171,5 @@ updateRssiAsync(): Promise<number>; | ||
| connect(callback?: (error: Error | undefined) => void): void; | ||
| /** Windows only; ConfirmOnly ("Just Works") ceremony only. */ | ||
| pair(callback?: (error: Error | null) => void): void; | ||
| pair(callback: (error: Error | undefined) => void): void; | ||
| pair(kind?: DevicePairingKinds, callback?: (error: Error | undefined) => void): void; | ||
| pair(kind: DevicePairingKinds, protectionLevel: DevicePairingProtectionLevel, callback?: (error: Error | undefined) => void): void; | ||
| disconnect(callback?: () => void): void; | ||
@@ -143,0 +176,0 @@ updateRssi(callback?: (error: Error | undefined, rssi: number) => void): void; |
+4
-0
| const withBindings = require('./lib/resolve-bindings'); | ||
| const hciStatusMessage = require('./lib/hci-status-message'); | ||
| const DevicePairingKinds = require('./lib/pairing-kinds'); | ||
| const DevicePairingProtectionLevel = require('./lib/pairing-protection-level'); | ||
@@ -7,1 +9,3 @@ module.exports = withBindings(); | ||
| module.exports.hciStatusMessage = hciStatusMessage; | ||
| module.exports.DevicePairingKinds = DevicePairingKinds; | ||
| module.exports.DevicePairingProtectionLevel = DevicePairingProtectionLevel; |
+63
-5
@@ -9,2 +9,5 @@ const debug = require('debug')('noble'); | ||
| const Descriptor = require('./descriptor'); | ||
| const DevicePairingKinds = require('./pairing-kinds'); | ||
| const DevicePairingProtectionLevel = require('./pairing-protection-level'); | ||
| const isUint32 = require('./uint32'); | ||
@@ -437,4 +440,58 @@ class Noble extends NobleEventEmitter { | ||
| pair (idOrAddress, callback) { | ||
| pair (idOrAddress, kind, protectionLevel, callback) { | ||
| const identifier = this._getPeripheralId(idOrAddress); | ||
| // Backward compat: `pair(id, callback)` — the legacy signature passes | ||
| // the callback in the 2nd position. Detect that and default `kind` to | ||
| // ConfirmOnly so existing callers see no behavior change. The new | ||
| // signature's `kind` is a number, so a function in this slot is | ||
| // unambiguously the legacy callback. | ||
| if (typeof kind === 'function') { | ||
| callback = kind; | ||
| kind = undefined; | ||
| protectionLevel = undefined; | ||
| } else if (typeof protectionLevel === 'function') { | ||
| callback = protectionLevel; | ||
| protectionLevel = undefined; | ||
| } | ||
| if (kind !== undefined && kind !== null && !isUint32(kind)) { | ||
| const err = new Error('pair() kind must be a finite uint32 DevicePairingKinds bitmask'); | ||
| if (typeof callback === 'function') { | ||
| callback(err); | ||
| } else { | ||
| this.emit('warning', err.message); | ||
| } | ||
| return; | ||
| } | ||
| // Reject an empty mask up-front. Otherwise the native handler's | ||
| // `kind & kinds` would always be 0, causing PairAsync to fail with | ||
| // a generic RejectedByHandler that's indistinguishable from the | ||
| // device refusing a ceremony we asked for. | ||
| if (kind === 0) { | ||
| const err = new Error('pair() requires at least one DevicePairingKinds value; DevicePairingKinds.None matches no ceremony'); | ||
| if (typeof callback === 'function') { | ||
| callback(err); | ||
| } else { | ||
| this.emit('warning', err.message); | ||
| } | ||
| return; | ||
| } | ||
| const pairingKind = (kind === undefined || kind === null) | ||
| ? DevicePairingKinds.ConfirmOnly | ||
| : kind; | ||
| if (protectionLevel !== undefined && | ||
| protectionLevel !== null && | ||
| !isUint32(protectionLevel)) { | ||
| const err = new Error('pair() protectionLevel must be a finite uint32 DevicePairingProtectionLevel'); | ||
| if (typeof callback === 'function') { | ||
| callback(err); | ||
| } else { | ||
| this.emit('warning', err.message); | ||
| } | ||
| return; | ||
| } | ||
| const pairingProtectionLevel = (protectionLevel === undefined || protectionLevel === null) | ||
| ? DevicePairingProtectionLevel.Encryption | ||
| : protectionLevel; | ||
| if (typeof callback === 'function') { | ||
@@ -447,3 +504,4 @@ this.onceExclusive(`pair:${identifier}`, error => callback(error)); | ||
| // callers don't hang waiting for a 'pair' completion that will | ||
| // never come. | ||
| // never come. The requested `kind` is ignored on unsupported | ||
| // platforms. | ||
| const err = new Error('Pairing is not supported on this platform'); | ||
@@ -461,8 +519,8 @@ const peripheral = this._peripherals.get(identifier); | ||
| } | ||
| this._bindings.pair(identifier); | ||
| this._bindings.pair(identifier, pairingKind, pairingProtectionLevel); | ||
| } | ||
| async pairAsync (idOrAddress) { | ||
| async pairAsync (idOrAddress, kind, protectionLevel) { | ||
| return new Promise((resolve, reject) => { | ||
| this.pair(idOrAddress, error => error ? reject(error) : resolve()); | ||
| this.pair(idOrAddress, kind, protectionLevel, error => error ? reject(error) : resolve()); | ||
| }); | ||
@@ -469,0 +527,0 @@ } |
+23
-10
@@ -60,15 +60,28 @@ 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); | ||
| pair (kind, protectionLevel, callback) { | ||
| // Backward compat: `peripheral.pair(callback)` — the legacy signature | ||
| // passes the callback in the 1st position. Detect that and forward | ||
| // `undefined` as the kind so `noble.pair` defaults to ConfirmOnly. | ||
| // | ||
| // The callback is delegated 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. | ||
| if (typeof kind === 'function') { | ||
| callback = kind; | ||
| kind = undefined; | ||
| protectionLevel = undefined; | ||
| } else if (typeof protectionLevel === 'function') { | ||
| callback = protectionLevel; | ||
| protectionLevel = undefined; | ||
| } | ||
| this._noble.pair(this.id, kind, protectionLevel, callback); | ||
| } | ||
| async pairAsync () { | ||
| async pairAsync (kind, protectionLevel) { | ||
| return new Promise((resolve, reject) => { | ||
| this.pair(error => error ? reject(error) : resolve()); | ||
| this.pair(kind, protectionLevel, error => error ? reject(error) : resolve()); | ||
| }); | ||
@@ -75,0 +88,0 @@ } |
@@ -391,6 +391,6 @@ #include "ble_manager.h" | ||
| bool BLEManager::Pair(const std::string& uuid) | ||
| bool BLEManager::Pair(const std::string& uuid, | ||
| winrt::Windows::Devices::Enumeration::DevicePairingKinds kinds, | ||
| winrt::Windows::Devices::Enumeration::DevicePairingProtectionLevel protectionLevel) | ||
| { | ||
| using winrt::Windows::Devices::Enumeration::DevicePairingKinds; | ||
| auto it = mDeviceMap.find(uuid); | ||
@@ -439,28 +439,55 @@ if (it == mDeviceMap.end() || !it->second.device.has_value()) | ||
| // 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. | ||
| // These ceremony values are not supported because this library does | ||
| // not collect PIN/password input to forward into an Accept(...) overload. | ||
| constexpr uint32_t unsupportedKinds = | ||
| 0x00000040u | 0x00000080u | 0x00000100u; | ||
| if ((static_cast<uint32_t>(kinds) & unsupportedKinds) != 0) | ||
| { | ||
| mEmit.Paired(uuid, false, | ||
| "pairing kinds requiring a PIN or password are not supported"); | ||
| return true; | ||
| } | ||
| // Always go through `pairing.Custom()`: only DeviceInformationCustomPairing | ||
| // accepts a `DevicePairingKinds` mask (DeviceInformationPairing.PairAsync | ||
| // takes only a protection level). The caller-supplied `kinds` may | ||
| // include ConfirmOnly, DisplayPin, or ConfirmPinMatch; PIN/password | ||
| // ceremonies are rejected before we get here because this library does | ||
| // not provide a way to collect or forward secrets to Accept(...). | ||
| // | ||
| // 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. | ||
| // The PairingRequested lambda must Accept() exactly once for any kind | ||
| // the caller advertised in `kinds`. Some devices negotiated through | ||
| // ConfirmPinMatch can still raise a ConfirmOnly prompt; treat that as a | ||
| // compatible fallback so pairing does not fail with status=Failed. | ||
| // For other kinds outside the mask we return without Accept(), which | ||
| // Windows treats as a rejection and PairAsync completes with | ||
| // RejectedByHandler / AuthenticationNotAllowed. We never forward a PIN | ||
| // or password back to JS — Windows drives the UI for non-ConfirmOnly | ||
| // ceremonies. | ||
| custom = pairing.Custom(); | ||
| token = custom.PairingRequested( | ||
| [](winrt::Windows::Devices::Enumeration::DeviceInformationCustomPairing const&, | ||
| winrt::Windows::Devices::Enumeration::DevicePairingRequestedEventArgs const& args) { | ||
| [kinds](winrt::Windows::Devices::Enumeration::DeviceInformationCustomPairing const&, | ||
| winrt::Windows::Devices::Enumeration::DevicePairingRequestedEventArgs const& args) { | ||
| auto kind = args.PairingKind(); | ||
| auto kindMask = static_cast<uint32_t>(kind); | ||
| auto requestedKinds = static_cast<uint32_t>(kinds); | ||
| bool confirmPinMatchRequested = | ||
| (requestedKinds & | ||
| static_cast<uint32_t>( | ||
| winrt::Windows::Devices::Enumeration::DevicePairingKinds::ConfirmPinMatch)) != 0; | ||
| bool confirmOnlyFallback = | ||
| confirmPinMatchRequested && | ||
| kind == winrt::Windows::Devices::Enumeration::DevicePairingKinds::ConfirmOnly; | ||
| LOGE("pairing requested, kind=%d", static_cast<int>(kind)); | ||
| if (kind == DevicePairingKinds::ConfirmOnly) | ||
| if ((kindMask & requestedKinds) != 0 || confirmOnlyFallback) | ||
| { | ||
| args.Accept(); | ||
| } | ||
| // Any other kind: do not call Accept() — Windows treats the | ||
| // handler returning as a rejection. | ||
| // Any kind outside the caller's mask: 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) | ||
| custom.PairAsync(kinds, protectionLevel) | ||
| .Completed(completed); | ||
@@ -467,0 +494,0 @@ } |
@@ -25,3 +25,6 @@ #pragma once | ||
| bool Connect(const std::string& uuid); | ||
| bool Pair(const std::string& uuid); | ||
| bool Pair( | ||
| const std::string& uuid, | ||
| winrt::Windows::Devices::Enumeration::DevicePairingKinds kinds, | ||
| winrt::Windows::Devices::Enumeration::DevicePairingProtectionLevel protectionLevel); | ||
| bool Disconnect(const std::string& uuid); | ||
@@ -28,0 +31,0 @@ bool CancelConnect(const std::string& uuid); |
@@ -71,1 +71,24 @@ #include "napi_winrt.h" | ||
| } | ||
| uint32_t getUint32(const Napi::Value& value, uint32_t def) | ||
| { | ||
| if (!value.IsNumber()) | ||
| { | ||
| return def; | ||
| } | ||
| const double asDouble = value.As<Napi::Number>().DoubleValue(); | ||
| // Reject NaN / infinities / negatives / values outside uint32 range. | ||
| // (NaN makes both comparisons false, so it is rejected here.) | ||
| if (!(asDouble >= 0.0 && asDouble <= 4294967295.0)) | ||
| { | ||
| return def; | ||
| } | ||
| const uint32_t asUint32 = static_cast<uint32_t>(asDouble); | ||
| // Reject fractional values (and values not exactly representable as uint32). | ||
| if (asDouble != static_cast<double>(asUint32)) | ||
| { | ||
| return def; | ||
| } | ||
| return asUint32; | ||
| } |
@@ -9,2 +9,3 @@ #pragma once | ||
| bool getBool(const Napi::Value& value, bool def); | ||
| uint32_t getUint32(const Napi::Value& value, uint32_t def); | ||
@@ -11,0 +12,0 @@ winrt::guid napiToUuid(Napi::String string); |
@@ -104,3 +104,3 @@ #include "noble_winrt.h" | ||
| // pair(deviceUuid) | ||
| // pair(deviceUuid, kinds?, protectionLevel?) | ||
| Napi::Value NobleWinrt::Pair(const Napi::CallbackInfo& info) | ||
@@ -111,3 +111,18 @@ { | ||
| auto uuid = info[0].As<Napi::String>().Utf8Value(); | ||
| manager->Pair(uuid); | ||
| // `kinds` is a DevicePairingKinds bitmask. The JS layer (noble.js) is | ||
| // expected to normalize the value before forwarding here — including | ||
| // rejecting DevicePairingKinds.None — so any default we pick is a | ||
| // belt-and-suspenders backstop, not a primary code path. We default | ||
| // to None (0) rather than ConfirmOnly to surface unexpected callers | ||
| // via the lambda's `(kind & kinds) == 0` check (PairAsync will fail | ||
| // with RejectedByHandler), instead of silently falling through to a | ||
| // ceremony the caller never asked for. | ||
| using winrt::Windows::Devices::Enumeration::DevicePairingKinds; | ||
| using winrt::Windows::Devices::Enumeration::DevicePairingProtectionLevel; | ||
| auto kinds = static_cast<DevicePairingKinds>( | ||
| getUint32(info[1], static_cast<uint32_t>(DevicePairingKinds::None))); | ||
| auto protectionLevel = static_cast<DevicePairingProtectionLevel>( | ||
| getUint32(info[2], static_cast<uint32_t>(DevicePairingProtectionLevel::Encryption))); | ||
| manager->Pair(uuid, kinds, protectionLevel); | ||
| return info.Env().Undefined(); | ||
@@ -114,0 +129,0 @@ } |
+1
-1
@@ -9,3 +9,3 @@ { | ||
| "description": "A Node.js BLE (Bluetooth Low Energy) central library.", | ||
| "version": "2.7.1", | ||
| "version": "2.8.0", | ||
| "repository": { | ||
@@ -12,0 +12,0 @@ "type": "git", |
+19
-7
@@ -394,2 +394,7 @@ #  | ||
| ```typescript | ||
| import noble, { | ||
| DevicePairingKinds, | ||
| DevicePairingProtectionLevel, | ||
| } from '@stoprocent/noble'; | ||
| // Pair at the Noble level (by ID or address) or on a Peripheral instance. | ||
@@ -400,2 +405,9 @@ await noble.pairAsync(idOrAddress); | ||
| // Optionally choose pairing ceremony + protection level (Windows only). | ||
| await noble.pairAsync( | ||
| idOrAddress, | ||
| DevicePairingKinds.ConfirmOnly, | ||
| DevicePairingProtectionLevel.EncryptionAndAuthentication | ||
| ); | ||
| // Callback form is also available on both. | ||
@@ -420,9 +432,9 @@ noble.pair(idOrAddress, error => { /* ... */ }); | ||
| 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. | ||
| - **Windows pairing options.** `kind` defaults to `ConfirmOnly` and | ||
| `protectionLevel` defaults to `Encryption`. You can pass a | ||
| `DevicePairingKinds` bitmask (single or OR-ed values) and (optionally) | ||
| `DevicePairingProtectionLevel`. | ||
| `ConfirmOnly`, `DisplayPin`, and `ConfirmPinMatch` use Windows' native UI; | ||
| PIN/password kinds (`ProvidePin`, `ProvidePassword`, `ConfirmPassword`) are | ||
| rejected because this library does not collect secrets to pass into WinRT. | ||
@@ -429,0 +441,0 @@ ### Service Methods |
@@ -11,2 +11,11 @@ jest.mock('../lib/resolve-bindings', () => jest.fn(() => ({}))); | ||
| }); | ||
| test('exposes DevicePairingProtectionLevel constants', () => { | ||
| expect(noble.DevicePairingProtectionLevel).toEqual({ | ||
| Default: 0, | ||
| None: 1, | ||
| Encryption: 2, | ||
| EncryptionAndAuthentication: 3, | ||
| }); | ||
| }); | ||
| }); |
@@ -184,3 +184,3 @@ const Peripheral = require('../../lib/peripheral'); | ||
| expect(mockNoble.pair).toHaveBeenCalledWith(mockId, undefined); | ||
| expect(mockNoble.pair).toHaveBeenCalledWith(mockId, undefined, undefined, undefined); | ||
| expect(mockNoble.pair).toHaveBeenCalledTimes(1); | ||
@@ -194,3 +194,5 @@ }); | ||
| expect(mockNoble.pair).toHaveBeenCalledWith(mockId, callback); | ||
| // `kind`/`protectionLevel` are forwarded as undefined | ||
| // (1st-arg-was-callback swap), and callback is in the 4th position. | ||
| expect(mockNoble.pair).toHaveBeenCalledWith(mockId, undefined, undefined, callback); | ||
| expect(mockNoble.pair).toHaveBeenCalledTimes(1); | ||
@@ -200,3 +202,3 @@ | ||
| // event, so drive the callback it received rather than the peripheral. | ||
| mockNoble.pair.mock.calls[0][1](null); | ||
| mockNoble.pair.mock.calls[0][3](null); | ||
| expect(callback).toHaveBeenCalledWith(null); | ||
@@ -211,3 +213,3 @@ expect(callback).toHaveBeenCalledTimes(1); | ||
| peripheral.pair(callback); | ||
| mockNoble.pair.mock.calls[0][1](error); | ||
| mockNoble.pair.mock.calls[0][3](error); | ||
@@ -230,4 +232,25 @@ expect(callback).toHaveBeenCalledWith(error); | ||
| expect(mockNoble.pair).toHaveBeenCalledWith(mockId, callback); | ||
| expect(mockNoble.pair).toHaveBeenCalledWith(mockId, undefined, undefined, callback); | ||
| }); | ||
| test('should forward explicit kind alongside callback', () => { | ||
| const kind = 0x00000010; // DevicePairingKinds.DisplayPin | ||
| const callback = jest.fn(); | ||
| peripheral.pair(kind, callback); | ||
| expect(mockNoble.pair).toHaveBeenCalledWith(mockId, kind, undefined, callback); | ||
| expect(mockNoble.pair).toHaveBeenCalledTimes(1); | ||
| }); | ||
| test('should forward explicit protection level alongside kind + callback', () => { | ||
| const kind = 0x00000008; // ConfirmOnly | ||
| const protectionLevel = 3; // EncryptionAndAuthentication | ||
| const callback = jest.fn(); | ||
| peripheral.pair(kind, protectionLevel, callback); | ||
| expect(mockNoble.pair).toHaveBeenCalledWith(mockId, kind, protectionLevel, callback); | ||
| expect(mockNoble.pair).toHaveBeenCalledTimes(1); | ||
| }); | ||
| }); | ||
@@ -239,6 +262,7 @@ | ||
| // Noble releases the delegated callback via pair:${id}; simulate it. | ||
| mockNoble.pair.mock.calls[0][1](null); | ||
| // After kind-swap, pairAsync calls noble.pair(id, undefined, undefined, callback). | ||
| mockNoble.pair.mock.calls[0][3](null); | ||
| await expect(promise).resolves.toBeUndefined(); | ||
| expect(mockNoble.pair).toHaveBeenCalledWith(mockId, expect.any(Function)); | ||
| expect(mockNoble.pair).toHaveBeenCalledWith(mockId, undefined, undefined, expect.any(Function)); | ||
| expect(mockNoble.pair).toHaveBeenCalledTimes(1); | ||
@@ -249,6 +273,27 @@ }); | ||
| const promise = peripheral.pairAsync(); | ||
| mockNoble.pair.mock.calls[0][1](new Error('pairing failed')); | ||
| mockNoble.pair.mock.calls[0][3](new Error('pairing failed')); | ||
| await expect(promise).rejects.toThrow('pairing failed'); | ||
| }); | ||
| test('should forward explicit kind', async () => { | ||
| const kind = 0x00000010; // DisplayPin | ||
| const promise = peripheral.pairAsync(kind); | ||
| mockNoble.pair.mock.calls[0][3](null); | ||
| await expect(promise).resolves.toBeUndefined(); | ||
| expect(mockNoble.pair).toHaveBeenCalledWith(mockId, kind, undefined, expect.any(Function)); | ||
| expect(mockNoble.pair).toHaveBeenCalledTimes(1); | ||
| }); | ||
| test('should forward explicit protection level', async () => { | ||
| const kind = 0x00000010; // DisplayPin | ||
| const protectionLevel = 1; // None | ||
| const promise = peripheral.pairAsync(kind, protectionLevel); | ||
| mockNoble.pair.mock.calls[0][3](null); | ||
| await expect(promise).resolves.toBeUndefined(); | ||
| expect(mockNoble.pair).toHaveBeenCalledWith(mockId, kind, protectionLevel, expect.any(Function)); | ||
| expect(mockNoble.pair).toHaveBeenCalledTimes(1); | ||
| }); | ||
| }); | ||
@@ -255,0 +300,0 @@ |
+209
-3
@@ -638,2 +638,183 @@ const Noble = require('../lib/noble'); | ||
| describe('pair', () => { | ||
| // Mirror lib/pairing-kinds.js — kept in sync manually since these tests | ||
| // exercise the JS layer (no WinRT binding in scope). | ||
| const DevicePairingKinds = { | ||
| None: 0, | ||
| ConfirmOnly: 0x00000008, | ||
| DisplayPin: 0x00000010, | ||
| ProvidePin: 0x00000040, | ||
| }; | ||
| const DevicePairingProtectionLevel = { | ||
| None: 1, | ||
| Encryption: 2, | ||
| EncryptionAndAuthentication: 3, | ||
| }; | ||
| // Use a hex id so `_getPeripheralId` accepts it without calling the | ||
| // mocked addressToId (whose return value would otherwise become the | ||
| // forwarded identifier). | ||
| const peripheralUuidHex = '00112233445566778899aabbccddeeff'; | ||
| test('should default to ConfirmOnly when kind is omitted', () => { | ||
| mockBindings.pair = jest.fn(); | ||
| noble.pair(peripheralUuidHex, () => {}); | ||
| expect(mockBindings.pair).toHaveBeenCalledWith( | ||
| peripheralUuidHex, | ||
| DevicePairingKinds.ConfirmOnly, | ||
| DevicePairingProtectionLevel.Encryption | ||
| ); | ||
| }); | ||
| test('should forward explicit kind to the binding', () => { | ||
| mockBindings.pair = jest.fn(); | ||
| noble.pair(peripheralUuidHex, DevicePairingKinds.DisplayPin, () => {}); | ||
| expect(mockBindings.pair).toHaveBeenCalledWith( | ||
| peripheralUuidHex, | ||
| DevicePairingKinds.DisplayPin, | ||
| DevicePairingProtectionLevel.Encryption | ||
| ); | ||
| }); | ||
| test('should accept a bitmask kind (multiple OR-ed values)', () => { | ||
| mockBindings.pair = jest.fn(); | ||
| const mask = DevicePairingKinds.ConfirmOnly | DevicePairingKinds.ProvidePin; | ||
| noble.pair(peripheralUuidHex, mask, () => {}); | ||
| expect(mockBindings.pair).toHaveBeenCalledWith( | ||
| peripheralUuidHex, | ||
| mask, | ||
| DevicePairingProtectionLevel.Encryption | ||
| ); | ||
| }); | ||
| test('should forward explicit protection level to the binding', () => { | ||
| mockBindings.pair = jest.fn(); | ||
| noble.pair( | ||
| peripheralUuidHex, | ||
| DevicePairingKinds.ConfirmOnly, | ||
| DevicePairingProtectionLevel.EncryptionAndAuthentication, | ||
| () => {} | ||
| ); | ||
| expect(mockBindings.pair).toHaveBeenCalledWith( | ||
| peripheralUuidHex, | ||
| DevicePairingKinds.ConfirmOnly, | ||
| DevicePairingProtectionLevel.EncryptionAndAuthentication | ||
| ); | ||
| }); | ||
| test('should treat function in 2nd position as callback (legacy signature)', () => { | ||
| mockBindings.pair = jest.fn(); | ||
| const cb = jest.fn(); | ||
| noble.pair(peripheralUuidHex, cb); | ||
| expect(mockBindings.pair).toHaveBeenCalledWith( | ||
| peripheralUuidHex, | ||
| DevicePairingKinds.ConfirmOnly, | ||
| DevicePairingProtectionLevel.Encryption | ||
| ); | ||
| }); | ||
| test('should treat function in 3rd position as callback (legacy signature)', () => { | ||
| mockBindings.pair = jest.fn(); | ||
| const cb = jest.fn(); | ||
| noble.pair(peripheralUuidHex, DevicePairingKinds.ConfirmOnly, cb); | ||
| expect(mockBindings.pair).toHaveBeenCalledWith( | ||
| peripheralUuidHex, | ||
| DevicePairingKinds.ConfirmOnly, | ||
| DevicePairingProtectionLevel.Encryption | ||
| ); | ||
| }); | ||
| test('pairAsync should default to ConfirmOnly', async () => { | ||
| mockBindings.pair = jest.fn(); | ||
| const promise = noble.pairAsync(peripheralUuidHex); | ||
| // pairAsync awaits a `pair:${identifier}` event; emit success | ||
| // (null error) to resolve the promise. | ||
| noble.emit(`pair:${peripheralUuidHex}`, null); | ||
| await promise; | ||
| expect(mockBindings.pair).toHaveBeenCalledWith( | ||
| peripheralUuidHex, | ||
| DevicePairingKinds.ConfirmOnly, | ||
| DevicePairingProtectionLevel.Encryption | ||
| ); | ||
| }); | ||
| test('pairAsync should forward explicit kind', async () => { | ||
| mockBindings.pair = jest.fn(); | ||
| const promise = noble.pairAsync(peripheralUuidHex, DevicePairingKinds.ProvidePin); | ||
| noble.emit(`pair:${peripheralUuidHex}`, null); | ||
| await promise; | ||
| expect(mockBindings.pair).toHaveBeenCalledWith( | ||
| peripheralUuidHex, | ||
| DevicePairingKinds.ProvidePin, | ||
| DevicePairingProtectionLevel.Encryption | ||
| ); | ||
| }); | ||
| test('pairAsync should forward explicit protection level', async () => { | ||
| mockBindings.pair = jest.fn(); | ||
| const promise = noble.pairAsync( | ||
| peripheralUuidHex, | ||
| DevicePairingKinds.ProvidePin, | ||
| DevicePairingProtectionLevel.None | ||
| ); | ||
| noble.emit(`pair:${peripheralUuidHex}`, null); | ||
| await promise; | ||
| expect(mockBindings.pair).toHaveBeenCalledWith( | ||
| peripheralUuidHex, | ||
| DevicePairingKinds.ProvidePin, | ||
| DevicePairingProtectionLevel.None | ||
| ); | ||
| }); | ||
| test('should reject invalid kind values', () => { | ||
| mockBindings.pair = jest.fn(); | ||
| const cb = jest.fn(); | ||
| noble.pair(peripheralUuidHex, Number.NaN, cb); | ||
| expect(cb).toHaveBeenCalledTimes(1); | ||
| expect(cb).toHaveBeenCalledWith(expect.any(Error)); | ||
| expect(cb.mock.calls[0][0].message).toBe( | ||
| 'pair() kind must be a finite uint32 DevicePairingKinds bitmask' | ||
| ); | ||
| expect(mockBindings.pair).not.toHaveBeenCalled(); | ||
| }); | ||
| test('should reject DevicePairingKinds.None (empty mask)', () => { | ||
| mockBindings.pair = jest.fn(); | ||
| const cb = jest.fn(); | ||
| noble.pair(peripheralUuidHex, DevicePairingKinds.None, cb); | ||
| expect(cb).toHaveBeenCalledTimes(1); | ||
| expect(cb).toHaveBeenCalledWith(expect.any(Error)); | ||
| expect(cb.mock.calls[0][0].message).toBe( | ||
| 'pair() requires at least one DevicePairingKinds value; DevicePairingKinds.None matches no ceremony' | ||
| ); | ||
| expect(mockBindings.pair).not.toHaveBeenCalled(); | ||
| }); | ||
| test('should reject invalid protection level type', () => { | ||
| mockBindings.pair = jest.fn(); | ||
| const cb = jest.fn(); | ||
| noble.pair(peripheralUuidHex, DevicePairingKinds.ConfirmOnly, 1.5, cb); | ||
| expect(cb).toHaveBeenCalledTimes(1); | ||
| expect(cb).toHaveBeenCalledWith(expect.any(Error)); | ||
| expect(cb.mock.calls[0][0].message).toBe( | ||
| 'pair() protectionLevel must be a finite uint32 DevicePairingProtectionLevel' | ||
| ); | ||
| expect(mockBindings.pair).not.toHaveBeenCalled(); | ||
| }); | ||
| test('should surface deterministic error when binding has no pair method', () => { | ||
| mockBindings.pair = jest.fn(); | ||
| delete mockBindings.pair; | ||
| const cb = jest.fn(); | ||
| noble.pair(peripheralUuidHex, DevicePairingKinds.DisplayPin, cb); | ||
| expect(cb).toHaveBeenCalledTimes(1); | ||
| const err = cb.mock.calls[0][0]; | ||
| expect(err).toBeInstanceOf(Error); | ||
| expect(err.message).toBe('Pairing is not supported on this platform'); | ||
| }); | ||
| }); | ||
| describe('onDisconnect', () => { | ||
@@ -671,3 +852,13 @@ test('should emit disconnect on existing peripheral', () => { | ||
| describe('pair', () => { | ||
| test('should delegate to binding', () => { | ||
| // Mirror lib/pairing-kinds.js — used by these legacy tests for assertions | ||
| // when the new pair() defaults to ConfirmOnly. | ||
| const DevicePairingKinds = { | ||
| None: 0, | ||
| ConfirmOnly: 0x00000008, | ||
| }; | ||
| const DevicePairingProtectionLevel = { | ||
| Encryption: 2, | ||
| }; | ||
| test('should delegate to binding with ConfirmOnly default', () => { | ||
| const peripheralUuid = 'aabbccddeeff'; | ||
@@ -678,3 +869,7 @@ | ||
| expect(mockBindings.pair).toHaveBeenCalledWith(peripheralUuid); | ||
| expect(mockBindings.pair).toHaveBeenCalledWith( | ||
| peripheralUuid, | ||
| DevicePairingKinds.ConfirmOnly, | ||
| DevicePairingProtectionLevel.Encryption | ||
| ); | ||
| expect(mockBindings.pair).toHaveBeenCalledTimes(1); | ||
@@ -711,2 +906,9 @@ }); | ||
| describe('pairAsync', () => { | ||
| const DevicePairingKinds = { | ||
| ConfirmOnly: 0x00000008, | ||
| }; | ||
| const DevicePairingProtectionLevel = { | ||
| Encryption: 2, | ||
| }; | ||
| test('should resolve on success', async () => { | ||
@@ -721,3 +923,7 @@ const peripheralUuid = 'aabbccddeeff'; | ||
| await expect(promise).resolves.toBeUndefined(); | ||
| expect(mockBindings.pair).toHaveBeenCalledWith(peripheralUuid); | ||
| expect(mockBindings.pair).toHaveBeenCalledWith( | ||
| peripheralUuid, | ||
| DevicePairingKinds.ConfirmOnly, | ||
| DevicePairingProtectionLevel.Encryption | ||
| ); | ||
| }); | ||
@@ -724,0 +930,0 @@ |
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.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
2599441
0.82%107
2.88%19267
1.89%767
1.59%