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

@lightninglabs/lnc-web

v0.3.6-alpha

Published

Lightning Node Connect npm module for web

Readme

@lightninglabs/lnc-web

A JavaScript library for connecting to Lightning Network nodes via Lightning Node Connect. Supports password and passkey authentication, automatic session management, and typed RPC access to LND, Loop, Pool, Faraday, Taproot Assets, and Lightning Terminal.

Install

npm i @lightninglabs/lnc-web

Quick Start

import { LightningNodeConnect } from '@lightninglabs/lnc-web';

const lnc = new LightningNodeConnect({
  namespace: 'my-app',
  allowPasskeys: true,
  enableSessions: true,
});

// First-time pairing (connects and persists in one step)
await lnc.pair('artefact morning piano photo consider light', {
  method: 'password',
  password: 'my-secure-password',
});

// Returning user login
await lnc.login({ method: 'password', password: 'my-secure-password' });

// Access services
const info = await lnc.lnd.lightning.getInfo();
const channels = await lnc.lnd.lightning.listChannels();

Configuration

The constructor accepts a LightningNodeConnectConfig object:

const lnc = new LightningNodeConnect({
  // LNC proxy server (default: mailbox.terminal.lightning.today:443)
  serverHost: 'mailbox.terminal.lightning.today:443',

  // Custom WASM binary URL - local or remote (default: Lightning Engineering CDN)
  wasmClientCode: 'https://lightning.engineering/lnc-v0.3.5-alpha.wasm',

  // Unique namespace for this connection (default: 'default')
  namespace: 'my-app',

  // Enable passkey-based authentication (default: true)
  allowPasskeys: true,

  // Display name shown during passkey creation (default: 'LNC User ({namespace})')
  passkeyDisplayName: 'My App',

  // Enable session-based authentication (default: true)
  enableSessions: true,

  // Session timing (see Sessions section below)
  session: {
    sessionDurationMs: 24 * 60 * 60 * 1000,    // 24 hours (default)
    enableActivityRefresh: true,                 // auto-extend on activity (default)
    maxRefreshes: 10,                            // max extensions per session (default)
    maxSessionAgeMs: 7 * 24 * 60 * 60 * 1000,   // absolute max age: 7 days (default)
  },
});

Custom Proxy Server

Some consumer apps construct the LightningNodeConnect instance on page load but allow users to specify a custom proxy server on the pairing screen. Use the serverHost setter:

const lnc = new LightningNodeConnect({ namespace: 'my-app' });

// Later, when the user provides a custom host...
lnc.serverHost = 'custom.proxy-server.host:443';

await lnc.pair(pairingPhrase, { method: 'password', password });

Pairing

Pairing is a one-time operation that connects your app to a Lightning node using a pairing phrase generated by LND or Lightning Terminal. After pairing and persisting credentials, the connection keys are stored locally so the user doesn't need to pair again.

One-step pairing (recommended)

Connect and persist credentials in a single call:

// With password
await lnc.pair('artefact morning piano photo consider light', {
  method: 'password',
  password: 'my-secure-password',
});

// With passkey (biometric / hardware key)
await lnc.pair('artefact morning piano photo consider light', {
  method: 'passkey',
});

Two-step pairing

For more control (e.g., verifying the connection before persisting):

// Step 1: pair and connect
await lnc.pair('artefact morning piano photo consider light');

// Step 2: verify the connection works
await lnc.lnd.lightning.listChannels();

// Step 3: persist credentials
await lnc.persistWithPassword('my-secure-password');
// or: await lnc.persistWithPasskey();

Detecting Saved Credentials

Use getAuthenticationInfo() to check whether the user has previously paired and how they should log in:

const auth = await lnc.getAuthenticationInfo();

if (!auth.hasStoredCredentials) {
  // New user - show pairing screen
  showPairingScreen();
} else if (auth.hasActiveSession) {
  // Active session - can auto-connect (see Sessions below)
  await lnc.login({ method: 'session' });
} else if (auth.hasPasskey) {
  // Returning user with passkey - prompt for biometric
  await lnc.login({ method: 'passkey' });
} else {
  // Returning user with password - prompt for password
  await lnc.login({ method: 'password', password: userInput });
}

The AuthenticationInfo object contains:

| Field | Type | Description | | ----------------------- | --------- | ------------------------------------------------------- | | isUnlocked | boolean | True if credentials have been decrypted into memory | | hasStoredCredentials | boolean | True if long-term credentials exist (password or passkey) | | hasActiveSession | boolean | True if a valid session exists for passwordless login | | sessionTimeRemaining | number | Milliseconds until the current session expires | | supportsPasskeys | boolean | True if passkeys are enabled in config and supported by the browser | | hasPasskey | boolean | True if a passkey credential has been stored | | preferredUnlockMethod | string | 'session', 'passkey', or 'password' - recommended method based on current state | | passkeyCredentialId | string? | The stored passkey credential ID, if any (for reusing across namespaces) |

