Move bunker service to Web Worker for persistence (v0.44.6)
Some checks failed
Go / build-and-release (push) Has been cancelled
Some checks failed
Go / build-and-release (push) Has been cancelled
- Add bunker-worker.js Web Worker for NIP-46 signing - Update rollup to build worker as separate bundle - Move bunker state to stores.js for persistence across tab switches - Worker maintains WebSocket connection independently of UI lifecycle Files modified: - app/web/src/bunker-worker.js: New Web Worker implementation - app/web/src/stores.js: Added bunker worker state management - app/web/src/BunkerView.svelte: Use worker instead of inline service - app/web/rollup.config.js: Build worker bundle separately 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
40
app/web/dist/bundle.js
vendored
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
2
app/web/dist/bundle.js.map
vendored
File diff suppressed because one or more lines are too long
@@ -34,7 +34,8 @@ function serve() {
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
// Main app bundle
|
||||
const mainConfig = {
|
||||
input: "src/main.js",
|
||||
output: {
|
||||
sourcemap: true,
|
||||
@@ -95,3 +96,23 @@ export default {
|
||||
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];
|
||||
|
||||
@@ -1,20 +1,32 @@
|
||||
<script>
|
||||
import { createEventDispatcher, onMount, onDestroy } from "svelte";
|
||||
import { createEventDispatcher, onMount } from "svelte";
|
||||
import QRCode from "qrcode";
|
||||
import { getBunkerInfo, createNIP98Auth } from "./api.js";
|
||||
import { BunkerService } from "./bunker-service.js";
|
||||
import { requestToken, encodeToken, TokenScope, getMintInfo } from "./cashu-client.js";
|
||||
import { hexToBytes } from "@noble/hashes/utils";
|
||||
import { hexToBytes, bytesToHex } from "@noble/hashes/utils";
|
||||
import {
|
||||
bunkerServiceActive,
|
||||
bunkerServiceCatToken,
|
||||
bunkerClientTokens,
|
||||
bunkerSelectedTokenId,
|
||||
bunkerConnectedClients,
|
||||
configureBunkerWorker,
|
||||
connectBunkerWorker,
|
||||
disconnectBunkerWorker,
|
||||
addBunkerSecret,
|
||||
requestBunkerStatus,
|
||||
resetBunkerState
|
||||
} from "./stores.js";
|
||||
|
||||
export let isLoggedIn = false;
|
||||
export let userPubkey = "";
|
||||
export let userSigner = null;
|
||||
export let userPrivkey = null; // User's private key for signing
|
||||
export let userPrivkey = null; // User's private key for signing (Uint8Array)
|
||||
export let currentEffectiveRole = "";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
// State
|
||||
// Local UI state
|
||||
let bunkerInfo = null;
|
||||
let isLoading = false;
|
||||
let error = "";
|
||||
@@ -22,17 +34,13 @@
|
||||
let signerQrDataUrl = "";
|
||||
let copiedItem = "";
|
||||
let bunkerSecret = "";
|
||||
|
||||
// Bunker service state
|
||||
let bunkerService = null;
|
||||
let isServiceActive = false;
|
||||
let isStartingService = false;
|
||||
let connectedClients = [];
|
||||
let serviceCatToken = null; // Token for ORLY's own relay connection
|
||||
|
||||
// Client tokens list - each device gets its own token
|
||||
let clientTokens = []; // [{id, name, token, encoded, createdAt, isEditing}]
|
||||
let selectedTokenId = null; // Currently selected token for the QR code
|
||||
// Subscribe to global bunker stores
|
||||
$: isServiceActive = $bunkerServiceActive;
|
||||
$: clientTokens = $bunkerClientTokens;
|
||||
$: selectedTokenId = $bunkerSelectedTokenId;
|
||||
$: connectedClients = $bunkerConnectedClients;
|
||||
|
||||
// Two-word name generator
|
||||
const adjectives = ["brave", "calm", "clever", "cosmic", "cozy", "daring", "eager", "fancy", "gentle", "happy", "jolly", "keen", "lively", "merry", "nimble", "peppy", "quick", "rustic", "shiny", "swift", "tender", "vivid", "witty", "zesty"];
|
||||
@@ -67,10 +75,10 @@
|
||||
createdAt: Date.now(),
|
||||
isExpanded: false
|
||||
};
|
||||
clientTokens = [...clientTokens, newToken];
|
||||
bunkerClientTokens.update(tokens => [...tokens, newToken]);
|
||||
// Select the new token if none selected
|
||||
if (!selectedTokenId) {
|
||||
selectedTokenId = id;
|
||||
if (!$bunkerSelectedTokenId) {
|
||||
bunkerSelectedTokenId.set(id);
|
||||
}
|
||||
console.log(`Client token "${newToken.name}" created, expires:`, new Date(token.expiry * 1000).toISOString());
|
||||
return newToken;
|
||||
@@ -150,18 +158,13 @@
|
||||
|
||||
onMount(async () => {
|
||||
await loadBunkerInfo();
|
||||
// Request current status from worker (in case it's already running)
|
||||
requestBunkerStatus();
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
// Stop bunker service on component unmount
|
||||
if (bunkerService) {
|
||||
bunkerService.disconnect();
|
||||
bunkerService = null;
|
||||
isServiceActive = false;
|
||||
}
|
||||
});
|
||||
// Note: No onDestroy cleanup - worker persists across component mounts
|
||||
|
||||
// Start the bunker service
|
||||
// Start the bunker service (via Web Worker)
|
||||
async function startBunkerService() {
|
||||
// Prevent starting if already active or starting
|
||||
if (isServiceActive || isStartingService) {
|
||||
@@ -178,6 +181,8 @@
|
||||
error = "";
|
||||
|
||||
try {
|
||||
let serviceCatTokenEncoded = null;
|
||||
|
||||
// Check if CAT is required and mint tokens
|
||||
if (bunkerInfo.cashu_enabled) {
|
||||
console.log("CAT required, minting tokens...");
|
||||
@@ -189,14 +194,16 @@
|
||||
return `Nostr ${header}`;
|
||||
};
|
||||
|
||||
// 1. Token for ORLY's BunkerService relay connection
|
||||
serviceCatToken = await requestToken(
|
||||
// 1. Token for worker's relay connection
|
||||
const serviceCatToken = await requestToken(
|
||||
mintInfo.mintUrl,
|
||||
TokenScope.NIP46,
|
||||
hexToBytes(userPubkey),
|
||||
signHttpAuth,
|
||||
[24133]
|
||||
);
|
||||
serviceCatTokenEncoded = encodeToken(serviceCatToken);
|
||||
bunkerServiceCatToken.set(serviceCatToken);
|
||||
console.log("Service CAT token acquired, expires:", new Date(serviceCatToken.expiry * 1000).toISOString());
|
||||
|
||||
// 2. Create first client token
|
||||
@@ -204,70 +211,35 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Create and start bunker service
|
||||
bunkerService = new BunkerService(
|
||||
bunkerInfo.relay_url,
|
||||
// Configure the worker with user credentials
|
||||
const privkeyHex = userPrivkey instanceof Uint8Array ? bytesToHex(userPrivkey) : userPrivkey;
|
||||
configureBunkerWorker({
|
||||
userPubkey,
|
||||
userPrivkey
|
||||
);
|
||||
userPrivkey: privkeyHex,
|
||||
relayUrl: bunkerInfo.relay_url,
|
||||
catTokenEncoded: serviceCatTokenEncoded,
|
||||
secrets: bunkerSecret ? [bunkerSecret] : []
|
||||
});
|
||||
|
||||
// Add the current secret
|
||||
if (bunkerSecret) {
|
||||
bunkerService.addAllowedSecret(bunkerSecret);
|
||||
}
|
||||
|
||||
// Set CAT token for service connection
|
||||
if (serviceCatToken) {
|
||||
bunkerService.setCatToken(serviceCatToken);
|
||||
}
|
||||
|
||||
// Set up callbacks
|
||||
bunkerService.onClientConnected = (pubkey) => {
|
||||
connectedClients = bunkerService.getConnectedClients();
|
||||
};
|
||||
|
||||
bunkerService.onStatusChange = (status) => {
|
||||
console.log("[BunkerView] Service status changed:", status);
|
||||
isServiceActive = status === 'connected';
|
||||
// Don't clear tokens on disconnect - they're still valid
|
||||
// Just clear the connected clients list
|
||||
if (status === 'disconnected') {
|
||||
connectedClients = [];
|
||||
}
|
||||
};
|
||||
|
||||
// Connect to relay
|
||||
await bunkerService.connect();
|
||||
isServiceActive = true;
|
||||
// Connect the worker
|
||||
connectBunkerWorker();
|
||||
|
||||
// Regenerate QR codes with CAT token
|
||||
await generateQRCodes();
|
||||
|
||||
console.log("Bunker service started successfully");
|
||||
console.log("Bunker worker started successfully");
|
||||
} catch (err) {
|
||||
console.error("Failed to start bunker service:", err);
|
||||
error = err.message || "Failed to start bunker service";
|
||||
bunkerService = null;
|
||||
isServiceActive = false;
|
||||
serviceCatToken = null;
|
||||
clientTokens = [];
|
||||
selectedTokenId = null;
|
||||
resetBunkerState();
|
||||
} finally {
|
||||
isStartingService = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Stop the bunker service
|
||||
// Stop the bunker service (via Web Worker)
|
||||
function stopBunkerService() {
|
||||
if (bunkerService) {
|
||||
bunkerService.disconnect();
|
||||
bunkerService = null;
|
||||
}
|
||||
isServiceActive = false;
|
||||
connectedClients = [];
|
||||
serviceCatToken = null;
|
||||
clientTokens = [];
|
||||
selectedTokenId = null;
|
||||
resetBunkerState();
|
||||
// Regenerate QR codes without CAT token
|
||||
generateQRCodes();
|
||||
}
|
||||
|
||||
423
app/web/src/bunker-worker.js
Normal file
423
app/web/src/bunker-worker.js
Normal file
@@ -0,0 +1,423 @@
|
||||
/**
|
||||
* 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');
|
||||
@@ -43,6 +43,118 @@ 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 ====================
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user