npm package: @lightninglabs/lnc-web
Description: Lightning Node Connect npm module for web
Author: Evan Kaloudis
Latest version: 0.3.6-alpha

# @lightninglabs/lnc-web

A JavaScript library for connecting to Lightning Network nodes via [Lightning Node Connect](https://docs.lightning.engineering/lightning-network-tools/lightning-terminal/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

```bash
npm i @lightninglabs/lnc-web
```

## Quick Start

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
// 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):

```typescript
// 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:

```typescript
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:

```typescript
// 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:

```typescript
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.

```typescript
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:

```typescript
// 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:

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

### Session configuration

```typescript
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:

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

## Passkeys

Passkeys use the [WebAuthn](https://webauthn.guide/) 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

```typescript
// 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

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

To disable passkeys:

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

## Clearing Credentials

The `clear()` method supports two levels of cleanup:

```typescript
// 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

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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:

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

See [LEGACY_LNC.md](LEGACY_LNC.md) for the legacy API documentation and [LEGACY_MIGRATE.md](LEGACY_MIGRATE.md) for migration instructions.

## Further Documentation

- [Lightning Node Connect](https://docs.lightning.engineering/lightning-network-tools/lightning-terminal/lightning-node-connect)
- [LNC npm package docs](https://docs.lightning.engineering/lightning-network-tools/lightning-terminal/lnc-npm)