Login

Returning users authenticate with one of three methods:

// Password
await lnc.login({ method: 'password', password: 'my-secure-password' });

// Passkey (triggers browser biometric / hardware key prompt)
await lnc.login({ method: 'passkey' });

// Session (no user interaction needed)
await lnc.login({ method: 'session' });

login() is a convenience that calls unlock() (decrypts credentials) followed by connect() (connects to the proxy). It throws if unlock fails, so wrap in try/catch. You can also call them separately if you need to inspect state between the two steps:

const success = await lnc.unlock({ method: 'password', password });
if (success) {
  await lnc.connect();
}

Accessing Services

All Lightning services are available as typed properties on the lnc object. Sub-services are nested, and all names are camelCased.

const { lnd, loop, pool, faraday, tapd, lit } = lnc;

// LND
const info = await lnd.lightning.getInfo();
const channels = await lnd.lightning.listChannels();
const invoices = await lnd.lightning.listInvoices();
await lnd.lightning.connectPeer({
  addr: { pubkey: '03aa49c1...', host: 'host:9735' },
});
const signature = await lnd.signer.signMessage({ msg: myBuffer });

// Loop
const swaps = await loop.swapClient.listSwaps();

// Pool
const account = await pool.trader.initAccount({
  accountValue: 100000000,
  relativeHeight: 1000,
});

// Faraday
const insights = await faraday.faradayServer.channelInsights();

// Taproot Assets
const assets = await tapd.taprootAssets.listAssets();

// Lightning Terminal
const sessions = await lit.sessions.listSessions();

Subscriptions

For streaming RPC endpoints, pass a message handler and an optional error handler:

// Subscribe to on-chain transactions
lnc.lnd.lightning.subscribeTransactions(
  {},
  (transaction) => console.log('New tx:', transaction),
  (error) => console.error('Stream error:', error),
);

// Subscribe to channel events
lnc.lnd.lightning.subscribeChannelEvents(
  {},
  (event) => console.log('Channel event:', event),
  (error) => console.error('Stream error:', error),
);

Sessions

When enableSessions is true (the default), a session is automatically created after each successful connection. Sessions allow returning users to reconnect without re-entering their password or triggering a passkey prompt, as long as the browser tab/window remains open.

Sessions are stored in sessionStorage and are scoped to the browser tab. They expire based on the session config.

How sessions work

  1. After a successful pair() or login(), the library encrypts the connection keys and stores them in sessionStorage.
  2. On the next page load (within the same tab), getAuthenticationInfo() will report hasActiveSession: true.
  3. Calling login({ method: 'session' }) restores the keys without any user interaction.
  4. If enableActivityRefresh is true, the session is automatically extended on activity, up to maxRefreshes times.
  5. Once maxSessionAgeMs is reached or the tab is closed, the session expires.

Auto-restore on page load

You can use tryAutoRestore() to pre-check whether a session can be restored, without connecting:

const restored = await lnc.tryAutoRestore();
if (restored) {
  // Credentials are in memory - connect when ready
  await lnc.connect();
}

Session configuration

const lnc = new LightningNodeConnect({
  enableSessions: true,
  session: {
    sessionDurationMs: 30 * 60 * 1000,    // 30 minutes
    enableActivityRefresh: true,            // extend session on activity
    maxRefreshes: 5,                        // allow up to 5 extensions
    maxSessionAgeMs: 4 * 60 * 60 * 1000,   // hard cap at 4 hours
  },
});

To disable sessions entirely:

const lnc = new LightningNodeConnect({ enableSessions: false });

Passkeys

Passkeys use the WebAuthn standard to encrypt stored credentials with biometric authentication (fingerprint, Face ID) or a hardware security key. This provides a passwordless experience for returning users.

How passkeys work

  1. During pairing, calling persistWithPasskey() (or using pair(phrase, { method: 'passkey' })) creates a WebAuthn credential and uses it to encrypt the connection keys.
  2. The encrypted keys are stored in IndexedDB (not localStorage).
  3. On subsequent visits, calling login({ method: 'passkey' }) triggers the browser's biometric prompt. The resulting WebAuthn assertion is used to decrypt the keys.

Checking passkey support

// Instance method - checks config AND browser support
if (lnc.supportsPasskeys()) {
  // Show passkey option in UI
}

