Browse Source

Remove bunker (NIP-46) functionality from web UI (v0.44.7)

- Delete BunkerView.svelte component and all bunker UI
- Remove bunker-service.js and bunker-worker.js implementations
- Clean up bunker stores and worker management from stores.js
- Remove getBunkerURL and getBunkerInfo API functions
- Remove bunker tab from navigation and App.svelte imports
- Simplify rollup.config.js by removing bunker-worker build
- Remove NIP46 token scope from cashu-client.js

Files modified:
- app/web/src/BunkerView.svelte: Deleted
- app/web/src/bunker-service.js: Deleted
- app/web/src/bunker-worker.js: Deleted
- app/web/src/stores.js: Removed bunker state and worker functions
- app/web/src/api.js: Removed bunker API functions
- app/web/src/App.svelte: Removed bunker tab and imports
- app/web/rollup.config.js: Simplified to single bundle
- app/web/src/cashu-client.js: Removed NIP46 scope
- pkg/version/version: Bumped to v0.44.7

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
main
mleku 2 weeks ago
parent
commit
0008d33792
  1. 3
      .claude/settings.local.json
  2. 1
      app/web/dist/bundle.css
  3. 40
      app/web/dist/bundle.js
  4. 2
      app/web/dist/bundle.js.map
  5. 23
      app/web/rollup.config.js
  6. 19
      app/web/src/App.svelte
  7. 1192
      app/web/src/BunkerView.svelte
  8. 33
      app/web/src/api.js
  9. 508
      app/web/src/bunker-service.js
  10. 423
      app/web/src/bunker-worker.js
  11. 5
      app/web/src/cashu-client.js
  12. 112
      app/web/src/stores.js
  13. 2
      pkg/version/version

3
.claude/settings.local.json

@ -5,7 +5,8 @@ @@ -5,7 +5,8 @@
"ask": [],
"additionalDirectories": [
"/home/mleku/smesh",
"/home/mleku/Tourmaline"
"/home/mleku/Tourmaline",
"/home/mleku/Amber"
]
},
"outputStyle": "Default",

1
app/web/dist/bundle.css vendored

File diff suppressed because one or more lines are too long

40
app/web/dist/bundle.js vendored

File diff suppressed because one or more lines are too long

2
app/web/dist/bundle.js.map vendored

File diff suppressed because one or more lines are too long

23
app/web/rollup.config.js

