6.8 KiB
NSec Bunker Page — Architecture & Implementation Plan
Overview
Transform www/bunker.html (currently a copy of template.html) into a NIP-46 remote signer (bunker) page. The user logs in normally via nostr-login-lite, and the page uses their nsec to run a bunker backend that signs events on behalf of remote client applications.
Protocol: NIP-46 Remote Signing
NIP-46 defines a protocol where a signer (bunker) holds the private key and a client (any nostr app) sends signing requests over nostr relays using kind 24133 events encrypted with NIP-44.
Connection Flow: bunker://
sequenceDiagram
participant U as User
participant B as Bunker Page
participant R as Relay
participant C as Client App
U->>B: Logs in via nostr-login-lite
B->>B: Extracts nsec from auth state
B->>B: Creates NDKPrivateKeySigner
B->>B: Instantiates NDKNip46Backend
B->>B: Generates bunker:// URI
B->>U: Displays URI + QR code
U-->>C: Copies bunker:// URI to client app
B->>R: Subscribes to kind 24133 tagged to pubkey
C->>R: Sends connect request with secret
R->>B: Delivers connect event
B->>B: Validates secret, approves
B->>R: Sends ack response
Note over C,B: Connection established
C->>R: sign_event request
R->>B: Delivers request
B->>B: Signs with nsec
B->>R: Sends signed event response
R->>C: Delivers signed event
Architecture
Key Decision: NDKNip46Backend over nostr-tools
Why NDKNip46Backend:
- Complete bunker server implementation already in
ndk-core.bundle.js - Handles all NIP-46 methods:
connect,sign_event,get_public_key,ping,nip04_encrypt/decrypt,nip44_encrypt/decrypt - Strategy pattern for handlers — extensible
permitCallbackfor authorization decisions- Uses NDK relay pool management
- Available via
window.NDK.NDKNip46Backend
Why not nostr-tools:
- nostr-tools
nip46module only has the client side —BunkerSignerconnects to a bunker - Building a bunker server from scratch with nostr-tools would require reimplementing the entire NIP-46 RPC protocol
Dual NDK Architecture
The page uses two separate NDK contexts:
- SharedWorker NDK (existing) — handles normal page operations via
init-ndk.mjs, relay UI, blossom, etc. - Standalone NDK instance (new) — created directly in the page for
NDKNip46Backend. This is necessary because the backend needs direct access to subscribe/publish, not through the worker message bridge.
flowchart TB
subgraph Page[bunker.html]
Auth[nostr-login-lite auth]
Worker[SharedWorker NDK - page ops]
BunkerNDK[Standalone NDK - bunker backend]
Backend[NDKNip46Backend]
Signer[NDKPrivateKeySigner - nsec]
UI[Bunker UI - URI, QR, log]
end
Auth -->|nsec| Signer
Auth -->|pubkey| Worker
Signer --> Backend
BunkerNDK --> Backend
Backend -->|kind 24133| Relays[Nostr Relays]
Relays -->|sign requests| Backend
Backend -->|signed responses| Relays
Worker -->|relay list| BunkerNDK
Implementation Details
1. Script Loading
Add ndk-core.bundle.js to the page before nostr-lite.js:
<script src="./nostr.bundle.js"></script>
<script src="./ndk-core.bundle.js"></script>
<script src="./nostr-lite.js"></script>
This makes window.NDK available with all exports including:
NDK(default) — the main NDK classNDKNip46Backend— the bunker backendNDKPrivateKeySigner— signer from nsecNDKNostrRpc— RPC layer
2. Extracting the nsec
After authentication via nostr-login-lite, the secret key is stored in localStorage under key nostr_login_lite_auth as authState.secret in nsec or hex format.
function getSecretKey() {
const stored = localStorage.getItem('nostr_login_lite_auth');
if (!stored) return null;
const authState = JSON.parse(stored);
if (authState.method !== 'local' || !authState.secret) return null;
return authState.secret; // nsec1... or hex string
}
Important: The bunker only works when logged in via the "local" method (nsec/hex key). Extension and NIP-46 auth methods don't expose the secret key.
3. Standalone NDK Instance
const { default: NDKClass, NDKPrivateKeySigner, NDKNip46Backend } = window.NDK;
// Get relay URLs from the SharedWorker NDK
const relayData = await getRelayData();
const relayUrls = relayData.relays.map(r => r.url);
// Create standalone NDK for the bunker
const bunkerNdk = new NDKClass({
explicitRelayUrls: relayUrls
});
await bunkerNdk.connect();
4. NDKNip46Backend Setup
const signer = new NDKPrivateKeySigner(secretKey);
const backend = new NDKNip46Backend(bunkerNdk, signer, async (params) => {
// Auto-approve all requests, log them to UI
logRequest(params);
return true;
});
await backend.start();
5. Generating the bunker:// URI
const user = await signer.user();
const secret = crypto.getRandomValues(new Uint8Array(32));
const secretHex = Array.from(secret).map(b => b.toString(16).padStart(2, '0')).join('');
const bunkerUrl = new URL(`bunker://${user.pubkey}`);
relayUrls.forEach(r => bunkerUrl.searchParams.append('relay', r));
bunkerUrl.searchParams.set('secret', secretHex);
const bunkerUri = bunkerUrl.toString();
6. UI Layout
The divBody section will contain:
- Status indicator — bunker running/stopped, connected clients count
- bunker:// URI display — copyable text field
- QR code — scannable QR of the bunker:// URI (using existing
qrcode-generator.min.js) - Request log — scrollable list showing incoming requests with timestamp, method, remote pubkey, and result
- Start/Stop button — toggle the bunker on/off
7. Lifecycle Management
- Page load: Auth → extract nsec → start bunker → display URI
- Page close/navigate away:
beforeunloadevent to clean up subscriptions - Stop button: Tear down backend, clear URI display
- Auth requirement: Must be logged in via "local" method. Show warning if using extension/NIP-46 auth since nsec is not available.
Security Considerations
- The nsec is already in localStorage (nostr-login-lite stores it in plaintext for the "local" method). The bunker page does not introduce new storage of the key.
- The bunker:// secret is a one-time random value generated per session.
- Auto-approve means any client with the bunker:// URI can sign events. The user controls access by sharing the URI selectively.
- The page should warn users that the bunker is active and signing requests while the tab is open.
Files Modified
| File | Changes |
|---|---|
www/bunker.html |
Full page implementation: add ndk-core.bundle.js, bunker UI, backend logic |
No new JS modules needed — all logic fits in the page's inline script, using window.NDK and window.NostrTools.