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

p2p-portal-drop

v0.1.0

Published

Framework-agnostic, LAN-only peer-to-peer file-share Web SDK for portals. Pairs a browser with native apps (Android/desktop) via QR or a short code and transfers files directly over the browser's native WebRTC — bytes never touch a server.

Downloads

157

Readme

p2p-portal-drop

Framework-agnostic, LAN-only embeddable file-share Web SDK for third-party portals.

The SDK speaks the LocalSend signaling protocol over WebSocket and transfers files directly between devices using the browser's native WebRTC. File bytes travel peer to peer over the local network and never pass through the signaling server.

  • Framework-agnostic — no runtime dependency on React, Vue, Svelte, or any UI library.
  • No npm runtime dependencies — compression uses the browser-native Compression Streams API; crypto uses Web Crypto.
  • Typed end to end — ships complete TypeScript definitions for the whole public surface.
  • LAN-only by design — host-only ICE ({ iceServers: [] }); no STUN, no TURN, no relays.
  • Streaming — received files stream straight to storage, so transfers can exceed tab memory.

Install

npm install p2p-portal-drop

Requires a secure browser context (https:// or localhost) for Web Crypto and WebRTC.

Quick start

import { createFileShareClient } from 'p2p-portal-drop';

const client = createFileShareClient();

// Observe the typed transfer lifecycle (Requirement 8.6).
client.on('request-received', (e) => {
  // Prompt the user, then accept or reject by transfer id.
  client.acceptTransfer(e.transferId);
});
client.on('progress', (e) => console.log(`${e.bytesTransferred}/${e.totalBytes}`));
client.on('completed', (e) => console.log('done', e.transferId));
client.on('failed', (e) => console.warn('failed', e.transferId, e.reason));

// Join a private session from a scanned QR payload.
const session = await client.joinSession({
  signalingUrl: 'wss://10.0.0.5:8080',
  sessionId: '<from-qr>',
  pairingToken: '<from-qr>',
  clientInfo: { alias: 'My Portal', version: '2.3', token: 'stable-merge-token' },
  onPeersChanged: (peers) => render(peers), // optional peer-list observer
});

// Send files to the paired peer.
const handle = await client.sendTransferRequest([
  { fileName: 'photo.jpg', fileType: 'image/jpeg', data: fileBlob },
]);
console.log('sending', handle.transferId);

Public API

createFileShareClient(options?) returns a FileShareClient:

| Method | Description | | --- | --- | | joinSession(opts) | Connect to a private session within the configured timeout; resolves with a SessionHandle. | | sendTransferRequest(files) | Propose a transfer to the paired peer; resolves with a TransferHandle. | | acceptTransfer(transferId) | Accept an incoming request (all proposed files). | | rejectTransfer(transferId) | Decline an incoming request; nothing is written. | | on(event, handler) | Subscribe to a typed lifecycle event; returns an unsubscribe function. |

Events: request-received, progress, completed, failed, connection-error, capability-error. Every event carries its type; transfer-scoped events carry a transferId.

SessionHandle exposes sessionId, peers() (a metadata-bearing snapshot), and leave().

Creating vs joining a session

The device that displays the QR creates the session; the device that scans it joins. Set create: true in joinSession on the creating side — it registers the derived token validator with the server (the raw token never leaves the device) and joins as the first peer. The scanning side leaves create unset.

// Creator (shows the QR)
await client.joinSession({ ...opts, create: true });
// Joiner (scanned the QR)
await client.joinSession({ ...opts });

Configuration

createFileShareClient({
  credentials,            // HandshakeCredentials — see "Security" below
  storage,                // FileStorage sink for received files (default: browser download)
  capabilityEnvironment,  // override the probed browser globals (testing)
  createSignalingClient,  // inject a signaling-client factory (testing)
  dataChannelLabel,       // default 'localsend'
  generateTransferId,     // deterministic ids for tests
});

For files larger than tab memory, pass createDirectoryFileStorage(dirHandle) (File System Access API) instead of the default download sink.

Architecture

QR payload ──► joinSession ──► SignalingClient (WebSocket, /v1/ws)
                                   │  HELLO / JOIN / UPDATE / LEFT / OFFER / ANSWER
                                   ▼
                         host-only RTCPeerConnection  ◄── SDP codec (zlib + base64url)
                                   │  RTCDataChannel
                                   ▼
                  handshake (nonce → signed token) ──► gated file transfer
                                   │
                                   ▼
                         streaming FileStorage sink

The SDK re-implements the LocalSend data-channel sequence (nonce → token → optional PIN → file list → file-token map → per-file header + chunked binary) so it interoperates with the native Rust clients. No file byte is sent before the nonce and signed-token exchange complete.

Security model

  • LAN-only: ICE is host-only; connectivity uses local-interface candidates with no external servers.
  • Pairing secret stays local: the QR carries a pairing token; the SDK derives a one-way HMAC-SHA256 validator and sends only that to the server — never the raw token.
  • Handshake gating: byte transfer is gated behind the nonce + signed-token exchange.

Production handshake credentials. The default HandshakeCredentials is a deterministic, non-cryptographic token that only satisfies the handshake's byte-sequencing so two SDK (browser) peers complete a transfer. For real signature verification and interoperability with native clients, inject the built-in Ed25519 credentials (Web Crypto), which produce and verify the exact crypto::token HASH.SIGN token the Rust core expects:

import { createFileShareClient, createTokenCredentials, generateSigningKeyPair } from 'p2p-portal-drop';

const keyPair = await generateSigningKeyPair();              // Ed25519
const client = createFileShareClient({
  credentials: createTokenCredentials({
    keyPair,
    expectedPeerPublicKey, // the paired peer's public key (out-of-band); enables verification
  }),
});

A failed signature terminates the transfer with zero file bytes. Ed25519 Web Crypto requires Node 20+, Chrome 137+, Safari 17+, or Firefox 129+.

Demo Portal

A runnable reference portal lives in demo/. It embeds this SDK to demonstrate QR pairing, the live peer list, bidirectional transfer, and the event-driven status log. See demo/README.md.

Develop

npm install
npm run typecheck   # strict type-check, no emit
npm test            # vitest unit + property tests
npm run build       # emit dist/ (.js + .d.ts)

Property-based tests (via fast-check) cover the signaling round-trip, the typed event surface, and capability gating.

End-to-end signaling harness

npm run build                 # the harness imports the built dist/
cd ../server && cargo run     # start a signaling server (another shell)
npm run harness:signaling     # drive the real server with two SDK clients

The harness validates create-intent registration, join validation, mutual peer presentation, and session-scoped OFFER/ANSWER routing against the real server. See harness/README.md.

Browser end-to-end test (real WebRTC)

The e2e/ package runs two real Chromium pages (via Playwright) through a genuine WebRTC data-channel transfer against a live signaling server — the one hop the Node harness can't cover:

cd e2e
npm install && npm run install:browser
npm test       # builds the SDK, starts the servers, runs the transfer

See e2e/README.md.

License

MIT — see LICENSE. This SDK is original WrapDrive code and is published independently under MIT for easy embedding. The wider WrapDrive project (native app + server) builds on LocalSend and is Apache-2.0; the SDK implements the LocalSend signaling/handshake protocol (a wire format), not its code.