@ -34,8 +34,7 @@ function serve() { @@ -34,8 +34,7 @@ function serve() {
};
}
// Main app bundle
const mainConfig = {
export default {
input: "src/main.js",
output: {
sourcemap: true,
@ -96,23 +95,3 @@ const mainConfig = { @@ -96,23 +95,3 @@ const mainConfig = {
clearScreen: false,
},
};
// Bunker worker bundle (runs in Web Worker context)
const workerConfig = {
input: "src/bunker-worker.js",
output: {
sourcemap: true,
format: "iife",
name: "bunkerWorker",
file: `${outputDir}/bunker-worker.js`,
},
plugins: [
resolve({
browser: true,
}),
commonjs(),
production && terser(),
],
};
export default [mainConfig, workerConfig];

19
app/web/src/App.svelte

@ -12,7 +12,6 @@ @@ -12,7 +12,6 @@
import SprocketView from "./SprocketView.svelte";
import PolicyView from "./PolicyView.svelte";
import BlossomView from "./BlossomView.svelte";
import BunkerView from "./BunkerView.svelte";
import LogView from "./LogView.svelte";
import SearchResultsView from "./SearchResultsView.svelte";
import FilterDisplay from "./FilterDisplay.svelte";
@ -56,7 +55,6 @@ @@ -56,7 +55,6 @@
let userProfile = null;
let userRole = "";
let userSigner = null;
let userPrivkey = null; // User's private key for bunker service (only set for nsec login)
let showSettingsDrawer = false;
let selectedTab = localStorage.getItem("selectedTab") || "export";
let showFilterBuilder = false; // Show filter builder in events view
@ -1651,7 +1649,6 @@ @@ -1651,7 +1649,6 @@
{ id: "import", icon: "💾", label: "Import", requiresAdmin: true },
{ id: "events", icon: "📡", label: "Events" },
{ id: "blossom", icon: "🌸", label: "Blossom" },
{ id: "bunker", icon: "🔐", label: "Bunker", requiresWrite: true },
{ id: "compose", icon: "✏", label: "Compose", requiresWrite: true },
{ id: "recovery", icon: "🔄", label: "Recovery" },
{
@ -1750,13 +1747,6 @@ @@ -1750,13 +1747,6 @@
userSigner = signer;
showLoginModal = false;
// Store private key for bunker service (only for nsec login)
if (method === "nsec" && privateKey) {
userPrivkey = privateKey;
} else {
userPrivkey = null;
}
// Initialize Nostr client and fetch profile
try {
await initializeNostrClient();
@ -2824,15 +2814,6 @@ @@ -2824,15 +2814,6 @@
{currentEffectiveRole}
on:openLoginModal={openLoginModal}
/>
{:else if selectedTab === "bunker"}
<BunkerView
{isLoggedIn}
{userPubkey}
{userSigner}
{userPrivkey}
{currentEffectiveRole}
on:openLoginModal={openLoginModal}
/>
{:else if selectedTab === "compose"}
<ComposeView
bind:composeEventJson

1192
app/web/src/BunkerView.svelte

File diff suppressed because it is too large Load Diff

33
app/web/src/api.js

@ -405,7 +405,7 @@ export async function importEvents(signer, pubkey, file) { @@ -405,7 +405,7 @@ export async function importEvents(signer, pubkey, file) {
return await response.json();
}
// ==================== WireGuard/Bunker API ====================
// ==================== WireGuard API ====================
/**
* Fetch WireGuard status
@ -462,24 +462,6 @@ export async function regenerateWireGuard(signer, pubkey) { @@ -462,24 +462,6 @@ export async function regenerateWireGuard(signer, pubkey) {
return await response.json();
}
/**
* Get Bunker URL for the authenticated user
* @param {object} signer - The signer instance
* @param {string} pubkey - User's pubkey
* @returns {Promise<object>} Bunker URL info
*/
export async function getBunkerURL(signer, pubkey) {
const url = `${window.location.origin}/api/bunker/url`;
const authHeader = await createNIP98Auth(signer, pubkey, "GET", url);
const response = await fetch(url, {
headers: authHeader ? { Authorization: `Nostr ${authHeader}` } : {},
});
if (!response.ok) {
const error = await response.text();
throw new Error(error || `Failed to get bunker URL: ${response.statusText}`);
}
return await response.json();
}
/**
* Get WireGuard audit log (revoked keys and access attempts)
@ -500,16 +482,3 @@ export async function getWireGuardAudit(signer, pubkey) { @@ -500,16 +482,3 @@ export async function getWireGuardAudit(signer, pubkey) {
return await response.json();
}
/**
* Get Bunker connection info (public endpoint)
* @returns {Promise<object>} Bunker info including relay URL, ACL mode, and CAT status
*/
export async function getBunkerInfo() {
const url = `${window.location.origin}/api/bunker/info`;
const response = await fetch(url);
if (!response.ok) {
const error = await response.text();
throw new Error(error || `Failed to get bunker info: ${response.statusText}`);
}
return await response.json();
}

508
app/web/src/bunker-service.js

@ -1,508 +0,0 @@ @@ -1,508 +0,0 @@
/**
* BunkerService - NIP-46 Remote Signer
*
* Implements the signer side of NIP-46 protocol.
* Listens for signing requests from remote clients and responds using
* the user's private key stored in ORLY.
*
* Protocol:
* - Kind 24133 events for request/response
* - NIP-04 encryption for payloads
* - Methods: connect, get_public_key, sign_event, nip04_encrypt, nip04_decrypt, ping
*/
import { nip04 } from 'nostr-tools';
import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
import { secp256k1 } from '@noble/curves/secp256k1';
import { encodeToken } from './cashu-client.js';
// NIP-46 methods
const NIP46_METHOD = {
CONNECT: 'connect',
GET_PUBLIC_KEY: 'get_public_key',
SIGN_EVENT: 'sign_event',
NIP04_ENCRYPT: 'nip04_encrypt',
NIP04_DECRYPT: 'nip04_decrypt',
PING: 'ping'
};
/**
* Generate a random hex string.
*/
function generateRandomHex(bytes = 16) {
const arr = new Uint8Array(bytes);
crypto.getRandomValues(arr);
return bytesToHex(arr);
}
/**
* BunkerService class - implements NIP-46 signer protocol.
*/
export class BunkerService {
/**
* @param {string} relayUrl - WebSocket URL of the relay
* @param {string} userPubkey - User's public key (hex)
* @param {Uint8Array} userPrivkey - User's private key (32 bytes)
*/
constructor(relayUrl, userPubkey, userPrivkey) {
this.relayUrl = relayUrl;
this.userPubkey = userPubkey;
this.userPrivkey = userPrivkey;
this.ws = null;
this.connected = false;
this.allowedSecrets = new Set();
this.connectedClients = new Map(); // pubkey -> { connectedAt, lastActivity }
this.requestLog = [];
this.heartbeatInterval = null;
this.subscriptionId = null;
this.catToken = null;
// Callbacks
this.onClientConnected = null;
this.onClientDisconnected = null;
this.onRequest = null;
this.onStatusChange = null;
}
/**
* Add an allowed connection secret.
*/
addAllowedSecret(secret) {
this.allowedSecrets.add(secret);
}
/**
* Remove an allowed secret.
*/
removeAllowedSecret(secret) {
this.allowedSecrets.delete(secret);
}
/**
* Set CAT token for WebSocket connection.
*/
setCatToken(token) {
this.catToken = token;
}
/**
* Connect to the relay and start listening for NIP-46 requests.
*/
async connect() {
return new Promise((resolve, reject) => {
// Build WebSocket URL
let wsUrl = this.relayUrl;
if (wsUrl.startsWith('http://')) {
wsUrl = 'ws://' + wsUrl.slice(7);
} else if (wsUrl.startsWith('https://')) {
wsUrl = 'wss://' + wsUrl.slice(8);
} else if (!wsUrl.startsWith('ws://') && !wsUrl.startsWith('wss://')) {
wsUrl = 'wss://' + wsUrl;
}
// Add CAT token if available
if (this.catToken) {
const tokenEncoded = encodeToken(this.catToken);
const url = new URL(wsUrl);
url.searchParams.set('token', tokenEncoded);
wsUrl = url.toString();
}
console.log('[BunkerService] Connecting to:', wsUrl.split('?')[0]);
const ws = new WebSocket(wsUrl);
const timeout = setTimeout(() => {
ws.close();
reject(new Error('Connection timeout'));
}, 10000);
ws.onopen = () => {
clearTimeout(timeout);
this.ws = ws;
this.connected = true;
console.log('[BunkerService] Connected to relay');
// Subscribe to NIP-46 events for our pubkey
this.subscriptionId = generateRandomHex(8);
const sub = JSON.stringify([
'REQ',
this.subscriptionId,
{
kinds: [24133],
'#p': [this.userPubkey],
since: Math.floor(Date.now() / 1000) - 60
}
]);
ws.send(sub);
console.log('[BunkerService] Subscribed to NIP-46 events');
// Start heartbeat
this.startHeartbeat();
if (this.onStatusChange) {
this.onStatusChange('connected');
}
resolve();
};
ws.onerror = (error) => {
clearTimeout(timeout);
console.error('[BunkerService] WebSocket error:', error);
reject(new Error('WebSocket error'));
};
ws.onclose = () => {
this.connected = false;
this.ws = null;
this.stopHeartbeat();
console.log('[BunkerService] Disconnected from relay');
if (this.onStatusChange) {
this.onStatusChange('disconnected');
}
};
ws.onmessage = (event) => {
this.handleMessage(event.data);
};
});
}
/**
* Start WebSocket heartbeat to keep connection alive.
*/
startHeartbeat(intervalMs = 30000) {
this.stopHeartbeat();
this.heartbeatInterval = setInterval(() => {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
// Send a ping via Nostr protocol (re-subscribe)
const sub = JSON.stringify([
'REQ',
this.subscriptionId,
{
kinds: [24133],
'#p': [this.userPubkey],
since: Math.floor(Date.now() / 1000) - 60
}
]);
this.ws.send(sub);
}
}, intervalMs);
}
/**
* Stop WebSocket heartbeat.
*/
stopHeartbeat() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
}
/**
* Disconnect from the relay.
*/
disconnect() {
this.stopHeartbeat();
if (this.ws) {
// Close subscription
if (this.subscriptionId) {
this.ws.send(JSON.stringify(['CLOSE', this.subscriptionId]));
}
this.ws.close();
this.ws = null;
}
this.connected = false;
this.connectedClients.clear();
}
/**
* Handle incoming WebSocket messages.
*/
async handleMessage(data) {
try {
const msg = JSON.parse(data);
if (!Array.isArray(msg)) return;
const [type, ...rest] = msg;
if (type === 'EVENT') {
const [, event] = rest;
if (event.kind === 24133) {
await this.handleNIP46Request(event);
}
} else if (type === 'OK') {
// Event published confirmation
console.log('[BunkerService] Event published:', rest[0]?.substring(0, 8));
} else if (type === 'NOTICE') {
console.warn('[BunkerService] Relay notice:', rest[0]);
}
} catch (err) {
console.error('[BunkerService] Failed to parse message:', err);
}
}
/**
* Handle NIP-46 request event.
*/
async handleNIP46Request(event) {
try {
// Decrypt the content with NIP-04
const privkeyHex = bytesToHex(this.userPrivkey);
const decrypted = await nip04.decrypt(privkeyHex, event.pubkey, event.content);
const request = JSON.parse(decrypted);
console.log('[BunkerService] Received request:', request.method, 'from:', event.pubkey.substring(0, 8));
// Log the request
this.requestLog.push({
id: request.id,
method: request.method,
from: event.pubkey,
timestamp: Date.now()
});
if (this.requestLog.length > 100) {
this.requestLog.shift();
}
if (this.onRequest) {
this.onRequest(request, event.pubkey);
}
// Handle the request
let result = null;
let error = null;
try {
switch (request.method) {
case NIP46_METHOD.CONNECT:
result = await this.handleConnect(request, event.pubkey);
break;
case NIP46_METHOD.GET_PUBLIC_KEY:
result = await this.handleGetPublicKey(request, event.pubkey);
break;
case NIP46_METHOD.SIGN_EVENT:
result = await this.handleSignEvent(request, event.pubkey);
break;
case NIP46_METHOD.NIP04_ENCRYPT:
result = await this.handleNip04Encrypt(request, event.pubkey);
break;
case NIP46_METHOD.NIP04_DECRYPT:
result = await this.handleNip04Decrypt(request, event.pubkey);
break;
case NIP46_METHOD.PING:
result = 'pong';
break;
default:
error = `Unknown method: ${request.method}`;
}
} catch (err) {
console.error('[BunkerService] Error handling request:', err);
error = err.message;
}
// Send response
await this.sendResponse(request.id, result, error, event.pubkey);
} catch (err) {
console.error('[BunkerService] Failed to handle NIP-46 request:', err);
}
}
/**
* Handle connect request.
*/
async handleConnect(request, senderPubkey) {
const [clientPubkey, secret] = request.params;
// Validate secret if required
if (this.allowedSecrets.size > 0) {
if (!secret || !this.allowedSecrets.has(secret)) {
throw new Error('Invalid or missing connection secret');
}
}
// Register connected client
this.connectedClients.set(senderPubkey, {
clientPubkey: clientPubkey || senderPubkey,
connectedAt: Date.now(),
lastActivity: Date.now()
});
console.log('[BunkerService] Client connected:', senderPubkey.substring(0, 8));
if (this.onClientConnected) {
this.onClientConnected(senderPubkey);
}
return 'ack';
}
/**
* Handle get_public_key request.
*/
async handleGetPublicKey(request, senderPubkey) {
// Update last activity
if (this.connectedClients.has(senderPubkey)) {
this.connectedClients.get(senderPubkey).lastActivity = Date.now();
}
return this.userPubkey;
}
/**
* Handle sign_event request.
*/
async handleSignEvent(request, senderPubkey) {
// Check if client is connected
if (!this.connectedClients.has(senderPubkey)) {
throw new Error('Not connected');
}
// Update last activity
this.connectedClients.get(senderPubkey).lastActivity = Date.now();
const [eventJson] = request.params;
const event = JSON.parse(eventJson);
// Ensure pubkey matches
if (event.pubkey && event.pubkey !== this.userPubkey) {
throw new Error('Event pubkey does not match signer pubkey');
}
// Set pubkey if not set
event.pubkey = this.userPubkey;
// Calculate event ID
const serialized = JSON.stringify([
0,
event.pubkey,
event.created_at,
event.kind,
event.tags,
event.content
]);
const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(serialized));
event.id = bytesToHex(new Uint8Array(hash));
// Sign the event
const sig = secp256k1.sign(hexToBytes(event.id), this.userPrivkey);
event.sig = sig.toCompactHex();
console.log('[BunkerService] Signed event:', event.id.substring(0, 8), 'kind:', event.kind);
return JSON.stringify(event);
}
/**
* Handle nip04_encrypt request.
*/
async handleNip04Encrypt(request, senderPubkey) {
// Check if client is connected
if (!this.connectedClients.has(senderPubkey)) {
throw new Error('Not connected');
}
// Update last activity
this.connectedClients.get(senderPubkey).lastActivity = Date.now();
const [pubkey, plaintext] = request.params;
const privkeyHex = bytesToHex(this.userPrivkey);
const ciphertext = await nip04.encrypt(privkeyHex, pubkey, plaintext);
return ciphertext;
}
/**
* Handle nip04_decrypt request.
*/
async handleNip04Decrypt(request, senderPubkey) {
// Check if client is connected
if (!this.connectedClients.has(senderPubkey)) {
throw new Error('Not connected');
}
// Update last activity
this.connectedClients.get(senderPubkey).lastActivity = Date.now();
const [pubkey, ciphertext] = request.params;
const privkeyHex = bytesToHex(this.userPrivkey);
const plaintext = await nip04.decrypt(privkeyHex, pubkey, ciphertext);
return plaintext;
}
/**
* Send NIP-46 response to client.
*/
async sendResponse(requestId, result, error, recipientPubkey) {
if (!this.ws || !this.connected) {
console.error('[BunkerService] Cannot send response: not connected');
return;
}
const response = {
id: requestId,
result: result !== null ? result : undefined,
error: error !== null ? error : undefined
};
// Encrypt response with NIP-04
const privkeyHex = bytesToHex(this.userPrivkey);
const encrypted = await nip04.encrypt(privkeyHex, recipientPubkey, JSON.stringify(response));
// Create response event
const event = {
kind: 24133,
pubkey: this.userPubkey,
created_at: Math.floor(Date.now() / 1000),
content: encrypted,
tags: [['p', recipientPubkey]]
};
// Calculate event ID
const serialized = JSON.stringify([
0,
event.pubkey,
event.created_at,
event.kind,
event.tags,
event.content
]);
const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(serialized));
event.id = bytesToHex(new Uint8Array(hash));
// Sign the event
const sig = secp256k1.sign(hexToBytes(event.id), this.userPrivkey);
event.sig = sig.toCompactHex();
// Send to relay
this.ws.send(JSON.stringify(['EVENT', event]));
console.log('[BunkerService] Sent response for:', requestId);
}
/**
* Check if the service is connected.
*/
isConnected() {
return this.connected;
}
/**
* Get list of connected clients.
*/
getConnectedClients() {
return Array.from(this.connectedClients.entries()).map(([pubkey, info]) => ({
pubkey,
...info
}));
}
/**
* Get request log.
*/
getRequestLog() {
return [...this.requestLog];
}
}

423
app/web/src/bunker-worker.js

@ -1,423 +0,0 @@ @@ -1,423 +0,0 @@
/**
* BunkerWorker - Web Worker for persistent NIP-46 bunker service
*
* Runs in a separate thread to maintain WebSocket connection
* regardless of UI component lifecycle.
*/
import { nip04 } from 'nostr-tools';
import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
import { secp256k1 } from '@noble/curves/secp256k1';
// State
let ws = null;
let connected = false;
let userPubkey = null;
let userPrivkey = null;
let relayUrl = null;
let catTokenEncoded = null;
let subscriptionId = null;
let heartbeatInterval = null;
let allowedSecrets = new Set();
let connectedClients = new Map();
// NIP-46 methods
const NIP46_METHOD = {
CONNECT: 'connect',
GET_PUBLIC_KEY: 'get_public_key',
SIGN_EVENT: 'sign_event',
NIP04_ENCRYPT: 'nip04_encrypt',
NIP04_DECRYPT: 'nip04_decrypt',
PING: 'ping'
};
function generateRandomHex(bytes = 16) {
const arr = new Uint8Array(bytes);
crypto.getRandomValues(arr);
return bytesToHex(arr);
}
function postStatus(status, data = {}) {
self.postMessage({ type: 'status', status, ...data });
}
function postError(error) {
self.postMessage({ type: 'error', error });
}
function postClientsUpdate() {
const clients = Array.from(connectedClients.entries()).map(([pubkey, info]) => ({
pubkey,
...info
}));
self.postMessage({ type: 'clients', clients });
}
async function connect() {
if (connected || !relayUrl || !userPubkey || !userPrivkey) {
postError('Missing configuration or already connected');
return;
}
return new Promise((resolve, reject) => {
let wsUrl = relayUrl;
if (wsUrl.startsWith('http://')) {
wsUrl = 'ws://' + wsUrl.slice(7);
} else if (wsUrl.startsWith('https://')) {
wsUrl = 'wss://' + wsUrl.slice(8);
} else if (!wsUrl.startsWith('ws://') && !wsUrl.startsWith('wss://')) {
wsUrl = 'wss://' + wsUrl;
}
// Add CAT token if available
if (catTokenEncoded) {
const url = new URL(wsUrl);
url.searchParams.set('token', catTokenEncoded);
wsUrl = url.toString();
}
console.log('[BunkerWorker] Connecting to:', wsUrl.split('?')[0]);
ws = new WebSocket(wsUrl);
const timeout = setTimeout(() => {
ws.close();
postError('Connection timeout');
reject(new Error('Connection timeout'));
}, 10000);
ws.onopen = () => {
clearTimeout(timeout);
connected = true;
console.log('[BunkerWorker] Connected to relay');
// Subscribe to NIP-46 events
subscriptionId = generateRandomHex(8);
const sub = JSON.stringify([
'REQ',
subscriptionId,
{
kinds: [24133],
'#p': [userPubkey],
since: Math.floor(Date.now() / 1000) - 60
}
]);
ws.send(sub);
startHeartbeat();
postStatus('connected');
resolve();
};
ws.onerror = (error) => {
clearTimeout(timeout);
console.error('[BunkerWorker] WebSocket error:', error);
postError('WebSocket error');
reject(new Error('WebSocket error'));
};
ws.onclose = () => {
connected = false;
ws = null;
stopHeartbeat();
console.log('[BunkerWorker] Disconnected from relay');
postStatus('disconnected');
};
ws.onmessage = (event) => {
handleMessage(event.data);
};
});
}
function disconnect() {
stopHeartbeat();
if (ws) {
if (subscriptionId) {
ws.send(JSON.stringify(['CLOSE', subscriptionId]));
}
ws.close();
ws = null;
}
connected = false;
connectedClients.clear();
postStatus('disconnected');
postClientsUpdate();
}
function startHeartbeat(intervalMs = 30000) {
stopHeartbeat();
heartbeatInterval = setInterval(() => {
if (ws && ws.readyState === WebSocket.OPEN) {
const sub = JSON.stringify([
'REQ',
subscriptionId,
{
kinds: [24133],
'#p': [userPubkey],
since: Math.floor(Date.now() / 1000) - 60
}
]);
ws.send(sub);
}
}, intervalMs);
}
function stopHeartbeat() {
if (heartbeatInterval) {
clearInterval(heartbeatInterval);
heartbeatInterval = null;
}
}
async function handleMessage(data) {
try {
const msg = JSON.parse(data);
if (!Array.isArray(msg)) return;
const [type, ...rest] = msg;
if (type === 'EVENT') {
const [, event] = rest;
if (event.kind === 24133) {
await handleNIP46Request(event);
}
} else if (type === 'OK') {
console.log('[BunkerWorker] Event published:', rest[0]?.substring(0, 8));
} else if (type === 'NOTICE') {
console.warn('[BunkerWorker] Relay notice:', rest[0]);
}
} catch (err) {
console.error('[BunkerWorker] Failed to parse message:', err);
}
}
async function handleNIP46Request(event) {
try {
const privkeyHex = bytesToHex(userPrivkey);
const decrypted = await nip04.decrypt(privkeyHex, event.pubkey, event.content);
const request = JSON.parse(decrypted);
console.log('[BunkerWorker] Received request:', request.method, 'from:', event.pubkey.substring(0, 8));
// Log to main thread
self.postMessage({
type: 'request',
method: request.method,
from: event.pubkey,
timestamp: Date.now()
});
let result = null;
let error = null;
try {
switch (request.method) {
case NIP46_METHOD.CONNECT:
result = await handleConnect(request, event.pubkey);
break;
case NIP46_METHOD.GET_PUBLIC_KEY:
result = handleGetPublicKey(event.pubkey);
break;
case NIP46_METHOD.SIGN_EVENT:
result = await handleSignEvent(request, event.pubkey);
break;
case NIP46_METHOD.NIP04_ENCRYPT:
result = await handleNip04Encrypt(request, event.pubkey);
break;
case NIP46_METHOD.NIP04_DECRYPT:
result = await handleNip04Decrypt(request, event.pubkey);
break;
case NIP46_METHOD.PING:
result = 'pong';
break;
default:
error = `Unknown method: ${request.method}`;
}
} catch (err) {
console.error('[BunkerWorker] Error handling request:', err);
error = err.message;
}
await sendResponse(request.id, result, error, event.pubkey);
} catch (err) {
console.error('[BunkerWorker] Failed to handle NIP-46 request:', err);
}
}
async function handleConnect(request, senderPubkey) {
const [clientPubkey, secret] = request.params;
if (allowedSecrets.size > 0) {
if (!secret || !allowedSecrets.has(secret)) {
throw new Error('Invalid or missing connection secret');
}
}
connectedClients.set(senderPubkey, {
clientPubkey: clientPubkey || senderPubkey,
connectedAt: Date.now(),
lastActivity: Date.now()
});
console.log('[BunkerWorker] Client connected:', senderPubkey.substring(0, 8));
postClientsUpdate();
return 'ack';
}
function handleGetPublicKey(senderPubkey) {
if (connectedClients.has(senderPubkey)) {
connectedClients.get(senderPubkey).lastActivity = Date.now();
}
return userPubkey;
}
async function handleSignEvent(request, senderPubkey) {
if (!connectedClients.has(senderPubkey)) {
throw new Error('Not connected');
}
connectedClients.get(senderPubkey).lastActivity = Date.now();
const [eventJson] = request.params;
const event = JSON.parse(eventJson);
if (event.pubkey && event.pubkey !== userPubkey) {
throw new Error('Event pubkey does not match signer pubkey');
}
event.pubkey = userPubkey;
const serialized = JSON.stringify([
0,
event.pubkey,
event.created_at,
event.kind,
event.tags,
event.content
]);
const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(serialized));
event.id = bytesToHex(new Uint8Array(hash));
const sig = secp256k1.sign(hexToBytes(event.id), userPrivkey);
event.sig = sig.toCompactHex();
console.log('[BunkerWorker] Signed event:', event.id.substring(0, 8), 'kind:', event.kind);
return JSON.stringify(event);
}
async function handleNip04Encrypt(request, senderPubkey) {
if (!connectedClients.has(senderPubkey)) {
throw new Error('Not connected');
}
connectedClients.get(senderPubkey).lastActivity = Date.now();
const [pubkey, plaintext] = request.params;
const privkeyHex = bytesToHex(userPrivkey);
return await nip04.encrypt(privkeyHex, pubkey, plaintext);
}
async function handleNip04Decrypt(request, senderPubkey) {
if (!connectedClients.has(senderPubkey)) {
throw new Error('Not connected');
}
connectedClients.get(senderPubkey).lastActivity = Date.now();
const [pubkey, ciphertext] = request.params;
const privkeyHex = bytesToHex(userPrivkey);
return await nip04.decrypt(privkeyHex, pubkey, ciphertext);
}
async function sendResponse(requestId, result, error, recipientPubkey) {
if (!ws || !connected) {
console.error('[BunkerWorker] Cannot send response: not connected');
return;
}
const response = {
id: requestId,
result: result !== null ? result : undefined,
error: error !== null ? error : undefined
};
const privkeyHex = bytesToHex(userPrivkey);
const encrypted = await nip04.encrypt(privkeyHex, recipientPubkey, JSON.stringify(response));
const event = {
kind: 24133,
pubkey: userPubkey,
created_at: Math.floor(Date.now() / 1000),
content: encrypted,
tags: [['p', recipientPubkey]]
};
const serialized = JSON.stringify([
0,
event.pubkey,
event.created_at,
event.kind,
event.tags,
event.content
]);
const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(serialized));
event.id = bytesToHex(new Uint8Array(hash));
const sig = secp256k1.sign(hexToBytes(event.id), userPrivkey);
event.sig = sig.toCompactHex();
ws.send(JSON.stringify(['EVENT', event]));
console.log('[BunkerWorker] Sent response for:', requestId);
}
// Message handler from main thread
self.onmessage = async (event) => {
const { type, ...data } = event.data;
switch (type) {
case 'configure':
userPubkey = data.userPubkey;
userPrivkey = data.userPrivkey ? hexToBytes(data.userPrivkey) : null;
relayUrl = data.relayUrl;
catTokenEncoded = data.catTokenEncoded;
if (data.secrets) {
allowedSecrets = new Set(data.secrets);
}
console.log('[BunkerWorker] Configured for pubkey:', userPubkey?.substring(0, 8));
break;
case 'connect':
try {
await connect();
} catch (err) {
postError(err.message);
}
break;
case 'disconnect':
disconnect();
break;
case 'addSecret':
allowedSecrets.add(data.secret);
break;
case 'removeSecret':
allowedSecrets.delete(data.secret);
break;
case 'getStatus':
postStatus(connected ? 'connected' : 'disconnected');
postClientsUpdate();
break;
default:
console.warn('[BunkerWorker] Unknown message type:', type);
}
};
console.log('[BunkerWorker] Worker initialized');

