npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

picomesh

v0.1.0

Published

Layered Bluetooth piconet framework for Node.js: encrypted sessions, logical addressing, HTTP-style request/response routing, and host-blind end-to-end encryption over BLE (or TCP / in-memory transports).

Downloads

12

Readme

picomesh

A layered Bluetooth piconet framework for Node.js, with host-blind end-to-end encryption. One device is the Host (BLE central: router, DHCP, and DNS in one), every other device is a Client (BLE peripheral). On top of raw BLE characteristics you get:

  • Encrypted sessions — X25519 ECDH handshake signed by long-term Ed25519 identities, AES-256-GCM per direction, all via Node's built-in crypto. No dependence on BLE pairing.
  • Reliable messaging — fragmentation/reassembly for MTU-sized radio packets, ACKs, retransmits, dedup, replay protection.
  • Logical addressing — the host assigns short addresses (host is always 1), keeps a registry, resolves human-readable names, and evicts dead devices via heartbeats.
  • HTTP-style requestsclient.request('sensor-kitchen', { method: 'POST', path: '/data/sync', body }) routed client→host→client, with Express-style handlers on every node.
  • End-to-end encryptionclient.requestSecure(...) runs a pairwise X25519 handshake with the target device through the host; the host relays the traffic but can only see routing metadata, never the method, path, headers, or body.
  • Swappable transports — the same stack runs over BLE, TCP, or fully in-memory (for dev and tests). Zero runtime dependencies; BLE libs are optional peers.
