// src/backends/bluez/bluez-backend-provider.ts import { type BluezPairingGenerationController } from './bluez-pairing-generation' import { contractError } from '../../backend-contract/errors' import type { AdapterDescriptor, BackendProvider, HostNeutralBackendIdentity } from '../../backend-contract/identity' import { UNIFIED_BLE_IMPLEMENTATION_VERSION } from '../../implementation-version' import { monotonicTimestamp, opaqueId, version, versionRange, type BackendCompatibilityOffer } from '../../backend-contract/primitives' import { BluezBackend } from './bluez-backend' import { BLUEZ_ADAPTER_INTERFACE, BLUEZ_NO_AUTHORIZATION_CONCEPT_REASON, bluezSafeReason, type BluezBusKind, type BluezDbusBoundary, type BluezDbusBoundaryFactory } from './bluez-dbus-contract' import { BluezObjectStore } from './bluez-object-store' export const BLUEZ_BACKEND_ID = 'unified-ble:bluez-dbus' export const BLUEZ_PLATFORM_ID = 'unified-ble:linux-bluez' export const BLUEZ_IMPLEMENTATION_VERSION = UNIFIED_BLE_IMPLEMENTATION_VERSION export const bluezCompatibility: BackendCompatibilityOffer = Object.freeze({ backendContract: versionRange(version('backend-contract', 1), version('backend-contract', 1)), capabilitySchema: versionRange(version('capability-schema', 1), version('capability-schema', 1)), eventSchema: versionRange(version('event-schema', 1), version('event-schema', 1)), traceFormat: versionRange(version('trace-format', 1), version('trace-format', 1)) }) export interface BluezBackendProviderOptions { readonly busKind: BluezBusKind readonly boundaryFactory: BluezDbusBoundaryFactory readonly now: () => number /** * Optional privileged operation that selects the adapter's LE pairing * generation, enabling `PairOptions.secureConnections` `'require'` and * `'disallow'` on this backend. * * This package never acquires the privilege itself. Supplying this is how a * host opts in, and the setting is ADAPTER-WIDE and OUTLIVES the process, so * read `docs/BONDING.md` before implementing one. Omit it and the capability * is reported `unsupported`, which is the default posture. */ readonly pairingGeneration?: BluezPairingGenerationController } export function createBluezBackendProvider( options: BluezBackendProviderOptions ): BackendProvider> { return { descriptor: Object.freeze({ providerId: 'unified-ble:bluez-dbus-provider', hostKind: 'node', loadability: 'loadable', compatibility: bluezCompatibility }), listAdapters: async () => withBluezProbe(options, store => adapterDescriptors(store, options.now)), create: async selection => { const boundary = await options.boundaryFactory.open(options.busKind) let store: BluezObjectStore try { store = await BluezObjectStore.open(boundary.objectManager) const adapters = adapterDescriptors(store, options.now) const selected = adapters.filter(adapter => String(adapter.adapterId) === String(selection.selectedAdapterId)) if (selected.length !== 1) { throw contractError('adapter.unavailable', 'adapter', 'bluez.provider.select-adapter') } const descriptor = selected[0] if (descriptor === undefined) { throw contractError('adapter.unavailable', 'adapter', 'bluez.provider.select-adapter') } return new BluezBackend({ boundary, store, adapter: descriptor, now: options.now, busKind: options.busKind, pairingGeneration: options.pairingGeneration }) } catch (error) { return closeBoundaryAfterFailure(boundary, error) } } } } async function withBluezProbe( options: BluezBackendProviderOptions, operation: (store: BluezObjectStore) => Value ): Promise { const boundary = await options.boundaryFactory.open(options.busKind) let store: BluezObjectStore | null = null let result: Value try { store = await BluezObjectStore.open(boundary.objectManager) result = operation(store) } catch (primaryError) { if (store !== null) { store.close() } return closeBoundaryAfterFailure(boundary, primaryError) } if (store === null) { return closeBoundaryAfterFailure(boundary, new Error('BlueZ probe did not initialize its object store')) } store.close() await boundary.close() return result } async function closeBoundaryAfterFailure(boundary: BluezDbusBoundary, primaryError: unknown): Promise { try { await boundary.close() } catch (cleanupError) { throw new AggregateError([primaryError, cleanupError], 'BlueZ operation and boundary cleanup both failed') } throw primaryError } function adapterDescriptors(store: BluezObjectStore, now: () => number): readonly AdapterDescriptor[] { return Object.freeze( store.objectsWithInterface(BLUEZ_ADAPTER_INTERFACE).map(path => { const powered = store.optionalBooleanProperty(path, BLUEZ_ADAPTER_INTERFACE, 'Powered') return Object.freeze({ adapterId: opaqueId(path, 'adapter', 'bluez'), displayName: store.optionalStringProperty(path, BLUEZ_ADAPTER_INTERFACE, 'Alias'), state: Object.freeze({ availability: 'available', authorization: 'unknown', power: powered === true ? 'on' : powered === false ? 'off' : 'unknown', backendGeneration: opaqueId('1', 'backend-generation', 'bluez'), updatedAt: monotonicTimestamp(now()), safeReason: bluezSafeReason([ powered === false ? 'BlueZ adapter is powered off' : null, BLUEZ_NO_AUTHORIZATION_CONCEPT_REASON ]) }), adapterGeneration: opaqueId('1', 'adapter-generation', `bluez:${path}`), limitations: Object.freeze([ 'AcquireWrite and AcquireNotify are unavailable until separately proven', 'Pairing dispatch and the just-works Agent1 are implemented and unit-tested, but their behavior against a live BlueZ daemon is not proven until separately verified on real radio' ]) }) }) ) }