Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac61e56b61 |
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",
|
input: "src/main.js",
|
||||||
output: {
|
output: {
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
@@ -95,3 +96,23 @@ export default {
|
|||||||
clearScreen: false,
|
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>
|
<script>
|
||||||
import { createEventDispatcher, onMount, onDestroy } from "svelte";
|
import { createEventDispatcher, onMount } from "svelte";
|
||||||
import QRCode from "qrcode";
|
import QRCode from "qrcode";
|
||||||
import { getBunkerInfo, createNIP98Auth } from "./api.js";
|
import { getBunkerInfo, createNIP98Auth } from "./api.js";
|
||||||
import { BunkerService } from "./bunker-service.js";
|
|
||||||
import { requestToken, encodeToken, TokenScope, getMintInfo } from "./cashu-client.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 isLoggedIn = false;
|
||||||
export let userPubkey = "";
|
export let userPubkey = "";
|
||||||
export let userSigner = null;
|
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 = "";
|
export let currentEffectiveRole = "";
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
// State
|
// Local UI state
|
||||||
let bunkerInfo = null;
|
let bunkerInfo = null;
|
||||||
let isLoading = false;
|
let isLoading = false;
|
||||||
let error = "";
|
let error = "";
|
||||||
@@ -22,17 +34,13 @@
|
|||||||
let signerQrDataUrl = "";
|
let signerQrDataUrl = "";
|
||||||
let copiedItem = "";
|
let copiedItem = "";
|
||||||
let bunkerSecret = "";
|
let bunkerSecret = "";
|
||||||
|
|
||||||
// Bunker service state
|
|
||||||
let bunkerService = null;
|
|
||||||
let isServiceActive = false;
|
|
||||||
let isStartingService = 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
|
// Subscribe to global bunker stores
|
||||||
let clientTokens = []; // [{id, name, token, encoded, createdAt, isEditing}]
|
$: isServiceActive = $bunkerServiceActive;
|
||||||
let selectedTokenId = null; // Currently selected token for the QR code
|
$: clientTokens = $bunkerClientTokens;
|
||||||
|
$: selectedTokenId = $bunkerSelectedTokenId;
|
||||||
|
$: connectedClients = $bunkerConnectedClients;
|
||||||
|
|
||||||
// Two-word name generator
|
// 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"];
|
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(),
|
createdAt: Date.now(),
|
||||||
isExpanded: false
|
isExpanded: false
|
||||||
};
|
};
|
||||||
clientTokens = [...clientTokens, newToken];
|
bunkerClientTokens.update(tokens => [...tokens, newToken]);
|
||||||
// Select the new token if none selected
|
// Select the new token if none selected
|
||||||
if (!selectedTokenId) {
|
if (!$bunkerSelectedTokenId) {
|
||||||
selectedTokenId = id;
|
bunkerSelectedTokenId.set(id);
|
||||||
}
|
}
|
||||||
console.log(`Client token "${newToken.name}" created, expires:`, new Date(token.expiry * 1000).toISOString());
|
console.log(`Client token "${newToken.name}" created, expires:`, new Date(token.expiry * 1000).toISOString());
|
||||||
return newToken;
|
return newToken;
|
||||||
@@ -150,18 +158,13 @@
|
|||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await loadBunkerInfo();
|
await loadBunkerInfo();
|
||||||
|
// Request current status from worker (in case it's already running)
|
||||||
|
requestBunkerStatus();
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
// Note: No onDestroy cleanup - worker persists across component mounts
|
||||||
// Stop bunker service on component unmount
|
|
||||||
if (bunkerService) {
|
|
||||||
bunkerService.disconnect();
|
|
||||||
bunkerService = null;
|
|
||||||
isServiceActive = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Start the bunker service
|
// Start the bunker service (via Web Worker)
|
||||||
async function startBunkerService() {
|
async function startBunkerService() {
|
||||||
// Prevent starting if already active or starting
|
// Prevent starting if already active or starting
|
||||||
if (isServiceActive || isStartingService) {
|
if (isServiceActive || isStartingService) {
|
||||||
@@ -178,6 +181,8 @@
|
|||||||
error = "";
|
error = "";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
let serviceCatTokenEncoded = null;
|
||||||
|
|
||||||
// Check if CAT is required and mint tokens
|
// Check if CAT is required and mint tokens
|
||||||
if (bunkerInfo.cashu_enabled) {
|
if (bunkerInfo.cashu_enabled) {
|
||||||
console.log("CAT required, minting tokens...");
|
console.log("CAT required, minting tokens...");
|
||||||
@@ -189,14 +194,16 @@
|
|||||||
return `Nostr ${header}`;
|
return `Nostr ${header}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 1. Token for ORLY's BunkerService relay connection
|
// 1. Token for worker's relay connection
|
||||||
serviceCatToken = await requestToken(
|
const serviceCatToken = await requestToken(
|
||||||
mintInfo.mintUrl,
|
mintInfo.mintUrl,
|
||||||
TokenScope.NIP46,
|
TokenScope.NIP46,
|
||||||
hexToBytes(userPubkey),
|
hexToBytes(userPubkey),
|
||||||
signHttpAuth,
|
signHttpAuth,
|
||||||
[24133]
|
[24133]
|
||||||
);
|
);
|
||||||
|
serviceCatTokenEncoded = encodeToken(serviceCatToken);
|
||||||
|
bunkerServiceCatToken.set(serviceCatToken);
|
||||||
console.log("Service CAT token acquired, expires:", new Date(serviceCatToken.expiry * 1000).toISOString());
|
console.log("Service CAT token acquired, expires:", new Date(serviceCatToken.expiry * 1000).toISOString());
|
||||||
|
|
||||||
// 2. Create first client token
|
// 2. Create first client token
|
||||||
@@ -204,70 +211,35 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create and start bunker service
|
// Configure the worker with user credentials
|
||||||
bunkerService = new BunkerService(
|
const privkeyHex = userPrivkey instanceof Uint8Array ? bytesToHex(userPrivkey) : userPrivkey;
|
||||||
bunkerInfo.relay_url,
|
configureBunkerWorker({
|
||||||
userPubkey,
|
userPubkey,
|
||||||
userPrivkey
|
userPrivkey: privkeyHex,
|
||||||
);
|
relayUrl: bunkerInfo.relay_url,
|
||||||
|
catTokenEncoded: serviceCatTokenEncoded,
|
||||||
|
secrets: bunkerSecret ? [bunkerSecret] : []
|
||||||
|
});
|
||||||
|
|
||||||
// Add the current secret
|
// Connect the worker
|
||||||
if (bunkerSecret) {
|
connectBunkerWorker();
|
||||||
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;
|
|
||||||
|
|
||||||
// Regenerate QR codes with CAT token
|
// Regenerate QR codes with CAT token
|
||||||
await generateQRCodes();
|
await generateQRCodes();
|
||||||
|
|
||||||
console.log("Bunker service started successfully");
|
console.log("Bunker worker started successfully");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to start bunker service:", err);
|
console.error("Failed to start bunker service:", err);
|
||||||
error = err.message || "Failed to start bunker service";
|
error = err.message || "Failed to start bunker service";
|
||||||
bunkerService = null;
|
resetBunkerState();
|
||||||
isServiceActive = false;
|
|
||||||
serviceCatToken = null;
|
|
||||||
clientTokens = [];
|
|
||||||
selectedTokenId = null;
|
|
||||||
} finally {
|
} finally {
|
||||||
isStartingService = false;
|
isStartingService = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop the bunker service
|
// Stop the bunker service (via Web Worker)
|
||||||
function stopBunkerService() {
|
function stopBunkerService() {
|
||||||
if (bunkerService) {
|
resetBunkerState();
|
||||||
bunkerService.disconnect();
|
|
||||||
bunkerService = null;
|
|
||||||
}
|
|
||||||
isServiceActive = false;
|
|
||||||
connectedClients = [];
|
|
||||||
serviceCatToken = null;
|
|
||||||
clientTokens = [];
|
|
||||||
selectedTokenId = null;
|
|
||||||
// Regenerate QR codes without CAT token
|
// Regenerate QR codes without CAT token
|
||||||
generateQRCodes();
|
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 searchTabs = writable([]);
|
||||||
export const searchResults = writable(new Map());
|
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 ====================
|
// ==================== Helper Functions ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
v0.44.5
|
v0.44.6
|
||||||
|
|||||||
Reference in New Issue
Block a user