Add simplified NIP-46 bunker page with click-to-copy QR codes (v0.41.0)
Some checks failed
Go / build-and-release (push) Has been cancelled

- Add BunkerView with two QR codes: client (bunker://) and signer (nostr+connect://)
- Add click-to-copy functionality on QR codes with visual "Copied!" feedback
- Add CAT requirement warning (only shows when ACL mode is active)
- Remove WireGuard dependencies from bunker page
- Add /api/bunker/info public endpoint for relay URL, ACL mode, CAT status
- Add Cashu token verification for WebSocket connections
- Add kind permission checking for Cashu token scopes
- Add cashuToken field to Listener for connection-level token tracking

Files modified:
- app/handle-bunker.go: New bunker info endpoint (without WireGuard)
- app/handle-event.go: Add Cashu token kind permission check
- app/handle-websocket.go: Extract and verify Cashu token on WS upgrade
- app/listener.go: Add cashuToken field
- app/server.go: Register bunker info endpoint
- app/web/src/BunkerView.svelte: Complete rewrite with QR codes
- app/web/src/api.js: Add getBunkerInfo() function
- pkg/version/version: Bump to v0.41.0

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-28 18:36:04 +02:00
parent ea4a54c5e7
commit 1b17acb50c
12 changed files with 516 additions and 526 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,7 @@
<script>
import { createEventDispatcher, onMount } from "svelte";
import QRCode from "qrcode";
import { getWireGuardConfig, regenerateWireGuard, getBunkerURL, fetchWireGuardStatus, getWireGuardAudit } from "./api.js";
import { getBunkerInfo } from "./api.js";
export let isLoggedIn = false;
export let userPubkey = "";
@@ -11,14 +11,13 @@
const dispatch = createEventDispatcher();
// State
let wgConfig = null;
let bunkerInfo = null;
let wgStatus = null;
let auditData = null;
let isLoading = false;
let error = "";
let wgQrDataUrl = "";
let bunkerQrDataUrl = "";
let clientQrDataUrl = "";
let signerQrDataUrl = "";
let copiedItem = "";
let bunkerSecret = "";
$: canAccess = isLoggedIn && userPubkey && (
currentEffectiveRole === "write" ||
@@ -26,116 +25,79 @@
currentEffectiveRole === "owner"
);
let hasLoadedOnce = false;
// Generate bunker URLs when bunkerInfo and userPubkey are available
$: clientBunkerURL = bunkerInfo && userPubkey ?
`bunker://${userPubkey}?relay=${encodeURIComponent(bunkerInfo.relay_url)}${bunkerSecret ? `&secret=${bunkerSecret}` : ''}` : "";
$: signerBunkerURL = bunkerInfo ?
`nostr+connect://${bunkerInfo.relay_url}` : "";
onMount(async () => {
// Always check status first
await checkStatus();
if (canAccess && wgStatus?.available && !hasLoadedOnce) {
hasLoadedOnce = true;
await loadConfig();
}
await loadBunkerInfo();
});
$: if (canAccess && wgStatus?.available && !hasLoadedOnce && !isLoading) {
hasLoadedOnce = true;
loadConfig();
}
async function checkStatus() {
try {
wgStatus = await fetchWireGuardStatus();
} catch (err) {
console.error("Error checking WireGuard status:", err);
wgStatus = { available: false };
}
}
async function loadConfig() {
if (!userSigner || !userPubkey) return;
async function loadBunkerInfo() {
isLoading = true;
error = "";
try {
// Load WireGuard config, bunker URL, and audit data in parallel
const [wgResult, bunkerResult, auditResult] = await Promise.all([
getWireGuardConfig(userSigner, userPubkey),
getBunkerURL(userSigner, userPubkey),
getWireGuardAudit(userSigner, userPubkey).catch(() => null)
]);
bunkerInfo = await getBunkerInfo();
wgConfig = wgResult;
bunkerInfo = bunkerResult;
auditData = auditResult;
// Generate a random secret for secure connection
if (!bunkerSecret) {
bunkerSecret = generateSecret();
}
// Generate QR codes
if (wgConfig?.config_text) {
wgQrDataUrl = await QRCode.toDataURL(wgConfig.config_text, {
width: 256,
margin: 2,
color: { dark: "#000000", light: "#ffffff" }
});
}
if (bunkerInfo?.url) {
bunkerQrDataUrl = await QRCode.toDataURL(bunkerInfo.url, {
width: 256,
margin: 2,
color: { dark: "#000000", light: "#ffffff" }
});
}
await generateQRCodes();
} catch (err) {
console.error("Error loading bunker config:", err);
error = err.message || "Failed to load configuration";
console.error("Error loading bunker info:", err);
error = err.message || "Failed to load bunker information";
} finally {
isLoading = false;
}
}
function formatDate(timestamp) {
if (!timestamp) return "Never";
return new Date(timestamp * 1000).toLocaleString();
function generateSecret() {
const array = new Uint8Array(16);
crypto.getRandomValues(array);
return Array.from(array, b => b.toString(16).padStart(2, '0')).join('');
}
async function handleRegenerate() {
if (!confirm("Regenerate your WireGuard keys? Your current keys will stop working.")) {
return;
async function regenerateSecret() {
bunkerSecret = generateSecret();
await generateQRCodes();
}
async function generateQRCodes() {
if (clientBunkerURL) {
clientQrDataUrl = await QRCode.toDataURL(clientBunkerURL, {
width: 280,
margin: 2,
color: { dark: "#000000", light: "#ffffff" }
});
}
isLoading = true;
error = "";
try {
await regenerateWireGuard(userSigner, userPubkey);
// Reload config after regeneration
hasLoadedOnce = false;
await loadConfig();
} catch (err) {
console.error("Error regenerating keys:", err);
error = err.message || "Failed to regenerate keys";
} finally {
isLoading = false;
if (signerBunkerURL) {
signerQrDataUrl = await QRCode.toDataURL(signerBunkerURL, {
width: 280,
margin: 2,
color: { dark: "#000000", light: "#ffffff" }
});
}
}
// Regenerate QR codes when URLs change
$: if (clientBunkerURL || signerBunkerURL) {
generateQRCodes();
}
function copyToClipboard(text, label) {
navigator.clipboard.writeText(text);
alert(`${label} copied to clipboard!`);
}
function downloadConfig() {
if (!wgConfig?.config_text) return;
const blob = new Blob([wgConfig.config_text], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "wg-orly.conf";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
copiedItem = label;
setTimeout(() => {
copiedItem = "";
}, 2000);
}
function openLoginModal() {
@@ -143,19 +105,19 @@
}
</script>
{#if !wgStatus?.available}
{#if !bunkerInfo?.available}
<div class="bunker-view">
<div class="unavailable-message">
<h3>Remote Signing Not Available</h3>
<p>This relay does not have WireGuard/Bunker enabled, or ACL mode is set to "none".</p>
<p class="hint">Remote signing requires the relay operator to enable WireGuard VPN and use ACL mode "follows" or "managed".</p>
<p>This relay does not have bunker mode enabled, or ACL mode is set to "none".</p>
<p class="hint">Remote signing requires the relay operator to enable ACL mode "follows" or "managed".</p>
</div>
</div>
{:else if canAccess}
<div class="bunker-view">
<div class="header-section">
<h3>Remote Signing (Bunker)</h3>
<button class="refresh-btn" on:click={loadConfig} disabled={isLoading}>
<h3>Remote Signing (NIP-46 Bunker)</h3>
<button class="refresh-btn" on:click={loadBunkerInfo} disabled={isLoading}>
{isLoading ? "Loading..." : "Refresh"}
</button>
</div>
@@ -164,189 +126,118 @@
<div class="error-message">{error}</div>
{/if}
{#if isLoading && !wgConfig}
<div class="loading">Loading configuration...</div>
{:else if wgConfig}
{#if bunkerInfo?.cashu_enabled && bunkerInfo?.acl_mode !== "none"}
<div class="cat-warning">
<strong>CAT Required:</strong> This relay requires Cashu Access Tokens (CAT) for bunker connections.
Your client must support CAT authentication or connections will be rejected.
</div>
{/if}
{#if isLoading && !bunkerInfo}
<div class="loading">Loading bunker information...</div>
{:else if bunkerInfo}
<div class="instructions">
<p><strong>How it works:</strong> Connect to the relay's private VPN, then use Amber to sign events remotely.</p>
<p><strong>How it works:</strong> Both your signing app (Amber) and your client app connect to this relay.
The relay acts as a secure middleman for NIP-46 remote signing.</p>
</div>
<div class="config-sections">
<!-- Step 1: WireGuard -->
<section class="config-section">
<h4>Step 1: Install WireGuard</h4>
<p class="section-desc">Download the WireGuard app for your device:</p>
<div class="qr-sections">
<!-- Client QR Code -->
<section class="qr-section">
<h4>For Client App</h4>
<p class="section-desc">Scan with your Nostr client to request signatures from Amber:</p>
<div class="client-links">
<a href="https://play.google.com/store/apps/details?id=com.wireguard.android" target="_blank" rel="noopener noreferrer" class="client-link">
<span class="client-icon">Android</span>
<span class="client-store">Google Play</span>
</a>
<a href="https://f-droid.org/packages/com.wireguard.android/" target="_blank" rel="noopener noreferrer" class="client-link">
<span class="client-icon">Android</span>
<span class="client-store">F-Droid</span>
</a>
<a href="https://apps.apple.com/app/wireguard/id1441195209" target="_blank" rel="noopener noreferrer" class="client-link">
<span class="client-icon">iOS</span>
<span class="client-store">App Store</span>
</a>
<a href="https://www.wireguard.com/install/" target="_blank" rel="noopener noreferrer" class="client-link">
<span class="client-icon">Desktop</span>
<span class="client-store">Windows/Mac/Linux</span>
</a>
</div>
</section>
<!-- Step 2: WireGuard Config -->
<section class="config-section">
<h4>Step 2: Add VPN Configuration</h4>
<p class="section-desc">Scan this QR code with the WireGuard app:</p>
<div class="qr-container">
{#if wgQrDataUrl}
<img src={wgQrDataUrl} alt="WireGuard Configuration QR Code" class="qr-code" />
<div
class="qr-container clickable"
on:click={() => copyToClipboard(clientBunkerURL, "client")}
on:keypress={(e) => e.key === 'Enter' && copyToClipboard(clientBunkerURL, "client")}
role="button"
tabindex="0"
title="Click to copy bunker URL"
>
{#if clientQrDataUrl}
<img src={clientQrDataUrl} alt="Client Bunker QR Code" class="qr-code" />
<div class="qr-overlay" class:visible={copiedItem === "client"}>
Copied!
</div>
{:else}
<div class="qr-placeholder">Generating QR...</div>
{/if}
</div>
<div class="config-actions">
<button on:click={() => copyToClipboard(wgConfig.config_text, "Config")}>Copy Config</button>
<button on:click={downloadConfig}>Download .conf</button>
<div class="url-display">
<code class="bunker-url">{clientBunkerURL}</code>
</div>
<details class="config-text-details">
<summary>Show raw config</summary>
<pre class="config-text">{wgConfig.config_text}</pre>
</details>
<div class="copy-hint">Click QR code to copy</div>
</section>
<!-- Step 3: Connect VPN -->
<section class="config-section">
<h4>Step 3: Connect to VPN</h4>
<p class="section-desc">After importing the config, toggle the VPN connection ON in the WireGuard app.</p>
<div class="ip-info">
<span class="label">Your VPN IP:</span>
<code>{wgConfig.interface.address}</code>
<!-- Signer QR Code (Amber) -->
<section class="qr-section">
<h4>For Signer (Amber)</h4>
<p class="section-desc">Scan with <a href="https://github.com/greenart7c3/Amber" target="_blank" rel="noopener noreferrer">Amber</a> to connect as a signer:</p>
<div
class="qr-container clickable"
on:click={() => copyToClipboard(signerBunkerURL, "signer")}
on:keypress={(e) => e.key === 'Enter' && copyToClipboard(signerBunkerURL, "signer")}
role="button"
tabindex="0"
title="Click to copy connection URL"
>
{#if signerQrDataUrl}
<img src={signerQrDataUrl} alt="Signer Connection QR Code" class="qr-code" />
<div class="qr-overlay" class:visible={copiedItem === "signer"}>
Copied!
</div>
{:else}
<div class="qr-placeholder">Generating QR...</div>
{/if}
</div>
</section>
<!-- Step 4: Bunker URL -->
{#if bunkerInfo}
<section class="config-section">
<h4>Step 4: Add Bunker to Amber</h4>
<p class="section-desc">With VPN connected, scan this QR code in <a href="https://github.com/greenart7c3/Amber" target="_blank" rel="noopener noreferrer">Amber</a>:</p>
<div class="qr-container">
{#if bunkerQrDataUrl}
<img src={bunkerQrDataUrl} alt="Bunker URL QR Code" class="qr-code" />
{:else}
<div class="qr-placeholder">Generating QR...</div>
{/if}
</div>
<div class="bunker-url-container">
<code class="bunker-url">{bunkerInfo.url}</code>
<button on:click={() => copyToClipboard(bunkerInfo.url, "Bunker URL")}>Copy</button>
</div>
<div class="relay-info">
<span class="label">Relay npub:</span>
<code class="npub">{bunkerInfo.relay_npub}</code>
</div>
</section>
{/if}
<!-- Amber links -->
<section class="config-section">
<h4>Get Amber (NIP-46 Signer)</h4>
<p class="section-desc">Amber is an Android app for secure remote signing:</p>
<div class="client-links">
<a href="https://play.google.com/store/apps/details?id=com.greenart7c3.nostrsigner" target="_blank" rel="noopener noreferrer" class="client-link">
<span class="client-icon">Amber</span>
<span class="client-store">Google Play</span>
</a>
<a href="https://github.com/greenart7c3/Amber/releases" target="_blank" rel="noopener noreferrer" class="client-link">
<span class="client-icon">Amber</span>
<span class="client-store">GitHub APK</span>
</a>
<div class="url-display">
<code class="bunker-url">{signerBunkerURL}</code>
</div>
<div class="copy-hint">Click QR code to copy</div>
</section>
</div>
<!-- Danger zone -->
<div class="danger-zone">
<h4>Danger Zone</h4>
<p>Regenerate your WireGuard keys if you believe they've been compromised.</p>
<button class="danger-btn" on:click={handleRegenerate} disabled={isLoading}>
Regenerate Keys
</button>
</div>
<!-- Audit Log Section -->
{#if auditData && (auditData.revoked_keys?.length > 0 || auditData.access_logs?.length > 0)}
<div class="audit-section">
<h4>Key History & Access Log</h4>
<p class="audit-desc">Monitor activity on your old WireGuard keys. High access counts might indicate you left something connected or someone copied your credentials.</p>
{#if auditData.revoked_keys?.length > 0}
<div class="audit-subsection">
<h5>Revoked Keys</h5>
<div class="audit-table-container">
<table class="audit-table">
<thead>
<tr>
<th>Client IP</th>
<th>Created</th>
<th>Revoked</th>
<th>Access Count</th>
<th>Last Access</th>
</tr>
</thead>
<tbody>
{#each auditData.revoked_keys as key}
<tr class:warning={key.access_count > 0}>
<td><code>{key.client_ip}</code></td>
<td>{formatDate(key.created_at)}</td>
<td>{formatDate(key.revoked_at)}</td>
<td class:highlight={key.access_count > 0}>{key.access_count}</td>
<td>{formatDate(key.last_access_at)}</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
{/if}
{#if auditData.access_logs?.length > 0}
<div class="audit-subsection">
<h5>Recent Access Attempts (Obsolete Addresses)</h5>
<div class="audit-table-container">
<table class="audit-table">
<thead>
<tr>
<th>Client IP</th>
<th>Time</th>
<th>Remote Address</th>
</tr>
</thead>
<tbody>
{#each auditData.access_logs as log}
<tr>
<td><code>{log.client_ip}</code></td>
<td>{formatDate(log.timestamp)}</td>
<td><code>{log.remote_addr}</code></td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
{/if}
<!-- Connection Info -->
<div class="connection-info">
<h4>Connection Details</h4>
<div class="info-row">
<span class="label">Relay:</span>
<code>{bunkerInfo.relay_url}</code>
<button class="copy-btn" on:click={() => copyToClipboard(bunkerInfo.relay_url, "relay")}>
{copiedItem === "relay" ? "Copied!" : "Copy"}
</button>
</div>
{/if}
<div class="info-row">
<span class="label">Your npub:</span>
<code class="npub">{userPubkey}</code>
</div>
<div class="info-row">
<span class="label">Secret:</span>
<code class="secret">{bunkerSecret}</code>
<button class="copy-btn" on:click={regenerateSecret}>Regenerate</button>
</div>
</div>
<!-- Amber links -->
<section class="amber-section">
<h4>Get Amber (NIP-46 Signer)</h4>
<p class="section-desc">Amber is an Android app for secure remote signing:</p>
<div class="client-links">
<a href="https://play.google.com/store/apps/details?id=com.greenart7c3.nostrsigner" target="_blank" rel="noopener noreferrer" class="client-link">
<span class="client-icon">Amber</span>
<span class="client-store">Google Play</span>
</a>
<a href="https://github.com/greenart7c3/Amber/releases" target="_blank" rel="noopener noreferrer" class="client-link">
<span class="client-icon">Amber</span>
<span class="client-store">GitHub APK</span>
</a>
</div>
</section>
{/if}
</div>
{:else if isLoggedIn}
@@ -408,6 +299,16 @@
margin-bottom: 1em;
}
.cat-warning {
background-color: rgba(255, 193, 7, 0.15);
border: 1px solid rgba(255, 193, 7, 0.5);
color: var(--text-color);
padding: 0.75em 1em;
border-radius: 4px;
margin-bottom: 1em;
font-size: 0.95em;
}
.loading {
text-align: center;
padding: 2em;
@@ -427,19 +328,20 @@
color: var(--text-color);
}
.config-sections {
display: flex;
flex-direction: column;
.qr-sections {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5em;
margin-bottom: 1.5em;
}
.config-section {
.qr-section {
background-color: var(--card-bg);
padding: 1.25em;
border-radius: 8px;
}
.config-section h4 {
.qr-section h4 {
margin: 0 0 0.5em 0;
color: var(--text-color);
}
@@ -455,6 +357,157 @@
color: var(--primary);
}
.qr-container {
display: flex;
justify-content: center;
margin: 1em 0;
position: relative;
}
.qr-container.clickable {
cursor: pointer;
transition: transform 0.1s;
}
.qr-container.clickable:hover {
transform: scale(1.02);
}
.qr-container.clickable:active {
transform: scale(0.98);
}
.qr-code {
border-radius: 8px;
background: white;
padding: 8px;
}
.qr-overlay {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(0, 0, 0, 0.85);
color: #4ade80;
padding: 0.75em 1.5em;
border-radius: 8px;
font-weight: 600;
font-size: 1.1em;
opacity: 0;
transition: opacity 0.2s;
pointer-events: none;
}
.qr-overlay.visible {
opacity: 1;
}
.qr-placeholder {
width: 280px;
height: 280px;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--bg-color);
border-radius: 8px;
color: var(--text-color);
opacity: 0.5;
}
.url-display {
text-align: center;
margin-top: 0.5em;
}
.bunker-url {
font-family: monospace;
font-size: 0.75em;
word-break: break-all;
padding: 0.5em;
background-color: var(--bg-color);
border-radius: 4px;
display: inline-block;
max-width: 100%;
color: var(--text-color);
}
.copy-hint {
text-align: center;
font-size: 0.8em;
color: var(--text-color);
opacity: 0.6;
margin-top: 0.5em;
}
.connection-info {
background-color: var(--card-bg);
padding: 1.25em;
border-radius: 8px;
margin-bottom: 1.5em;
}
.connection-info h4 {
margin: 0 0 1em 0;
color: var(--text-color);
}
.info-row {
display: flex;
align-items: center;
gap: 0.5em;
margin-bottom: 0.75em;
flex-wrap: wrap;
}
.info-row:last-child {
margin-bottom: 0;
}
.label {
color: var(--text-color);
opacity: 0.7;
min-width: 80px;
}
code {
font-family: monospace;
padding: 0.25em 0.5em;
background-color: var(--bg-color);
border-radius: 4px;
color: var(--text-color);
word-break: break-all;
}
.npub, .secret {
font-size: 0.85em;
}
.copy-btn {
padding: 0.3em 0.6em;
background-color: var(--primary);
color: var(--text-color);
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.8em;
}
.copy-btn:hover {
background-color: var(--accent-hover-color);
}
.amber-section {
background-color: var(--card-bg);
padding: 1.25em;
border-radius: 8px;
}
.amber-section h4 {
margin: 0 0 0.5em 0;
color: var(--text-color);
}
.client-links {
display: flex;
flex-wrap: wrap;
@@ -490,165 +543,6 @@
opacity: 0.7;
}
.qr-container {
display: flex;
justify-content: center;
margin: 1em 0;
}
.qr-code {
border-radius: 8px;
background: white;
padding: 8px;
}
.qr-placeholder {
width: 256px;
height: 256px;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--bg-color);
border-radius: 8px;
color: var(--text-color);
opacity: 0.5;
}
.config-actions {
display: flex;
justify-content: center;
gap: 0.75em;
margin-top: 1em;
}
.config-actions button {
padding: 0.5em 1em;
background-color: var(--primary);
color: var(--text-color);
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.9em;
}
.config-actions button:hover {
background-color: var(--accent-hover-color);
}
.config-text-details {
margin-top: 1em;
}
.config-text-details summary {
cursor: pointer;
color: var(--text-color);
opacity: 0.8;
font-size: 0.9em;
}
.config-text {
margin-top: 0.5em;
padding: 1em;
background-color: var(--bg-color);
border-radius: 4px;
font-size: 0.85em;
overflow-x: auto;
white-space: pre;
color: var(--text-color);
}
.ip-info, .relay-info {
display: flex;
align-items: center;
gap: 0.5em;
margin-top: 0.5em;
}
.label {
color: var(--text-color);
opacity: 0.7;
}
code {
font-family: monospace;
padding: 0.25em 0.5em;
background-color: var(--bg-color);
border-radius: 4px;
color: var(--text-color);
}
.bunker-url-container {
display: flex;
align-items: center;
gap: 0.5em;
justify-content: center;
flex-wrap: wrap;
}
.bunker-url {
word-break: break-all;
max-width: 400px;
}
.bunker-url-container button {
padding: 0.4em 0.8em;
background-color: var(--primary);
color: var(--text-color);
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.85em;
}
.bunker-url-container button:hover {
background-color: var(--accent-hover-color);
}
.npub {
word-break: break-all;
font-size: 0.85em;
}
.danger-zone {
margin-top: 2em;
padding: 1.25em;
border: 1px solid var(--warning);
border-radius: 8px;
background-color: rgba(255, 100, 100, 0.05);
}
.danger-zone h4 {
margin: 0 0 0.5em 0;
color: var(--warning);
}
.danger-zone p {
margin: 0 0 1em 0;
color: var(--text-color);
opacity: 0.8;
font-size: 0.95em;
}
.danger-btn {
background-color: transparent;
border: 1px solid var(--warning);
color: var(--warning);
padding: 0.5em 1em;
border-radius: 4px;
cursor: pointer;
font-size: 0.9em;
}
.danger-btn:hover:not(:disabled) {
background-color: var(--warning);
color: var(--text-color);
}
.danger-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.unavailable-message, .access-denied {
text-align: center;
padding: 2em;
@@ -703,83 +597,11 @@
background-color: var(--accent-hover-color);
}
/* Audit section styles */
.audit-section {
margin-top: 2em;
padding: 1.25em;
border: 1px solid var(--border-color);
border-radius: 8px;
background-color: var(--card-bg);
}
.audit-section h4 {
margin: 0 0 0.5em 0;
color: var(--text-color);
}
.audit-desc {
margin: 0 0 1em 0;
color: var(--text-color);
opacity: 0.8;
font-size: 0.9em;
}
.audit-subsection {
margin-bottom: 1.5em;
}
.audit-subsection:last-child {
margin-bottom: 0;
}
.audit-subsection h5 {
margin: 0 0 0.5em 0;
color: var(--text-color);
font-size: 0.95em;
}
.audit-table-container {
overflow-x: auto;
}
.audit-table {
width: 100%;
border-collapse: collapse;
font-size: 0.85em;
}
.audit-table th,
.audit-table td {
padding: 0.5em 0.75em;
text-align: left;
border-bottom: 1px solid var(--border-color);
}
.audit-table th {
background-color: var(--bg-color);
color: var(--text-color);
font-weight: 500;
}
.audit-table td {
color: var(--text-color);
}
.audit-table td code {
font-size: 0.9em;
padding: 0.15em 0.3em;
}
.audit-table tr.warning {
background-color: rgba(255, 100, 100, 0.1);
}
.audit-table td.highlight {
color: var(--warning);
font-weight: 600;
}
@media (max-width: 600px) {
.qr-sections {
grid-template-columns: 1fr;
}
.client-links {
flex-direction: column;
}
@@ -789,16 +611,12 @@
}
.bunker-url {
font-size: 0.75em;
font-size: 0.65em;
}
.audit-table {
font-size: 0.75em;
}
.audit-table th,
.audit-table td {
padding: 0.4em 0.5em;
.info-row {
flex-direction: column;
align-items: flex-start;
}
}
</style>

View File

@@ -486,3 +486,17 @@ 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();
}