┌─────────────────────────────────────────────┐
│ E2E session   pairwise AES-GCM, host-blind  │  e2e.js (optional, requestSecure)
├─────────────────────────────────────────────┤
│ Application   REQ/RES JSON envelopes        │  host.js / client.js / handlers.js
├─────────────────────────────────────────────┤
│ Registration  addresses, names, heartbeats  │  registry.js
├─────────────────────────────────────────────┤
│ Session       ECDH handshake, AES-256-GCM   │  session.js
├─────────────────────────────────────────────┤
│ Reliability   ACK, retransmit, dedup        │  link.js
├─────────────────────────────────────────────┤
│ Framing       fragment / reassemble to MTU  │  framing.js
├─────────────────────────────────────────────┤
│ Transport     BLE · TCP · in-memory         │  src/transport/*
└─────────────────────────────────────────────┘

Install

npm install picomesh

# only where you actually use BLE:
npm install @abandonware/noble   # host machine (central)
npm install @abandonware/bleno   # client devices (peripheral)

Quickstart (no radios needed)

const { Host, Client, MemoryNetwork } = require('picomesh');

const net = new MemoryNetwork({ mtu: 185 });

// The host: router + name server
const host = new Host({ name: 'hub', transport: net.host() });
host.handle('GET', '/status', (req, res) => res.send({ ok: true }));
await host.listen();

// A device that serves a path
const sensor = new Client({ name: 'sensor-kitchen', transport: net.client() });
sensor.handle('/data/sync', (req, res) => {
  res.send({ accepted: req.body.records.length });
});
await sensor.connect();

// Another device talks to it BY NAME — routed through the host,
// encrypted per hop, fragmented to the MTU, acked and retransmitted.
const phone = new Client({ name: 'phone', transport: net.client() });
await phone.connect();
const res = await phone.request('sensor-kitchen', {
  method: 'POST',
  path: '/data/sync',
  body: { records: [{ t: 21.5 }], since: 1720800000 },
});
// res -> { status: 200, headers: {}, body: { accepted: 1 } }

// Same thing, but the host cannot read it — end-to-end encrypted,
// with the sensor's identity pinned:
const sealed = await phone.requestSecure('sensor-kitchen', {
  path: '/data/sync',
  body: { records: [{ t: 21.5 }] },
  peer: sensor.pubKeyHash, // exchange this out of band in real deployments
});
// sealed -> { status, headers, body, peer: { pubKeyHash } }

Swap MemoryNetwork for TcpHostTransport/TcpClientTransport (LAN) or BleCentralTransport/BlePeripheralTransport (real Bluetooth) and nothing else changes. Runnable versions live in examples/.

API

new Host(options)

| option | default | | |---|---|---| | transport | (required) | a host transport (net.host(), new TcpHostTransport(), new BleCentralTransport()) | | name | 'host' | human-readable name | | identity | generated | long-term Ed25519 keypair (persist it with identityToJSON) | | authorize | accept all | ({ pubKeyHash }) => boolean — vet devices during the handshake | | heartbeatTimeout | 30000 | evict devices silent for this many ms | | requestTimeout | 10000 | default timeout for host.request() |

  • host.handle([method,] path, (req, res) => …) — register a handler.
  • await host.listen() / await host.close()
  • host.devices() — registry snapshot.
  • await host.request(addressOrName, { method, path, headers, body, timeout })
  • await host.broadcast(opts) — same request to every device; Map<address, Response|Error>.
  • Events: 'join' (device), 'leave' (device, reason), 'error'.

new Client(options)

| option | default | | |---|---|---| | name | (required) | registered with the host; how other devices address you | | transport | (required) | a client transport | | identity | generated | persist for a stable device identity (and sticky address) | | trustedHosts | trust on first use | array of host pubKeyHash values to pin | | authorizePeer | accept all | ({ address, pubKeyHash }) => boolean — vet incoming end-to-end sessions | | heartbeatInterval | 10000 | ms between heartbeats | | autoReconnect | true | reconnect with exponential backoff after a drop |

  • client.handle([method,] path, handler) — serve a path (register before connect() so it's advertised).
  • await client.connect(){ address, host }
  • await client.request(target, opts) — target is 'host', a name, or an address.
  • await client.requestSecure(target, opts) — same, but end-to-end encrypted (see below); opts.peer pins the expected device pubKeyHash.
  • await client.resolve(name) / await client.devices()
  • Events: 'connect', 'disconnect', 'reconnect', 'request', 'error'.

Handlers

node.handle('/echo', (req, res) => res.send(req.body));          // any method
node.handle('GET', '/status', () => ({ ok: true }));             // returned value -> 200 body
node.handle('/fail', (req, res) => res.status(503).send({ error: 'busy' }));
// req: { method, path, headers, body, src, srcName? }

Thrown errors become a 500 response; unknown paths a 404.

Identities

const { generateIdentity, identityToJSON, identityFromJSON, publicKeyHash } = require('picomesh');

const id = generateIdentity();
fs.writeFileSync('identity.json', JSON.stringify(identityToJSON(id)));
// later:
const restored = identityFromJSON(JSON.parse(fs.readFileSync('identity.json')));

A device is known by publicKeyHash(identity.publicKey) (SHA-256 of the public key). Reconnecting with the same identity gets the same logical address back.

Security model

  • The handshake is TLS-like: the host signs its ephemeral X25519 key with its Ed25519 identity (its "certificate"); the client signs both ephemerals back (replay-proof). HKDF-SHA256 derives independent AES-256-GCM keys per direction; counters give replay protection within a session.
  • Pin the host on clients with trustedHosts: [hash]; vet clients on the host with authorize(). Without pinning you get trust-on-first-use.
  • client.request() traffic is encrypted per hop: the host decrypts and re-encrypts what it relays — exactly like a router doing TLS termination.
  • client.requestSecure() adds true end-to-end encryption between two devices. The devices run their own X25519 handshake (relayed by the host as opaque bodies, with distinct signature contexts so host-handshake messages can't be replayed into it), then seal the entire inner request/response — method, path, headers, body, status — with pairwise AES-256-GCM keys. The host still sees routing metadata (who talks to whom, when, and how much), because it has to route. The envelope id is bound inside the ciphertext so the host cannot splice responses between requests, and a sliding replay window rejects duplicated messages while tolerating out-of-order delivery.
  • End-to-end trust needs an out-of-band anchor: pass peer: '<pubKeyHash>' to requestSecure() (and/or use authorizePeer on the receiving side). Without pinning, the first e2e handshake trusts whatever identity answers — a malicious host could substitute its own. Device pubKeyHash values are visible in devices(), but for secrecy from the host you must exchange them out of band.
  • Note that advertised service paths (services, defaulting to your handler paths) are registered with the host by design. Pass services: [] if even path names are sensitive.
  • Envelope src addresses are stamped by the host on relay, so devices cannot spoof each other.

Windows BLE — the Rust bridge

Node has no working BLE peripheral bindings on Windows, but the OS itself supports the role (WinRT GattServiceProvider). This package ships a small Rust bridge (bridge/) that talks to the Windows Bluetooth stack directly — peripheral role via WinRT, central role via btleplug — and pipes datagrams to Node over a loopback socket. No dongles, no drivers, no extra hardware: it uses the built-in adapter.

npm run build:bridge     # requires Rust (rustup.rs); builds bridge/target/release/picomesh-ble-bridge.exe
// Machine 1 — host
const { Host, WinBleHostTransport } = require('picomesh');
const host = new Host({ name: 'hub', transport: new WinBleHostTransport() });

// Machine 2 — client
const { Client, WinBleClientTransport } = require('picomesh');
const client = new Client({ name: 'laptop-b', transport: new WinBleClientTransport() });

Runnable versions: examples/winble-host.js / examples/winble-client.js.

Requirements: Windows 10 1703+ and an adapter with IsPeripheralRoleSupported (check with the snippet below), Bluetooth toggled on. Most modern laptop adapters qualify.

# Does this machine support peripheral mode?
Add-Type -AssemblyName System.Runtime.WindowsRuntime
[Windows.Devices.Bluetooth.BluetoothAdapter,Windows.Devices.Bluetooth,ContentType=WindowsRuntime]::GetDefaultAsync().GetResults() |
  Select IsCentralRoleSupported, IsPeripheralRoleSupported

Platform reality check (BLE)

  • Windows ↔ Windows: use the Rust bridge transports above (WinBleHostTransport / WinBleClientTransport).
  • Linux (Raspberry Pi, embedded): use the noble/bleno transports (BleCentralTransport / BlePeripheralTransport) — no Rust needed there.
  • The two interoperate: they speak the same GATT service, so a Windows bridge host can register Linux bleno clients and vice versa.
  • A BLE central realistically holds 5–10 simultaneous connections. This is a small-cluster design; if you outgrow it, swap the transport for TCP/Wi-Fi — the layers above don't change (that's the point of the layering, and the TCP transport in this package is the proof).
  • MemoryNetwork simulates MTU, latency, and packet loss for radio-free development anywhere.

Tests

npm test

41 tests cover framing at tiny MTUs, handshake tampering/replay, routing, spoof protection, name conflicts, heartbeat eviction, auto-reconnect with sticky addresses, a 25%-frame-loss soak, end-to-end encryption (host blindness, replay rejection, peer pinning, out-of-order responses, transparent re-handshake), the full stack over real TCP sockets, and the Windows bridge protocol against a simulated radio.