// Static method - checks browser support only (no instance needed)
if (await LightningNodeConnect.isPasskeySupported()) {
  // Browser supports passkeys
}

Passkey configuration

const lnc = new LightningNodeConnect({
  allowPasskeys: true,                  // enable passkey support (default: true)
  passkeyDisplayName: 'My Lightning App', // name shown during passkey creation
});

To disable passkeys:

const lnc = new LightningNodeConnect({ allowPasskeys: false });

Clearing Credentials

The clear() method supports two levels of cleanup:

// Logout - clears session only (default). User can still log back in with password/passkey.
lnc.clear();

// Forget this node - clears session AND long-term stored credentials.
// User will need to pair again with a new phrase.
lnc.clear({ persisted: true });

clear() options:

| Option | Default | Description | | ----------- | ------- | -------------------------------------------------------- | | session | true | Clear the short-term session from sessionStorage | | persisted | false | Clear long-term credentials from localStorage / IndexedDB |

Note: clear() only removes stored credentials — it does not tear down the active WASM connection. To fully disconnect, perform a page reload after clearing (e.g. window.location.reload()).

Status Properties

lnc.isReady;        // true when WASM client is loaded and ready
lnc.isConnected;    // true when connected to the LNC proxy server
lnc.status;         // current status string
lnc.expiry;         // Date when the LNC session token expires
lnc.isReadOnly;     // true if the connection has read-only permissions

lnc.hasPerms('lnrpc.Lightning.SendPaymentSync');  // check a specific permission

Preloading WASM

The WASM binary (~5 MB) is downloaded on first connection. To improve perceived performance, preload it on your pairing/login screens while waiting for user input:

const lnc = new LightningNodeConnect({ namespace: 'my-app' });

// Start downloading WASM immediately (e.g., in a useEffect or on app mount)
lnc.preload();

// Later, when the user is ready to pair or log in, the WASM is already cached
await lnc.pair(phrase, { method: 'password', password });

Multiple Connections

Use unique namespace values to maintain simultaneous connections to different nodes:

const alice = new LightningNodeConnect({ namespace: 'alice' });
const bob = new LightningNodeConnect({ namespace: 'bob' });

await alice.pair(alicePhrase, { method: 'password', password: alicePassword });
await bob.pair(bobPhrase, { method: 'password', password: bobPassword });

// Each instance has its own credentials, sessions, and service objects
const aliceChannels = await alice.lnd.lightning.listChannels();
const bobChannels = await bob.lnd.lightning.listChannels();

Credentials, sessions, and passkeys are all scoped to the namespace, so they won't collide.

React Integration

A common pattern is to create a singleton instance in a custom hook:

import { useCallback, useEffect, useState } from 'react';
import {
  AuthenticationInfo,
  LightningNodeConnect,
  PersistOptions,
  UnlockOptions,
} from '@lightninglabs/lnc-web';

const lnc = new LightningNodeConnect({
  namespace: 'my-app',
  allowPasskeys: true,
  enableSessions: true,
});

const useLNC = () => {
  const [auth, setAuth] = useState<AuthenticationInfo | null>(null);

  useEffect(() => {
    lnc.getAuthenticationInfo().then(setAuth);
  }, []);

  const pair = useCallback(
    async (phrase: string, options: PersistOptions) => {
      await lnc.pair(phrase);
      await lnc.lnd.lightning.listChannels(); // verify connection

      if (options.method === 'password') {
        await lnc.persistWithPassword(options.password);
      } else if (options.method === 'passkey') {
        await lnc.persistWithPasskey();
      }
    },
    [],
  );

  const login = useCallback(async (options: UnlockOptions) => {
    await lnc.login(options);
  }, []);

  const logout = useCallback(() => {
    lnc.clear();
    window.location.reload();
  }, []);

  return { lnc, pair, login, logout, auth };
};

export default useLNC;

Auto-connect with sessions

Restore sessions automatically when the page loads:

import { useEffect, useState } from 'react';
import useLNC from './useLNC';

export const useAutoConnect = () => {
  const { lnc, login, auth } = useLNC();
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');

  useEffect(() => {
    if (auth?.hasActiveSession && !lnc.isConnected) {
      setLoading(true);
      login({ method: 'session' })
        .catch((err) => setError((err as Error).message))
        .finally(() => setLoading(false));
    }
  }, [auth?.hasActiveSession, lnc.isConnected, login]);

  return { loading, error };
};

Legacy API

The original LNC class is still available as the default export for backwards compatibility:

import LNC from '@lightninglabs/lnc-web';

See LEGACY_LNC.md for the legacy API documentation and LEGACY_MIGRATE.md for migration instructions.

Further Documentation