5
app/web/src/cashu-client.js

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
/**
* Cashu Token Client
*
* Manages Cashu access tokens for bunker authentication.
* Manages Cashu access tokens for relay authentication.
* Handles token issuance using blind signature protocol.
*
* Token flow:
@ -20,7 +20,6 @@ import { bytesToHex, hexToBytes } from '@noble/hashes/utils'; @@ -20,7 +20,6 @@ import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
// Token scopes matching ORLY's token.Scope
export const TokenScope = {
RELAY: 'relay',
NIP46: 'nip46',
BLOSSOM: 'blossom',
API: 'api'
};
@ -168,7 +167,7 @@ export function decodeToken(encoded) { @@ -168,7 +167,7 @@ export function decodeToken(encoded) {
/**
* Request a new token from the mint.
* @param {string} mintUrl - The mint URL (e.g., https://relay.example.com)
* @param {string} scope - Token scope (relay, nip46, blossom, api)
* @param {string} scope - Token scope (relay, blossom, api)
* @param {Uint8Array} userPubkey - User's public key (32 bytes)
* @param {Function} signHttpAuth - Function to create NIP-98 auth header
* @param {number[]} [kinds] - Permitted event kinds

112
app/web/src/stores.js

@ -43,118 +43,6 @@ export const searchQuery = writable(""); @@ -43,118 +43,6 @@ export const searchQuery = writable("");
export const searchTabs = writable([]);
export const searchResults = writable(new Map());
// ==================== Bunker Worker State ====================
// Persists across component mounts/unmounts using Web Worker
export const bunkerWorker = writable(null);
export const bunkerServiceActive = writable(false);
export const bunkerServiceCatToken = writable(null);
export const bunkerClientTokens = writable([]); // [{id, name, token, encoded, createdAt, isExpanded}]
export const bunkerSelectedTokenId = writable(null);
export const bunkerConnectedClients = writable([]);
// Internal worker reference (not reactive)
let _bunkerWorker = null;
/**
* Initialize the bunker worker
*/
export function initBunkerWorker() {
if (_bunkerWorker) {
return _bunkerWorker;
}
_bunkerWorker = new Worker('/bunker-worker.js');
_bunkerWorker.onmessage = (event) => {
const { type, ...data } = event.data;
switch (type) {
case 'status':
bunkerServiceActive.set(data.status === 'connected');
break;
case 'clients':
bunkerConnectedClients.set(data.clients || []);
break;
case 'error':
console.error('[BunkerStore] Worker error:', data.error);
break;
case 'request':
console.log('[BunkerStore] NIP-46 request:', data.method, 'from:', data.from?.substring(0, 8));
break;
}
};
_bunkerWorker.onerror = (error) => {
console.error('[BunkerStore] Worker error:', error);
bunkerServiceActive.set(false);
};
bunkerWorker.set(_bunkerWorker);
return _bunkerWorker;
}
/**
* Get or create the bunker worker
*/
export function getBunkerWorker() {
return _bunkerWorker || initBunkerWorker();
}
/**
* Configure the bunker worker
*/
export function configureBunkerWorker(config) {
const worker = getBunkerWorker();
worker.postMessage({ type: 'configure', ...config });
}
/**
* Start the bunker worker connection
*/
export function connectBunkerWorker() {
const worker = getBunkerWorker();
worker.postMessage({ type: 'connect' });
}
/**
* Stop the bunker worker connection
*/
export function disconnectBunkerWorker() {
if (_bunkerWorker) {
_bunkerWorker.postMessage({ type: 'disconnect' });
}
}
/**
* Add a secret to the worker
*/
export function addBunkerSecret(secret) {
const worker = getBunkerWorker();
worker.postMessage({ type: 'addSecret', secret });
}
/**
* Request status from worker
*/
export function requestBunkerStatus() {
if (_bunkerWorker) {
_bunkerWorker.postMessage({ type: 'getStatus' });
}
}
/**
* Reset bunker state (call on logout or stop)
*/
export function resetBunkerState() {
disconnectBunkerWorker();
bunkerServiceActive.set(false);
bunkerServiceCatToken.set(null);
bunkerClientTokens.set([]);
bunkerSelectedTokenId.set(null);
bunkerConnectedClients.set([]);
}
// ==================== Helper Functions ====================
/**

2
pkg/version/version

@ -1 +1 @@ @@ -1 +1 @@
v0.44.6
v0.44.7

Loading…
Cancel
Save