@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-webQuick 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
- After a successful
pair()orlogin(), the library encrypts the connection keys and stores them insessionStorage. - On the next page load (within the same tab),
getAuthenticationInfo()will reporthasActiveSession: true. - Calling
login({ method: 'session' })restores the keys without any user interaction. - If
enableActivityRefreshistrue, the session is automatically extended on activity, up tomaxRefreshestimes. - Once
maxSessionAgeMsis 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
- During pairing, calling
persistWithPasskey()(or usingpair(phrase, { method: 'passkey' })) creates a WebAuthn credential and uses it to encrypt the connection keys. - The encrypted keys are stored in IndexedDB (not localStorage).
- 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 permissionPreloading 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.
