From 3ef1ce9a3ab43a6385802d3a131346e87f79e12c Mon Sep 17 00:00:00 2001
From: woikos
Date: Fri, 23 Jan 2026 11:01:42 +0100
Subject: [PATCH] Add admin web UI to orly-launcher with NIP-98 authentication
- Add embedded Svelte admin web UI for process monitoring and control
- Implement NIP-98 HTTP authentication middleware for secure API access
- Add binary update/rollback system with versioned symlinks
- Add admin API endpoints: status, config, binaries, update, restart, rollback
- Update CI workflow to build all binaries for AMD64 and ARM64 architectures
- Add launcher-web Makefile target for building admin UI separately
Files modified:
- .gitea/workflows/go.yml: Build all binaries and launcher admin UI
- Makefile: Add launcher-web and orly-launcher-no-web targets
- cmd/orly-launcher/auth.go: NIP-98 authentication middleware
- cmd/orly-launcher/config.go: Admin UI configuration (port, owners)
- cmd/orly-launcher/main.go: Start admin server, updated help text
- cmd/orly-launcher/server.go: Admin HTTP server with API endpoints
- cmd/orly-launcher/supervisor.go: GetProcessStatuses, RestartAll methods
- cmd/orly-launcher/updater.go: Binary version management with symlinks
- cmd/orly-launcher/web.go: Embedded admin UI serving
- cmd/orly-launcher/web/: Svelte admin UI (dashboard, config, update pages)
- pkg/version/version: Bump to v0.55.11
Co-Authored-By: Claude Opus 4.5
---
.gitea/workflows/go.yml | 157 +++---
Makefile | 27 +-
cmd/orly-launcher/auth.go | 82 ++++
cmd/orly-launcher/config.go | 45 ++
cmd/orly-launcher/main.go | 76 ++-
cmd/orly-launcher/server.go | 316 ++++++++++++
cmd/orly-launcher/supervisor.go | 129 +++++
cmd/orly-launcher/updater.go | 287 +++++++++++
cmd/orly-launcher/web.go | 89 ++++
cmd/orly-launcher/web/bun.lock | 272 +++++++++++
cmd/orly-launcher/web/dist/.gitkeep | 0
cmd/orly-launcher/web/dist/bundle.css | 7 +
cmd/orly-launcher/web/dist/bundle.js | 14 +
cmd/orly-launcher/web/dist/index.html | 12 +
cmd/orly-launcher/web/package.json | 24 +
cmd/orly-launcher/web/public/index.html | 12 +
cmd/orly-launcher/web/rollup.config.js | 35 ++
cmd/orly-launcher/web/src/App.svelte | 185 +++++++
cmd/orly-launcher/web/src/LoginModal.svelte | 454 ++++++++++++++++++
cmd/orly-launcher/web/src/api.js | 151 ++++++
.../web/src/components/Header.svelte | 149 ++++++
.../web/src/components/ProcessCard.svelte | 117 +++++
cmd/orly-launcher/web/src/main.js | 7 +
cmd/orly-launcher/web/src/pages/Config.svelte | 300 ++++++++++++
.../web/src/pages/Dashboard.svelte | 202 ++++++++
cmd/orly-launcher/web/src/pages/Update.svelte | 430 +++++++++++++++++
cmd/orly-launcher/web/src/stores.js | 16 +
pkg/version/version | 2 +-
28 files changed, 3515 insertions(+), 82 deletions(-)
create mode 100644 cmd/orly-launcher/auth.go
create mode 100644 cmd/orly-launcher/server.go
create mode 100644 cmd/orly-launcher/updater.go
create mode 100644 cmd/orly-launcher/web.go
create mode 100644 cmd/orly-launcher/web/bun.lock
create mode 100644 cmd/orly-launcher/web/dist/.gitkeep
create mode 100644 cmd/orly-launcher/web/dist/bundle.css
create mode 100644 cmd/orly-launcher/web/dist/bundle.js
create mode 100644 cmd/orly-launcher/web/dist/index.html
create mode 100644 cmd/orly-launcher/web/package.json
create mode 100644 cmd/orly-launcher/web/public/index.html
create mode 100644 cmd/orly-launcher/web/rollup.config.js
create mode 100644 cmd/orly-launcher/web/src/App.svelte
create mode 100644 cmd/orly-launcher/web/src/LoginModal.svelte
create mode 100644 cmd/orly-launcher/web/src/api.js
create mode 100644 cmd/orly-launcher/web/src/components/Header.svelte
create mode 100644 cmd/orly-launcher/web/src/components/ProcessCard.svelte
create mode 100644 cmd/orly-launcher/web/src/main.js
create mode 100644 cmd/orly-launcher/web/src/pages/Config.svelte
create mode 100644 cmd/orly-launcher/web/src/pages/Dashboard.svelte
create mode 100644 cmd/orly-launcher/web/src/pages/Update.svelte
create mode 100644 cmd/orly-launcher/web/src/stores.js
diff --git a/.gitea/workflows/go.yml b/.gitea/workflows/go.yml
index e026876..5279a13 100644
--- a/.gitea/workflows/go.yml
+++ b/.gitea/workflows/go.yml
@@ -1,5 +1,4 @@
-# This workflow will build a golang project for Gitea Actions
-# Using inline commands to avoid external action dependencies
+# This workflow builds and releases ORLY binaries for Gitea
#
# NOTE: All builds use CGO_ENABLED=0 since p8k library uses purego (not CGO)
# The library dynamically loads libsecp256k1 at runtime via purego
@@ -10,9 +9,10 @@
# git tag v1.2.3
# git push origin v1.2.3
# 3. The workflow will automatically:
-# - Build binaries for Linux AMD64
+# - Build all binaries for Linux AMD64 and ARM64
+# - Build the launcher admin web UI
# - Run tests
-# - Create a Gitea release with the binaries
+# - Create a Gitea release with all binaries
# - Generate checksums
name: Go
@@ -31,14 +31,10 @@ jobs:
set -e
echo "Cloning repository..."
echo "GITHUB_REF_NAME=${GITHUB_REF_NAME}"
- echo "GITHUB_SERVER_URL=${GITHUB_SERVER_URL}"
- echo "GITHUB_REPOSITORY=${GITHUB_REPOSITORY}"
- echo "GITHUB_WORKSPACE=${GITHUB_WORKSPACE}"
git clone --depth 1 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git ${GITHUB_WORKSPACE}
cd ${GITHUB_WORKSPACE}
echo "Cloned successfully. Last commit:"
git log -1
- ls -la
- name: Set up Go
run: |
@@ -60,7 +56,7 @@ jobs:
export PATH="$BUN_INSTALL/bin:$PATH"
bun --version
- - name: Build Web UI
+ - name: Build Main Web UI
run: |
set -e
export BUN_INSTALL="$HOME/.bun"
@@ -70,52 +66,92 @@ jobs:
bun install
echo "Building web app..."
bun run build
- echo "Verifying dist directory was created..."
ls -lah dist/
- echo "Web UI build complete"
+ echo "Main web UI build complete"
- - name: Build (Pure Go + purego)
+ - name: Build Launcher Admin Web UI
+ run: |
+ set -e
+ export BUN_INSTALL="$HOME/.bun"
+ export PATH="$BUN_INSTALL/bin:$PATH"
+ cd ${GITHUB_WORKSPACE}/cmd/orly-launcher/web
+ echo "Installing launcher admin dependencies..."
+ bun install
+ echo "Building launcher admin UI..."
+ bun run build
+ ls -lah dist/
+ echo "Launcher admin UI build complete"
+
+ - name: Build All Packages
run: |
set -e
export PATH=/usr/local/go/bin:$PATH
cd ${GITHUB_WORKSPACE}
- echo "Building with CGO_ENABLED=0..."
+ echo "Building all packages..."
CGO_ENABLED=0 go build -v ./...
- - name: Test (Pure Go + purego)
+ - name: Test
run: |
set -e
export PATH=/usr/local/go/bin:$PATH
cd ${GITHUB_WORKSPACE}
echo "Running tests..."
- # libsecp256k1.so is included in the repository
chmod +x libsecp256k1.so
- # Set LD_LIBRARY_PATH so tests can find the library
export LD_LIBRARY_PATH=${GITHUB_WORKSPACE}:${LD_LIBRARY_PATH}
- # Run tests but don't fail the build on test failures (some tests may need specific env)
CGO_ENABLED=0 go test -v $(go list ./... | grep -v '/cmd/benchmark/external/' | xargs -n1 sh -c 'ls $0/*_test.go 1>/dev/null 2>&1 && echo $0' | grep .) || echo "Some tests failed, continuing..."
- - name: Build Release Binaries (Pure Go + purego)
+ - name: Build Release Binaries
run: |
set -e
export PATH=/usr/local/go/bin:$PATH
cd ${GITHUB_WORKSPACE}
- # Extract version from tag (e.g., v1.2.3 -> 1.2.3)
VERSION=${GITHUB_REF_NAME#v}
- echo "Building release binaries for version $VERSION (pure Go + purego)"
+ echo "Building release binaries for version $VERSION"
- # Create directory for binaries
mkdir -p release-binaries
- # Copy libsecp256k1.so from repository to release binaries
+ # List of binaries to build
+ BINARIES=(
+ "orly:."
+ "orly-db-badger:./cmd/orly-db-badger"
+ "orly-db-neo4j:./cmd/orly-db-neo4j"
+ "orly-acl-follows:./cmd/orly-acl-follows"
+ "orly-acl-managed:./cmd/orly-acl-managed"
+ "orly-acl-curation:./cmd/orly-acl-curation"
+ "orly-launcher:./cmd/orly-launcher"
+ "orly-sync-negentropy:./cmd/orly-sync-negentropy"
+ )
+
+ # Build for AMD64
+ echo "Building for Linux AMD64..."
+ for entry in "${BINARIES[@]}"; do
+ name="${entry%%:*}"
+ path="${entry##*:}"
+ echo " Building ${name}..."
+ GOEXPERIMENT=greenteagc,jsonv2 GOOS=linux GOARCH=amd64 CGO_ENABLED=0 \
+ go build -ldflags "-s -w" -o "release-binaries/${name}-${VERSION}-linux-amd64" "${path}"
+ done
+
+ # Build for ARM64
+ echo "Building for Linux ARM64..."
+ for entry in "${BINARIES[@]}"; do
+ name="${entry%%:*}"
+ path="${entry##*:}"
+ echo " Building ${name}..."
+ GOEXPERIMENT=greenteagc,jsonv2 GOOS=linux GOARCH=arm64 CGO_ENABLED=0 \
+ go build -ldflags "-s -w" -o "release-binaries/${name}-${VERSION}-linux-arm64" "${path}"
+ done
+
+ # Copy libsecp256k1.so for AMD64
cp libsecp256k1.so release-binaries/libsecp256k1-linux-amd64.so
chmod +x release-binaries/libsecp256k1-linux-amd64.so
- # Build for Linux AMD64 (pure Go + purego dynamic loading)
- echo "Building Linux AMD64 (pure Go + purego dynamic loading)..."
- GOEXPERIMENT=greenteagc,jsonv2 GOOS=linux GOARCH=amd64 CGO_ENABLED=0 \
- go build -ldflags "-s -w" -o release-binaries/orly-${VERSION}-linux-amd64 .
+ # Note: ARM64 libsecp256k1 must be built separately and added to repo
+ if [ -f libsecp256k1-arm64.so ]; then
+ cp libsecp256k1-arm64.so release-binaries/libsecp256k1-linux-arm64.so
+ chmod +x release-binaries/libsecp256k1-linux-arm64.so
+ fi
# Create checksums
cd release-binaries
@@ -130,75 +166,78 @@ jobs:
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
- set -e # Exit on any error
- export PATH=/usr/local/go/bin:$PATH
+ set -e
cd ${GITHUB_WORKSPACE}
- # Validate GITEA_TOKEN is set
if [ -z "${GITEA_TOKEN}" ]; then
echo "ERROR: GITEA_TOKEN secret is not set!"
- echo "Please configure the GITEA_TOKEN secret in repository settings."
exit 1
fi
VERSION=${GITHUB_REF_NAME}
+ VERSION_NUM=${GITHUB_REF_NAME#v}
REPO_OWNER=$(echo ${GITHUB_REPOSITORY} | cut -d'/' -f1)
REPO_NAME=$(echo ${GITHUB_REPOSITORY} | cut -d'/' -f2)
echo "Creating release for ${REPO_OWNER}/${REPO_NAME} version ${VERSION}"
- # Verify release binaries exist
- if [ ! -f "release-binaries/orly-${VERSION#v}-linux-amd64" ]; then
- echo "ERROR: Release binary not found!"
- ls -la release-binaries/ || echo "release-binaries directory does not exist"
- exit 1
- fi
-
- # Use Gitea API directly (more reliable than tea CLI)
- cd ${GITHUB_WORKSPACE}
-
API_URL="${GITHUB_SERVER_URL}/api/v1"
- echo "Creating release via Gitea API..."
- echo "API URL: ${API_URL}/repos/${REPO_OWNER}/${REPO_NAME}/releases"
-
# Create the release
+ RELEASE_BODY="## ORLY Release ${VERSION}
+
+ ### Binaries Included
+ - **orly** - Main relay binary
+ - **orly-db-badger** - Badger database server
+ - **orly-db-neo4j** - Neo4j database server
+ - **orly-acl-follows** - Follows ACL server
+ - **orly-acl-managed** - Managed ACL server
+ - **orly-acl-curation** - Curation ACL server
+ - **orly-launcher** - Process supervisor with admin UI
+ - **orly-sync-negentropy** - Negentropy sync service
+ - **libsecp256k1** - Required shared library
+
+ ### Architectures
+ - Linux AMD64 (x86_64)
+ - Linux ARM64 (aarch64)
+
+ ### Installation
+ 1. Download the appropriate binaries for your architecture
+ 2. Make them executable: \`chmod +x orly-*\`
+ 3. Place libsecp256k1 in \`/usr/local/lib/\` or set \`LD_LIBRARY_PATH\`
+ 4. Run with: \`./orly-launcher\` (for split mode) or \`./orly\` (standalone)
+ "
+
RELEASE_RESPONSE=$(curl -s -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
- -d "{\"tag_name\": \"${VERSION}\", \"name\": \"Release ${VERSION}\", \"body\": \"Automated release ${VERSION}\"}" \
+ -d "{\"tag_name\": \"${VERSION}\", \"name\": \"Release ${VERSION}\", \"body\": $(echo "$RELEASE_BODY" | jq -Rs .)}" \
"${API_URL}/repos/${REPO_OWNER}/${REPO_NAME}/releases")
echo "Release response: ${RELEASE_RESPONSE}"
- # Extract release ID
RELEASE_ID=$(echo "${RELEASE_RESPONSE}" | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2)
if [ -z "${RELEASE_ID}" ]; then
- echo "ERROR: Failed to create release or extract release ID"
- echo "Full response: ${RELEASE_RESPONSE}"
+ echo "ERROR: Failed to create release"
exit 1
fi
echo "Release created with ID: ${RELEASE_ID}"
- # Upload assets
- for ASSET in release-binaries/orly-${VERSION#v}-linux-amd64 release-binaries/libsecp256k1-linux-amd64.so release-binaries/SHA256SUMS.txt; do
+ # Upload all assets
+ for ASSET in release-binaries/*; do
FILENAME=$(basename "${ASSET}")
echo "Uploading ${FILENAME}..."
- UPLOAD_RESPONSE=$(curl -s -X POST \
+ curl -s -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-F "attachment=@${ASSET}" \
- "${API_URL}/repos/${REPO_OWNER}/${REPO_NAME}/releases/${RELEASE_ID}/assets?name=${FILENAME}")
-
- echo "Upload response for ${FILENAME}: ${UPLOAD_RESPONSE}"
+ "${API_URL}/repos/${REPO_OWNER}/${REPO_NAME}/releases/${RELEASE_ID}/assets?name=${FILENAME}"
done
- echo "Release ${VERSION} created successfully with all assets!"
-
- # Verify release exists
- VERIFY=$(curl -s -H "Authorization: token ${GITEA_TOKEN}" \
- "${API_URL}/repos/${REPO_OWNER}/${REPO_NAME}/releases/tags/${VERSION}")
- echo "Verification: ${VERIFY}" | head -c 500
+ echo "Release ${VERSION} created successfully!"
+ # Verify
+ curl -s -H "Authorization: token ${GITEA_TOKEN}" \
+ "${API_URL}/repos/${REPO_OWNER}/${REPO_NAME}/releases/tags/${VERSION}" | head -c 500
diff --git a/Makefile b/Makefile
index fd780b7..0146cd1 100644
--- a/Makefile
+++ b/Makefile
@@ -5,6 +5,7 @@
.PHONY: orly-sync-negentropy all-sync arm64-sync
.PHONY: quick-deploy quick-deploy-restart deploy-both deploy-both-restart deploy-new list-releases rollback
.PHONY: orly-unified orly-unified-full arm64-unified install-unified
+.PHONY: launcher-web orly-launcher-no-web
# Build flags
CGO_ENABLED ?= 0
@@ -90,7 +91,15 @@ orly-acl-curation:
$(BUILD_FLAGS) go build -o $(ORLY_ACL_CURATION) ./cmd/orly-acl-curation
# Process supervisor/launcher
-orly-launcher:
+orly-launcher: launcher-web
+ $(BUILD_FLAGS) go build -o $(ORLY_LAUNCHER) ./cmd/orly-launcher
+
+# Build launcher admin web UI
+launcher-web:
+ cd cmd/orly-launcher/web && bun install && bun run build
+
+# Build launcher without web UI (for development)
+orly-launcher-no-web:
$(BUILD_FLAGS) go build -o $(ORLY_LAUNCHER) ./cmd/orly-launcher
# === Unified Binary (New Architecture) ===
@@ -253,13 +262,15 @@ help:
@echo " orly-acl - Build monolithic ACL server"
@echo ""
@echo " Core:"
- @echo " orly - Build main relay binary"
- @echo " orly-launcher - Build process supervisor"
- @echo " proto - Generate protobuf code"
- @echo " web - Rebuild embedded web UI"
- @echo " test - Run test suite"
- @echo " clean - Remove build artifacts"
- @echo " help - Show this help"
+ @echo " orly - Build main relay binary"
+ @echo " orly-launcher - Build process supervisor with admin UI"
+ @echo " orly-launcher-no-web - Build launcher without admin UI"
+ @echo " proto - Generate protobuf code"
+ @echo " web - Rebuild main embedded web UI"
+ @echo " launcher-web - Rebuild launcher admin web UI"
+ @echo " test - Run test suite"
+ @echo " clean - Remove build artifacts"
+ @echo " help - Show this help"
@echo ""
@echo " Sync Services:"
@echo " orly-sync-negentropy - Build NIP-77 negentropy sync server"
diff --git a/cmd/orly-launcher/auth.go b/cmd/orly-launcher/auth.go
new file mode 100644
index 0000000..575f98c
--- /dev/null
+++ b/cmd/orly-launcher/auth.go
@@ -0,0 +1,82 @@
+package main
+
+import (
+ "encoding/hex"
+ "net/http"
+ "strings"
+
+ "git.mleku.dev/mleku/nostr/httpauth"
+ "lol.mleku.dev/chk"
+)
+
+// AuthMiddleware provides NIP-98 authentication for the admin API.
+type AuthMiddleware struct {
+ owners map[string]struct{}
+}
+
+// NewAuthMiddleware creates a new auth middleware with the given owner pubkeys.
+func NewAuthMiddleware(owners []string) *AuthMiddleware {
+ ownerMap := make(map[string]struct{}, len(owners))
+ for _, o := range owners {
+ // Normalize to lowercase hex
+ ownerMap[strings.ToLower(o)] = struct{}{}
+ }
+ return &AuthMiddleware{owners: ownerMap}
+}
+
+// RequireAuth wraps a handler to require NIP-98 authentication from an owner.
+func (a *AuthMiddleware) RequireAuth(next http.HandlerFunc) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ // Validate NIP-98 authentication
+ valid, pubkeyBytes, err := httpauth.CheckAuth(r)
+ if chk.E(err) || !valid {
+ errorMsg := "NIP-98 authentication required"
+ if err != nil {
+ errorMsg = err.Error()
+ }
+ http.Error(w, errorMsg, http.StatusUnauthorized)
+ return
+ }
+
+ // Convert pubkey bytes to hex string
+ pubkeyHex := hex.EncodeToString(pubkeyBytes)
+
+ // Check if pubkey is in owners list
+ if !a.IsOwner(pubkeyHex) {
+ http.Error(w, "Not authorized - owner access required", http.StatusForbidden)
+ return
+ }
+
+ // Authentication successful, call the next handler
+ next(w, r)
+ }
+}
+
+// IsOwner checks if the given pubkey is in the owners list.
+func (a *AuthMiddleware) IsOwner(pubkey string) bool {
+ if len(a.owners) == 0 {
+ // No owners configured - deny all access
+ return false
+ }
+ _, ok := a.owners[strings.ToLower(pubkey)]
+ return ok
+}
+
+// AddOwner adds a pubkey to the owners list.
+func (a *AuthMiddleware) AddOwner(pubkey string) {
+ a.owners[strings.ToLower(pubkey)] = struct{}{}
+}
+
+// RemoveOwner removes a pubkey from the owners list.
+func (a *AuthMiddleware) RemoveOwner(pubkey string) {
+ delete(a.owners, strings.ToLower(pubkey))
+}
+
+// Owners returns the list of owner pubkeys.
+func (a *AuthMiddleware) Owners() []string {
+ owners := make([]string, 0, len(a.owners))
+ for o := range a.owners {
+ owners = append(owners, o)
+ }
+ return owners
+}
diff --git a/cmd/orly-launcher/config.go b/cmd/orly-launcher/config.go
index cf65e90..12ebb19 100644
--- a/cmd/orly-launcher/config.go
+++ b/cmd/orly-launcher/config.go
@@ -3,6 +3,8 @@ package main
import (
"os"
"path/filepath"
+ "strconv"
+ "strings"
"time"
"github.com/adrg/xdg"
@@ -82,6 +84,16 @@ type Config struct {
// SyncReadyTimeout is how long to wait for sync services to be ready
SyncReadyTimeout time.Duration
+
+ // Admin UI configuration
+ // AdminEnabled controls whether to run the admin HTTP server
+ AdminEnabled bool
+ // AdminPort is the port for the admin HTTP server
+ AdminPort int
+ // AdminOwners is a list of pubkeys (hex) allowed to access the admin UI
+ AdminOwners []string
+ // BinDir is the directory for versioned binary management
+ BinDir string
}
func loadConfig() (*Config, error) {
@@ -93,6 +105,9 @@ func loadConfig() (*Config, error) {
defaultDBBinary := "orly-db-" + dbBackend
defaultACLBinary := "orly-acl-" + aclMode
+ // Parse admin owners (comma-separated hex pubkeys)
+ adminOwners := parseOwnersList(getEnvOrDefault("ORLY_LAUNCHER_OWNERS", ""))
+
cfg := &Config{
DBBackend: dbBackend,
DBBinary: getEnvOrDefault("ORLY_LAUNCHER_DB_BINARY", defaultDBBinary),
@@ -126,11 +141,41 @@ func loadConfig() (*Config, error) {
NegentropyListen: getEnvOrDefault("ORLY_LAUNCHER_SYNC_NEGENTROPY_LISTEN", "127.0.0.1:50064"),
SyncReadyTimeout: parseDuration("ORLY_LAUNCHER_SYNC_READY_TIMEOUT", 30*time.Second),
+
+ // Admin UI configuration
+ AdminEnabled: getEnvOrDefault("ORLY_LAUNCHER_ADMIN_ENABLED", "true") == "true",
+ AdminPort: parseInt("ORLY_LAUNCHER_ADMIN_PORT", 8080),
+ AdminOwners: adminOwners,
+ BinDir: getEnvOrDefault("ORLY_LAUNCHER_BIN_DIR", filepath.Join(xdg.DataHome, "orly", "bin")),
}
return cfg, nil
}
+func parseOwnersList(s string) []string {
+ if s == "" {
+ return nil
+ }
+ parts := strings.Split(s, ",")
+ var owners []string
+ for _, p := range parts {
+ p = strings.TrimSpace(p)
+ if p != "" {
+ owners = append(owners, p)
+ }
+ }
+ return owners
+}
+
+func parseInt(key string, defaultValue int) int {
+ if v := os.Getenv(key); v != "" {
+ if i, err := strconv.Atoi(v); err == nil {
+ return i
+ }
+ }
+ return defaultValue
+}
+
func getEnvOrDefault(key, defaultValue string) string {
if v := os.Getenv(key); v != "" {
return v
diff --git a/cmd/orly-launcher/main.go b/cmd/orly-launcher/main.go
index 477d142..c7fd2c5 100644
--- a/cmd/orly-launcher/main.go
+++ b/cmd/orly-launcher/main.go
@@ -54,6 +54,30 @@ func main() {
log.I.F("relay binary: %s", cfg.RelayBinary)
log.I.F("database listen: %s", cfg.DBListen)
+ // Start admin server if enabled
+ var adminServer *AdminServer
+ if cfg.AdminEnabled {
+ adminServer = NewAdminServer(cfg, supervisor)
+
+ // Ensure binary directory structure exists
+ if err := adminServer.updater.EnsureDirectories(); chk.E(err) {
+ log.W.F("failed to create binary directories: %v", err)
+ }
+
+ go func() {
+ if err := adminServer.Start(ctx); err != nil {
+ // Don't exit on admin server error, just log it
+ log.W.F("admin server stopped: %v", err)
+ }
+ }()
+ log.I.F("admin UI available at http://localhost:%d/admin", cfg.AdminPort)
+ if len(cfg.AdminOwners) > 0 {
+ log.I.F("admin owners: %v", cfg.AdminOwners)
+ } else {
+ log.W.F("no admin owners configured - admin API access disabled")
+ }
+ }
+
if err := supervisor.Start(); chk.E(err) {
fmt.Fprintf(os.Stderr, "failed to start: %v\n", err)
os.Exit(1)
@@ -73,7 +97,7 @@ func main() {
func printHelp() {
fmt.Printf(`orly-launcher %s
-Process supervisor for split-mode deployment of ORLY relay.
+Process supervisor for split-mode deployment of ORLY relay with admin web UI.
Usage: orly-launcher [command]
@@ -82,27 +106,51 @@ Commands:
version, -v, --version Show version
Environment Variables:
- ORLY_LAUNCHER_DB_BINARY Path to orly-db binary (default: orly-db)
- ORLY_LAUNCHER_RELAY_BINARY Path to orly binary (default: orly)
- ORLY_LAUNCHER_DB_LISTEN Address for database server (default: 127.0.0.1:50051)
- ORLY_LAUNCHER_DB_READY_TIMEOUT Timeout waiting for DB ready (default: 30s)
- ORLY_LAUNCHER_STOP_TIMEOUT Timeout for graceful stop (default: 10s)
- ORLY_DATA_DIR Data directory (passed to orly-db)
- ORLY_DB_LOG_LEVEL Database log level (passed to orly-db)
+ Process Management:
+ ORLY_LAUNCHER_DB_BINARY Path to orly-db binary (default: orly-db-{backend})
+ ORLY_LAUNCHER_RELAY_BINARY Path to orly binary (default: orly)
+ ORLY_LAUNCHER_ACL_BINARY Path to orly-acl binary (default: orly-acl-{mode})
+ ORLY_LAUNCHER_DB_BACKEND Database backend: badger, neo4j (default: badger)
+ ORLY_LAUNCHER_DB_LISTEN Address for database server (default: 127.0.0.1:50051)
+ ORLY_LAUNCHER_ACL_LISTEN Address for ACL server (default: 127.0.0.1:50052)
+ ORLY_LAUNCHER_ACL_ENABLED Enable ACL server (default: false)
+ ORLY_ACL_MODE ACL mode: follows, managed, curation (default: follows)
+ ORLY_LAUNCHER_DB_READY_TIMEOUT Timeout waiting for DB ready (default: 30s)
+ ORLY_LAUNCHER_STOP_TIMEOUT Timeout for graceful stop (default: 30s)
+ ORLY_DATA_DIR Data directory (passed to orly-db)
+ ORLY_LOG_LEVEL Log level for all processes (default: info)
+
+ Admin UI:
+ ORLY_LAUNCHER_ADMIN_ENABLED Enable admin HTTP server (default: true)
+ ORLY_LAUNCHER_ADMIN_PORT Admin server port (default: 8080)
+ ORLY_LAUNCHER_OWNERS Comma-separated hex pubkeys for admin access
+ ORLY_LAUNCHER_BIN_DIR Directory for versioned binaries
The launcher will:
-1. Start the database server (orly-db)
-2. Wait for the database to be ready
-3. Start the relay (orly) with ORLY_DB_TYPE=grpc
-4. Monitor both processes and restart if they crash
-5. On shutdown, stop relay first, then database
+1. Start the admin HTTP server (optional)
+2. Start the database server (orly-db)
+3. Wait for the database to be ready
+4. Start the ACL server if enabled (orly-acl)
+5. Start sync services if enabled
+6. Start the relay (orly) with ORLY_DB_TYPE=grpc
+7. Monitor all processes and restart if they crash
+8. On shutdown, stop in reverse dependency order
+
+Admin UI Features:
+- View process status and versions
+- Update binaries from release URLs
+- Edit configuration
+- Restart/rollback binaries
Example:
# Start with default binaries in PATH
orly-launcher
+ # Start with admin access for a specific pubkey
+ ORLY_LAUNCHER_OWNERS=abc123... orly-launcher
+
# Start with custom binary paths
- ORLY_LAUNCHER_DB_BINARY=/opt/orly/orly-db \
+ ORLY_LAUNCHER_DB_BINARY=/opt/orly/orly-db-badger \
ORLY_LAUNCHER_RELAY_BINARY=/opt/orly/orly \
orly-launcher
`, version.V)
diff --git a/cmd/orly-launcher/server.go b/cmd/orly-launcher/server.go
new file mode 100644
index 0000000..6cf1ae0
--- /dev/null
+++ b/cmd/orly-launcher/server.go
@@ -0,0 +1,316 @@
+package main
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "time"
+
+ "lol.mleku.dev/chk"
+ "lol.mleku.dev/log"
+)
+
+// AdminServer provides HTTP endpoints for managing the launcher.
+type AdminServer struct {
+ cfg *Config
+ supervisor *Supervisor
+ updater *Updater
+ auth *AuthMiddleware
+ server *http.Server
+ startTime time.Time
+}
+
+// NewAdminServer creates a new admin HTTP server.
+func NewAdminServer(cfg *Config, supervisor *Supervisor) *AdminServer {
+ return &AdminServer{
+ cfg: cfg,
+ supervisor: supervisor,
+ updater: NewUpdater(cfg.BinDir),
+ auth: NewAuthMiddleware(cfg.AdminOwners),
+ startTime: time.Now(),
+ }
+}
+
+// Start starts the admin HTTP server.
+func (s *AdminServer) Start(ctx context.Context) error {
+ mux := http.NewServeMux()
+
+ // Public endpoints
+ mux.HandleFunc("/admin", s.serveUI)
+ mux.HandleFunc("/admin/", s.serveUI)
+
+ // Authenticated API endpoints
+ mux.HandleFunc("/api/status", s.auth.RequireAuth(s.handleStatus))
+ mux.HandleFunc("/api/config", s.auth.RequireAuth(s.handleConfig))
+ mux.HandleFunc("/api/binaries", s.auth.RequireAuth(s.handleBinaries))
+ mux.HandleFunc("/api/update", s.auth.RequireAuth(s.handleUpdate))
+ mux.HandleFunc("/api/restart", s.auth.RequireAuth(s.handleRestart))
+ mux.HandleFunc("/api/rollback", s.auth.RequireAuth(s.handleRollback))
+
+ addr := fmt.Sprintf(":%d", s.cfg.AdminPort)
+ s.server = &http.Server{
+ Addr: addr,
+ Handler: mux,
+ }
+
+ log.I.F("starting admin server on %s", addr)
+
+ go func() {
+ <-ctx.Done()
+ shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+ s.server.Shutdown(shutdownCtx)
+ }()
+
+ return s.server.ListenAndServe()
+}
+
+// StatusResponse is the response for GET /api/status
+type StatusResponse struct {
+ Version string `json:"version"`
+ Uptime string `json:"uptime"`
+ Processes []ProcessStatus `json:"processes"`
+}
+
+// ProcessStatus represents the status of a single managed process.
+type ProcessStatus struct {
+ Name string `json:"name"`
+ Binary string `json:"binary"`
+ Version string `json:"version"`
+ Status string `json:"status"`
+ PID int `json:"pid"`
+ Restarts int `json:"restarts"`
+ StartedAt string `json:"started_at,omitempty"`
+}
+
+func (s *AdminServer) handleStatus(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodGet {
+ http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+
+ uptime := time.Since(s.startTime).Round(time.Second).String()
+ processes := s.supervisor.GetProcessStatuses()
+
+ response := StatusResponse{
+ Version: s.updater.CurrentVersion(),
+ Uptime: uptime,
+ Processes: processes,
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(response)
+}
+
+// ConfigResponse is the response for GET /api/config
+type ConfigResponse struct {
+ DBBackend string `json:"db_backend"`
+ DBBinary string `json:"db_binary"`
+ RelayBinary string `json:"relay_binary"`
+ ACLBinary string `json:"acl_binary"`
+ DBListen string `json:"db_listen"`
+ ACLListen string `json:"acl_listen"`
+ ACLEnabled bool `json:"acl_enabled"`
+ ACLMode string `json:"acl_mode"`
+ DataDir string `json:"data_dir"`
+ LogLevel string `json:"log_level"`
+ DistributedSyncEnabled bool `json:"distributed_sync_enabled"`
+ ClusterSyncEnabled bool `json:"cluster_sync_enabled"`
+ RelayGroupEnabled bool `json:"relay_group_enabled"`
+ NegentropyEnabled bool `json:"negentropy_enabled"`
+ AdminOwners []string `json:"admin_owners"`
+ BinDir string `json:"bin_dir"`
+}
+
+func (s *AdminServer) handleConfig(w http.ResponseWriter, r *http.Request) {
+ switch r.Method {
+ case http.MethodGet:
+ s.handleGetConfig(w, r)
+ case http.MethodPost:
+ s.handleSetConfig(w, r)
+ default:
+ http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+ }
+}
+
+func (s *AdminServer) handleGetConfig(w http.ResponseWriter, r *http.Request) {
+ response := ConfigResponse{
+ DBBackend: s.cfg.DBBackend,
+ DBBinary: s.cfg.DBBinary,
+ RelayBinary: s.cfg.RelayBinary,
+ ACLBinary: s.cfg.ACLBinary,
+ DBListen: s.cfg.DBListen,
+ ACLListen: s.cfg.ACLListen,
+ ACLEnabled: s.cfg.ACLEnabled,
+ ACLMode: s.cfg.ACLMode,
+ DataDir: s.cfg.DataDir,
+ LogLevel: s.cfg.LogLevel,
+ DistributedSyncEnabled: s.cfg.DistributedSyncEnabled,
+ ClusterSyncEnabled: s.cfg.ClusterSyncEnabled,
+ RelayGroupEnabled: s.cfg.RelayGroupEnabled,
+ NegentropyEnabled: s.cfg.NegentropyEnabled,
+ AdminOwners: s.auth.Owners(),
+ BinDir: s.cfg.BinDir,
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(response)
+}
+
+func (s *AdminServer) handleSetConfig(w http.ResponseWriter, r *http.Request) {
+ // TODO: Implement config update (requires restart)
+ http.Error(w, "Config update not implemented yet", http.StatusNotImplemented)
+}
+
+// BinariesResponse is the response for GET /api/binaries
+type BinariesResponse struct {
+ CurrentVersion string `json:"current_version"`
+ AvailableVersions []VersionInfo `json:"available_versions"`
+}
+
+func (s *AdminServer) handleBinaries(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodGet {
+ http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+
+ response := BinariesResponse{
+ CurrentVersion: s.updater.CurrentVersion(),
+ AvailableVersions: s.updater.ListVersions(),
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(response)
+}
+
+// UpdateRequest is the request body for POST /api/update
+type UpdateRequest struct {
+ Version string `json:"version"`
+ URLs map[string]string `json:"urls"` // binary name -> download URL
+}
+
+// UpdateResponse is the response for POST /api/update
+type UpdateResponse struct {
+ Success bool `json:"success"`
+ Message string `json:"message"`
+ Version string `json:"version"`
+ DownloadedFiles []string `json:"downloaded_files"`
+}
+
+func (s *AdminServer) handleUpdate(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+
+ var req UpdateRequest
+ if err := json.NewDecoder(r.Body).Decode(&req); chk.E(err) {
+ http.Error(w, "Invalid request body", http.StatusBadRequest)
+ return
+ }
+
+ if req.Version == "" {
+ http.Error(w, "Version is required", http.StatusBadRequest)
+ return
+ }
+
+ if len(req.URLs) == 0 {
+ http.Error(w, "At least one binary URL is required", http.StatusBadRequest)
+ return
+ }
+
+ // Perform the update
+ downloadedFiles, err := s.updater.Update(req.Version, req.URLs)
+ if chk.E(err) {
+ response := UpdateResponse{
+ Success: false,
+ Message: err.Error(),
+ Version: req.Version,
+ }
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusInternalServerError)
+ json.NewEncoder(w).Encode(response)
+ return
+ }
+
+ response := UpdateResponse{
+ Success: true,
+ Message: fmt.Sprintf("Successfully updated to version %s", req.Version),
+ Version: req.Version,
+ DownloadedFiles: downloadedFiles,
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(response)
+}
+
+// RestartResponse is the response for POST /api/restart
+type RestartResponse struct {
+ Success bool `json:"success"`
+ Message string `json:"message"`
+}
+
+func (s *AdminServer) handleRestart(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+
+ // Signal supervisor to restart all processes
+ go func() {
+ if err := s.supervisor.RestartAll(); chk.E(err) {
+ log.E.F("restart failed: %v", err)
+ }
+ }()
+
+ response := RestartResponse{
+ Success: true,
+ Message: "Restart initiated",
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(response)
+}
+
+// RollbackResponse is the response for POST /api/rollback
+type RollbackResponse struct {
+ Success bool `json:"success"`
+ Message string `json:"message"`
+ PreviousVersion string `json:"previous_version"`
+ CurrentVersion string `json:"current_version"`
+}
+
+func (s *AdminServer) handleRollback(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+
+ previousVersion := s.updater.CurrentVersion()
+
+ if err := s.updater.Rollback(); chk.E(err) {
+ response := RollbackResponse{
+ Success: false,
+ Message: err.Error(),
+ }
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusInternalServerError)
+ json.NewEncoder(w).Encode(response)
+ return
+ }
+
+ response := RollbackResponse{
+ Success: true,
+ Message: "Rollback successful - restart required to apply",
+ PreviousVersion: previousVersion,
+ CurrentVersion: s.updater.CurrentVersion(),
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(response)
+}
+
+func (s *AdminServer) serveUI(w http.ResponseWriter, r *http.Request) {
+ s.serveAdminUI(w, r)
+}
diff --git a/cmd/orly-launcher/supervisor.go b/cmd/orly-launcher/supervisor.go
index dee9c40..040eb67 100644
--- a/cmd/orly-launcher/supervisor.go
+++ b/cmd/orly-launcher/supervisor.go
@@ -826,3 +826,132 @@ func (s *Supervisor) startNegentropy() error {
log.I.F("started negentropy service (pid %d)", cmd.Process.Pid)
return nil
}
+
+// GetProcessStatuses returns the status of all managed processes.
+func (s *Supervisor) GetProcessStatuses() []ProcessStatus {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+
+ var statuses []ProcessStatus
+
+ // Database process
+ if s.dbProc != nil {
+ statuses = append(statuses, s.getProcessStatus(s.dbProc, s.cfg.DBBinary))
+ }
+
+ // ACL process
+ if s.cfg.ACLEnabled && s.aclProc != nil {
+ statuses = append(statuses, s.getProcessStatus(s.aclProc, s.cfg.ACLBinary))
+ }
+
+ // Sync services
+ if s.cfg.DistributedSyncEnabled && s.distributedSyncProc != nil {
+ statuses = append(statuses, s.getProcessStatus(s.distributedSyncProc, s.cfg.DistributedSyncBinary))
+ }
+ if s.cfg.ClusterSyncEnabled && s.clusterSyncProc != nil {
+ statuses = append(statuses, s.getProcessStatus(s.clusterSyncProc, s.cfg.ClusterSyncBinary))
+ }
+ if s.cfg.RelayGroupEnabled && s.relayGroupProc != nil {
+ statuses = append(statuses, s.getProcessStatus(s.relayGroupProc, s.cfg.RelayGroupBinary))
+ }
+ if s.cfg.NegentropyEnabled && s.negentropyProc != nil {
+ statuses = append(statuses, s.getProcessStatus(s.negentropyProc, s.cfg.NegentropyBinary))
+ }
+
+ // Relay process
+ if s.relayProc != nil {
+ statuses = append(statuses, s.getProcessStatus(s.relayProc, s.cfg.RelayBinary))
+ }
+
+ return statuses
+}
+
+func (s *Supervisor) getProcessStatus(p *Process, binaryPath string) ProcessStatus {
+ status := "stopped"
+ pid := 0
+
+ p.mu.Lock()
+ defer p.mu.Unlock()
+
+ if p.cmd != nil && p.cmd.Process != nil {
+ // Check if process is still running
+ select {
+ case <-p.exited:
+ status = "stopped"
+ default:
+ status = "running"
+ pid = p.cmd.Process.Pid
+ }
+ }
+
+ return ProcessStatus{
+ Name: p.name,
+ Binary: binaryPath,
+ Version: "", // Will be filled by caller if needed
+ Status: status,
+ PID: pid,
+ Restarts: p.restarts,
+ }
+}
+
+// RestartAll stops all processes and starts them again.
+func (s *Supervisor) RestartAll() error {
+ log.I.F("restarting all processes...")
+
+ // Stop in reverse dependency order
+ s.mu.Lock()
+ if s.relayProc != nil {
+ s.mu.Unlock()
+ s.stopProcess(s.relayProc, 5*time.Second)
+ s.mu.Lock()
+ }
+
+ s.mu.Unlock()
+ s.stopSyncServices()
+ s.mu.Lock()
+
+ if s.cfg.ACLEnabled && s.aclProc != nil {
+ s.mu.Unlock()
+ s.stopProcess(s.aclProc, 5*time.Second)
+ s.mu.Lock()
+ }
+
+ if s.dbProc != nil {
+ s.mu.Unlock()
+ s.stopProcess(s.dbProc, s.cfg.StopTimeout)
+ s.mu.Lock()
+ }
+ s.mu.Unlock()
+
+ // Small delay to ensure ports are released
+ time.Sleep(500 * time.Millisecond)
+
+ // Start again in dependency order
+ if err := s.startDB(); err != nil {
+ return fmt.Errorf("failed to restart database: %w", err)
+ }
+
+ if err := s.waitForDBReady(s.cfg.DBReadyTimeout); err != nil {
+ return fmt.Errorf("database not ready after restart: %w", err)
+ }
+
+ if s.cfg.ACLEnabled {
+ if err := s.startACL(); err != nil {
+ return fmt.Errorf("failed to restart ACL: %w", err)
+ }
+ if err := s.waitForACLReady(s.cfg.ACLReadyTimeout); err != nil {
+ return fmt.Errorf("ACL not ready after restart: %w", err)
+ }
+ }
+
+ if err := s.startSyncServices(); err != nil {
+ return fmt.Errorf("failed to restart sync services: %w", err)
+ }
+
+ if err := s.startRelay(); err != nil {
+ return fmt.Errorf("failed to restart relay: %w", err)
+ }
+
+ log.I.F("all processes restarted successfully")
+ return nil
+}
diff --git a/cmd/orly-launcher/updater.go b/cmd/orly-launcher/updater.go
new file mode 100644
index 0000000..454b091
--- /dev/null
+++ b/cmd/orly-launcher/updater.go
@@ -0,0 +1,287 @@
+package main
+
+import (
+ "fmt"
+ "io"
+ "net/http"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "sort"
+ "strings"
+ "time"
+
+ "lol.mleku.dev/chk"
+ "lol.mleku.dev/log"
+)
+
+// Updater manages versioned binary updates with symlinks.
+// Directory structure:
+//
+// ~/.local/share/orly/bin/
+// versions/
+// v0.55.10/
+// orly
+// orly-db-badger
+// orly-acl-follows
+// orly-launcher
+// v0.55.11/
+// ...
+// current -> versions/v0.55.11 (symlink)
+// orly -> current/orly (symlink)
+// orly-db-badger -> current/orly-db-badger (symlink)
+// ...
+type Updater struct {
+ binDir string // Base directory for binaries
+ versionsDir string // Directory containing version subdirectories
+}
+
+// VersionInfo contains information about an installed version.
+type VersionInfo struct {
+ Version string `json:"version"`
+ InstalledAt time.Time `json:"installed_at"`
+ IsCurrent bool `json:"is_current"`
+ Binaries []string `json:"binaries"`
+}
+
+// NewUpdater creates a new Updater.
+func NewUpdater(binDir string) *Updater {
+ return &Updater{
+ binDir: binDir,
+ versionsDir: filepath.Join(binDir, "versions"),
+ }
+}
+
+// CurrentVersion returns the currently active version.
+func (u *Updater) CurrentVersion() string {
+ currentLink := filepath.Join(u.binDir, "current")
+ target, err := os.Readlink(currentLink)
+ if err != nil {
+ return "unknown"
+ }
+ // Extract version from path like "versions/v0.55.10"
+ return filepath.Base(target)
+}
+
+// ListVersions returns all installed versions.
+func (u *Updater) ListVersions() []VersionInfo {
+ var versions []VersionInfo
+
+ entries, err := os.ReadDir(u.versionsDir)
+ if err != nil {
+ return versions
+ }
+
+ currentVersion := u.CurrentVersion()
+
+ for _, entry := range entries {
+ if !entry.IsDir() {
+ continue
+ }
+
+ versionDir := filepath.Join(u.versionsDir, entry.Name())
+ info, err := entry.Info()
+ if err != nil {
+ continue
+ }
+
+ // List binaries in this version
+ binaries, _ := u.listBinaries(versionDir)
+
+ versions = append(versions, VersionInfo{
+ Version: entry.Name(),
+ InstalledAt: info.ModTime(),
+ IsCurrent: entry.Name() == currentVersion,
+ Binaries: binaries,
+ })
+ }
+
+ // Sort by version descending (newest first)
+ sort.Slice(versions, func(i, j int) bool {
+ return versions[i].Version > versions[j].Version
+ })
+
+ return versions
+}
+
+// listBinaries returns the list of binary files in a directory.
+func (u *Updater) listBinaries(dir string) ([]string, error) {
+ var binaries []string
+ entries, err := os.ReadDir(dir)
+ if err != nil {
+ return nil, err
+ }
+ for _, entry := range entries {
+ if entry.IsDir() {
+ continue
+ }
+ info, err := entry.Info()
+ if err != nil {
+ continue
+ }
+ // Check if executable
+ if info.Mode()&0111 != 0 {
+ binaries = append(binaries, entry.Name())
+ }
+ }
+ return binaries, nil
+}
+
+// Update downloads binaries from URLs and installs them as a new version.
+func (u *Updater) Update(version string, urls map[string]string) ([]string, error) {
+ // Create version directory
+ versionDir := filepath.Join(u.versionsDir, version)
+ if err := os.MkdirAll(versionDir, 0755); err != nil {
+ return nil, fmt.Errorf("failed to create version directory: %w", err)
+ }
+
+ var downloadedFiles []string
+
+ // Download each binary
+ for name, url := range urls {
+ destPath := filepath.Join(versionDir, name)
+ log.I.F("downloading %s from %s", name, url)
+
+ if err := u.downloadFile(destPath, url); chk.E(err) {
+ // Clean up on failure
+ os.RemoveAll(versionDir)
+ return nil, fmt.Errorf("failed to download %s: %w", name, err)
+ }
+
+ // Make executable
+ if err := os.Chmod(destPath, 0755); err != nil {
+ os.RemoveAll(versionDir)
+ return nil, fmt.Errorf("failed to chmod %s: %w", name, err)
+ }
+
+ downloadedFiles = append(downloadedFiles, name)
+ }
+
+ // Update symlinks
+ if err := u.activateVersion(version); chk.E(err) {
+ return downloadedFiles, fmt.Errorf("failed to activate version: %w", err)
+ }
+
+ log.I.F("successfully updated to version %s", version)
+ return downloadedFiles, nil
+}
+
+// downloadFile downloads a file from a URL.
+func (u *Updater) downloadFile(destPath, url string) error {
+ resp, err := http.Get(url)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return fmt.Errorf("HTTP %d: %s", resp.StatusCode, resp.Status)
+ }
+
+ out, err := os.Create(destPath)
+ if err != nil {
+ return err
+ }
+ defer out.Close()
+
+ _, err = io.Copy(out, resp.Body)
+ return err
+}
+
+// activateVersion updates symlinks to point to the specified version.
+func (u *Updater) activateVersion(version string) error {
+ versionDir := filepath.Join(u.versionsDir, version)
+
+ // Verify version directory exists
+ if _, err := os.Stat(versionDir); os.IsNotExist(err) {
+ return fmt.Errorf("version %s not found", version)
+ }
+
+ // Update 'current' symlink
+ currentLink := filepath.Join(u.binDir, "current")
+ tempLink := currentLink + ".tmp"
+
+ // Create new symlink to temp location
+ relPath, _ := filepath.Rel(u.binDir, versionDir)
+ if err := os.Symlink(relPath, tempLink); err != nil {
+ return fmt.Errorf("failed to create temp symlink: %w", err)
+ }
+
+ // Atomic rename
+ if err := os.Rename(tempLink, currentLink); err != nil {
+ os.Remove(tempLink)
+ return fmt.Errorf("failed to update current symlink: %w", err)
+ }
+
+ // Update individual binary symlinks
+ binaries, err := u.listBinaries(versionDir)
+ if err != nil {
+ return fmt.Errorf("failed to list binaries: %w", err)
+ }
+
+ for _, binary := range binaries {
+ binaryLink := filepath.Join(u.binDir, binary)
+ tempBinaryLink := binaryLink + ".tmp"
+ targetPath := filepath.Join("current", binary)
+
+ // Create new symlink
+ if err := os.Symlink(targetPath, tempBinaryLink); err != nil {
+ log.W.F("failed to create symlink for %s: %v", binary, err)
+ continue
+ }
+
+ // Atomic rename
+ if err := os.Rename(tempBinaryLink, binaryLink); err != nil {
+ os.Remove(tempBinaryLink)
+ log.W.F("failed to update symlink for %s: %v", binary, err)
+ }
+ }
+
+ return nil
+}
+
+// Rollback reverts to the previous version.
+func (u *Updater) Rollback() error {
+ versions := u.ListVersions()
+ if len(versions) < 2 {
+ return fmt.Errorf("no previous version available for rollback")
+ }
+
+ // Find current version index
+ currentVersion := u.CurrentVersion()
+ var previousVersion string
+
+ for i, v := range versions {
+ if v.Version == currentVersion && i+1 < len(versions) {
+ previousVersion = versions[i+1].Version
+ break
+ }
+ }
+
+ if previousVersion == "" {
+ return fmt.Errorf("could not determine previous version")
+ }
+
+ log.I.F("rolling back from %s to %s", currentVersion, previousVersion)
+ return u.activateVersion(previousVersion)
+}
+
+// GetBinaryVersion attempts to get the version from a binary using -v flag.
+func (u *Updater) GetBinaryVersion(binaryPath string) string {
+ cmd := exec.Command(binaryPath, "-v")
+ output, err := cmd.Output()
+ if err != nil {
+ // Try --version
+ cmd = exec.Command(binaryPath, "--version")
+ output, err = cmd.Output()
+ if err != nil {
+ return "unknown"
+ }
+ }
+ return strings.TrimSpace(string(output))
+}
+
+// EnsureDirectories creates the required directory structure.
+func (u *Updater) EnsureDirectories() error {
+ return os.MkdirAll(u.versionsDir, 0755)
+}
diff --git a/cmd/orly-launcher/web.go b/cmd/orly-launcher/web.go
new file mode 100644
index 0000000..715116c
--- /dev/null
+++ b/cmd/orly-launcher/web.go
@@ -0,0 +1,89 @@
+package main
+
+import (
+ "embed"
+ "io/fs"
+ "net/http"
+ "path"
+ "strings"
+)
+
+//go:embed all:web/dist all:web/public
+var adminFS embed.FS
+
+// getAdminFS returns the embedded filesystem for the admin UI.
+func getAdminFS() (http.FileSystem, error) {
+ // Try dist first (built assets)
+ distFS, err := fs.Sub(adminFS, "web/dist")
+ if err == nil {
+ // Check if dist has content
+ entries, _ := fs.ReadDir(distFS, ".")
+ if len(entries) > 0 {
+ return http.FS(distFS), nil
+ }
+ }
+
+ // Fall back to public (template)
+ publicFS, err := fs.Sub(adminFS, "web/public")
+ if err != nil {
+ return nil, err
+ }
+ return http.FS(publicFS), nil
+}
+
+// serveAdminUI serves the embedded admin web UI.
+func (s *AdminServer) serveAdminUI(w http.ResponseWriter, r *http.Request) {
+ fsys, err := getAdminFS()
+ if err != nil {
+ http.Error(w, "Admin UI not available", http.StatusInternalServerError)
+ return
+ }
+
+ // Strip /admin prefix from path
+ urlPath := r.URL.Path
+ if strings.HasPrefix(urlPath, "/admin") {
+ urlPath = strings.TrimPrefix(urlPath, "/admin")
+ if urlPath == "" {
+ urlPath = "/"
+ }
+ }
+
+ // Try to serve the file
+ filePath := strings.TrimPrefix(urlPath, "/")
+ if filePath == "" {
+ filePath = "index.html"
+ }
+
+ // Check if file exists
+ f, err := fsys.Open(filePath)
+ if err != nil {
+ // Serve index.html for SPA routing
+ filePath = "index.html"
+ f, err = fsys.Open(filePath)
+ if err != nil {
+ http.Error(w, "Not found", http.StatusNotFound)
+ return
+ }
+ }
+ f.Close()
+
+ // Set content type
+ switch path.Ext(filePath) {
+ case ".html":
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ case ".css":
+ w.Header().Set("Content-Type", "text/css; charset=utf-8")
+ case ".js":
+ w.Header().Set("Content-Type", "application/javascript; charset=utf-8")
+ case ".json":
+ w.Header().Set("Content-Type", "application/json; charset=utf-8")
+ case ".svg":
+ w.Header().Set("Content-Type", "image/svg+xml")
+ case ".png":
+ w.Header().Set("Content-Type", "image/png")
+ }
+
+ // Serve the file
+ r.URL.Path = "/" + filePath
+ http.FileServer(fsys).ServeHTTP(w, r)
+}
diff --git a/cmd/orly-launcher/web/bun.lock b/cmd/orly-launcher/web/bun.lock
new file mode 100644
index 0000000..8d8efe0
--- /dev/null
+++ b/cmd/orly-launcher/web/bun.lock
@@ -0,0 +1,272 @@
+{
+ "lockfileVersion": 1,
+ "configVersion": 1,
+ "workspaces": {
+ "": {
+ "name": "orly-launcher-admin",
+ "dependencies": {
+ "argon2-browser": "^1.18.0",
+ "nostr-tools": "^2.1.4",
+ },
+ "devDependencies": {
+ "@rollup/plugin-commonjs": "^25.0.7",
+ "@rollup/plugin-node-resolve": "^15.2.3",
+ "@rollup/plugin-terser": "^0.4.4",
+ "rollup": "^4.9.0",
+ "rollup-plugin-css-only": "^4.5.2",
+ "rollup-plugin-livereload": "^2.0.5",
+ "rollup-plugin-svelte": "^7.1.6",
+ "svelte": "^4.2.8",
+ },
+ },
+ },
+ "packages": {
+ "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
+
+ "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
+
+ "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
+
+ "@jridgewell/source-map": ["@jridgewell/source-map@0.3.11", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA=="],
+
+ "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
+
+ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
+
+ "@noble/ciphers": ["@noble/ciphers@0.5.3", "", {}, "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w=="],
+
+ "@noble/curves": ["@noble/curves@1.2.0", "", { "dependencies": { "@noble/hashes": "1.3.2" } }, "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw=="],
+
+ "@noble/hashes": ["@noble/hashes@1.3.1", "", {}, "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA=="],
+
+ "@rollup/plugin-commonjs": ["@rollup/plugin-commonjs@25.0.8", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "commondir": "^1.0.1", "estree-walker": "^2.0.2", "glob": "^8.0.3", "is-reference": "1.2.1", "magic-string": "^0.30.3" }, "peerDependencies": { "rollup": "^2.68.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-ZEZWTK5n6Qde0to4vS9Mr5x/0UZoqCxPVR9KRUjU4kA2sO7GEUn1fop0DAwpO6z0Nw/kJON9bDmSxdWxO/TT1A=="],
+
+ "@rollup/plugin-node-resolve": ["@rollup/plugin-node-resolve@15.3.1", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "@types/resolve": "1.20.2", "deepmerge": "^4.2.2", "is-module": "^1.0.0", "resolve": "^1.22.1" }, "peerDependencies": { "rollup": "^2.78.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA=="],
+
+ "@rollup/plugin-terser": ["@rollup/plugin-terser@0.4.4", "", { "dependencies": { "serialize-javascript": "^6.0.1", "smob": "^1.0.0", "terser": "^5.17.4" }, "peerDependencies": { "rollup": "^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A=="],
+
+ "@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="],
+
+ "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.56.0", "", { "os": "android", "cpu": "arm" }, "sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw=="],
+
+ "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.56.0", "", { "os": "android", "cpu": "arm64" }, "sha512-lfbVUbelYqXlYiU/HApNMJzT1E87UPGvzveGg2h0ktUNlOCxKlWuJ9jtfvs1sKHdwU4fzY7Pl8sAl49/XaEk6Q=="],
+
+ "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.56.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-EgxD1ocWfhoD6xSOeEEwyE7tDvwTgZc8Bss7wCWe+uc7wO8G34HHCUH+Q6cHqJubxIAnQzAsyUsClt0yFLu06w=="],
+
+ "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.56.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-1vXe1vcMOssb/hOF8iv52A7feWW2xnu+c8BV4t1F//m9QVLTfNVpEdja5ia762j/UEJe2Z1jAmEqZAK42tVW3g=="],
+
+ "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.56.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-bof7fbIlvqsyv/DtaXSck4VYQ9lPtoWNFCB/JY4snlFuJREXfZnm+Ej6yaCHfQvofJDXLDMTVxWscVSuQvVWUQ=="],
+
+ "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.56.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KNa6lYHloW+7lTEkYGa37fpvPq+NKG/EHKM8+G/g9WDU7ls4sMqbVRV78J6LdNuVaeeK5WB9/9VAFbKxcbXKYg=="],
+
+ "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.56.0", "", { "os": "linux", "cpu": "arm" }, "sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A=="],
+
+ "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.56.0", "", { "os": "linux", "cpu": "arm" }, "sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw=="],
+
+ "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.56.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ=="],
+
+ "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.56.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA=="],
+
+ "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg=="],
+
+ "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA=="],
+
+ "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.56.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw=="],
+
+ "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.56.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg=="],
+
+ "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew=="],
+
+ "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ=="],
+
+ "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.56.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ=="],
+
+ "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.56.0", "", { "os": "linux", "cpu": "x64" }, "sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw=="],
+
+ "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.56.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA=="],
+
+ "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.56.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA=="],
+
+ "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.56.0", "", { "os": "none", "cpu": "arm64" }, "sha512-LhN/Reh+7F3RCgQIRbgw8ZMwUwyqJM+8pXNT6IIJAqm2IdKkzpCh/V9EdgOMBKuebIrzswqy4ATlrDgiOwbRcQ=="],
+
+ "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.56.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-kbFsOObXp3LBULg1d3JIUQMa9Kv4UitDmpS+k0tinPBz3watcUiV2/LUDMMucA6pZO3WGE27P7DsfaN54l9ing=="],
+
+ "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.56.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-vSSgny54D6P4vf2izbtFm/TcWYedw7f8eBrOiGGecyHyQB9q4Kqentjaj8hToe+995nob/Wv48pDqL5a62EWtg=="],
+
+ "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.56.0", "", { "os": "win32", "cpu": "x64" }, "sha512-FeCnkPCTHQJFbiGG49KjV5YGW/8b9rrXAM2Mz2kiIoktq2qsJxRD5giEMEOD2lPdgs72upzefaUvS+nc8E3UzQ=="],
+
+ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.56.0", "", { "os": "win32", "cpu": "x64" }, "sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g=="],
+
+ "@scure/base": ["@scure/base@1.1.1", "", {}, "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA=="],
+
+ "@scure/bip32": ["@scure/bip32@1.3.1", "", { "dependencies": { "@noble/curves": "~1.1.0", "@noble/hashes": "~1.3.1", "@scure/base": "~1.1.0" } }, "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A=="],
+
+ "@scure/bip39": ["@scure/bip39@1.2.1", "", { "dependencies": { "@noble/hashes": "~1.3.0", "@scure/base": "~1.1.0" } }, "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg=="],
+
+ "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
+
+ "@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="],
+
+ "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
+
+ "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
+
+ "argon2-browser": ["argon2-browser@1.18.0", "", {}, "sha512-ImVAGIItnFnvET1exhsQB7apRztcoC5TnlSqernMJDUjbc/DLq3UEYeXFrLPrlaIl8cVfwnXb6wX2KpFf2zxHw=="],
+
+ "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
+
+ "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
+
+ "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
+
+ "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
+
+ "brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
+
+ "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
+
+ "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
+
+ "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
+
+ "code-red": ["code-red@1.0.4", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15", "@types/estree": "^1.0.1", "acorn": "^8.10.0", "estree-walker": "^3.0.3", "periscopic": "^3.1.0" } }, "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw=="],
+
+ "commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
+
+ "commondir": ["commondir@1.0.1", "", {}, "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="],
+
+ "css-tree": ["css-tree@2.3.1", "", { "dependencies": { "mdn-data": "2.0.30", "source-map-js": "^1.0.1" } }, "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw=="],
+
+ "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
+
+ "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
+
+ "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
+
+ "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
+
+ "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
+
+ "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
+
+ "glob": ["glob@8.1.0", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^5.0.1", "once": "^1.3.0" } }, "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ=="],
+
+ "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
+
+ "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
+
+ "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="],
+
+ "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
+
+ "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="],
+
+ "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
+
+ "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
+
+ "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
+
+ "is-module": ["is-module@1.0.0", "", {}, "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g=="],
+
+ "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
+
+ "is-reference": ["is-reference@1.2.1", "", { "dependencies": { "@types/estree": "*" } }, "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ=="],
+
+ "livereload": ["livereload@0.9.3", "", { "dependencies": { "chokidar": "^3.5.0", "livereload-js": "^3.3.1", "opts": ">= 1.2.0", "ws": "^7.4.3" }, "bin": { "livereload": "bin/livereload.js" } }, "sha512-q7Z71n3i4X0R9xthAryBdNGVGAO2R5X+/xXpmKeuPMrteg+W2U8VusTKV3YiJbXZwKsOlFlHe+go6uSNjfxrZw=="],
+
+ "livereload-js": ["livereload-js@3.4.1", "", {}, "sha512-5MP0uUeVCec89ZbNOT/i97Mc+q3SxXmiUGhRFOTmhrGPn//uWVQdCvcLJDy64MSBR5MidFdOR7B9viumoavy6g=="],
+
+ "locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="],
+
+ "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
+
+ "mdn-data": ["mdn-data@2.0.30", "", {}, "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="],
+
+ "minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="],
+
+ "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
+
+ "nostr-tools": ["nostr-tools@2.20.0", "", { "dependencies": { "@noble/ciphers": "^0.5.1", "@noble/curves": "1.2.0", "@noble/hashes": "1.3.1", "@scure/base": "1.1.1", "@scure/bip32": "1.3.1", "@scure/bip39": "1.2.1", "nostr-wasm": "0.1.0" }, "peerDependencies": { "typescript": ">=5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-Kq/2lMyeOdGvpDsYH2an8HP4H0aFCqwKythhTzxfgZTVv4L3NOgrJw2SxH8jkWlH8xPhWxGfN6lFtC+EAa2qYQ=="],
+
+ "nostr-wasm": ["nostr-wasm@0.1.0", "", {}, "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA=="],
+
+ "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
+
+ "opts": ["opts@2.0.2", "", {}, "sha512-k41FwbcLnlgnFh69f4qdUfvDQ+5vaSDnVPFI/y5XuhKRq97EnVVneO9F1ESVCdiVu4fCS2L8usX3mU331hB7pg=="],
+
+ "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
+
+ "periscopic": ["periscopic@3.1.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^3.0.0", "is-reference": "^3.0.0" } }, "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw=="],
+
+ "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
+
+ "randombytes": ["randombytes@2.1.0", "", { "dependencies": { "safe-buffer": "^5.1.0" } }, "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ=="],
+
+ "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
+
+ "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="],
+
+ "resolve.exports": ["resolve.exports@2.0.3", "", {}, "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A=="],
+
+ "rollup": ["rollup@4.56.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.56.0", "@rollup/rollup-android-arm64": "4.56.0", "@rollup/rollup-darwin-arm64": "4.56.0", "@rollup/rollup-darwin-x64": "4.56.0", "@rollup/rollup-freebsd-arm64": "4.56.0", "@rollup/rollup-freebsd-x64": "4.56.0", "@rollup/rollup-linux-arm-gnueabihf": "4.56.0", "@rollup/rollup-linux-arm-musleabihf": "4.56.0", "@rollup/rollup-linux-arm64-gnu": "4.56.0", "@rollup/rollup-linux-arm64-musl": "4.56.0", "@rollup/rollup-linux-loong64-gnu": "4.56.0", "@rollup/rollup-linux-loong64-musl": "4.56.0", "@rollup/rollup-linux-ppc64-gnu": "4.56.0", "@rollup/rollup-linux-ppc64-musl": "4.56.0", "@rollup/rollup-linux-riscv64-gnu": "4.56.0", "@rollup/rollup-linux-riscv64-musl": "4.56.0", "@rollup/rollup-linux-s390x-gnu": "4.56.0", "@rollup/rollup-linux-x64-gnu": "4.56.0", "@rollup/rollup-linux-x64-musl": "4.56.0", "@rollup/rollup-openbsd-x64": "4.56.0", "@rollup/rollup-openharmony-arm64": "4.56.0", "@rollup/rollup-win32-arm64-msvc": "4.56.0", "@rollup/rollup-win32-ia32-msvc": "4.56.0", "@rollup/rollup-win32-x64-gnu": "4.56.0", "@rollup/rollup-win32-x64-msvc": "4.56.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg=="],
+
+ "rollup-plugin-css-only": ["rollup-plugin-css-only@4.5.5", "", { "dependencies": { "@rollup/pluginutils": "5" }, "peerDependencies": { "rollup": "<5" } }, "sha512-O2m2Sj8qsAtjUVqZyGTDXJypaOFFNV4knz8OlS6wJBws6XEICIiLsXmI56SbQEmWDqYU5TgRgWmslGj4THofJQ=="],
+
+ "rollup-plugin-livereload": ["rollup-plugin-livereload@2.0.5", "", { "dependencies": { "livereload": "^0.9.1" } }, "sha512-vqQZ/UQowTW7VoiKEM5ouNW90wE5/GZLfdWuR0ELxyKOJUIaj+uismPZZaICU4DnWPVjnpCDDxEqwU7pcKY/PA=="],
+
+ "rollup-plugin-svelte": ["rollup-plugin-svelte@7.2.3", "", { "dependencies": { "@rollup/pluginutils": "^4.1.0", "resolve.exports": "^2.0.0" }, "peerDependencies": { "rollup": ">=2.0.0", "svelte": ">=3.5.0" } }, "sha512-LlniP+h00DfM+E4eav/Kk8uGjgPUjGIBfrAS/IxQvsuFdqSM0Y2sXf31AdxuIGSW9GsmocDqOfaxR5QNno/Tgw=="],
+
+ "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
+
+ "serialize-javascript": ["serialize-javascript@6.0.2", "", { "dependencies": { "randombytes": "^2.1.0" } }, "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g=="],
+
+ "smob": ["smob@1.5.0", "", {}, "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig=="],
+
+ "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
+
+ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
+
+ "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
+
+ "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
+
+ "svelte": ["svelte@4.2.20", "", { "dependencies": { "@ampproject/remapping": "^2.2.1", "@jridgewell/sourcemap-codec": "^1.4.15", "@jridgewell/trace-mapping": "^0.3.18", "@types/estree": "^1.0.1", "acorn": "^8.9.0", "aria-query": "^5.3.0", "axobject-query": "^4.0.0", "code-red": "^1.0.3", "css-tree": "^2.3.1", "estree-walker": "^3.0.3", "is-reference": "^3.0.1", "locate-character": "^3.0.0", "magic-string": "^0.30.4", "periscopic": "^3.1.0" } }, "sha512-eeEgGc2DtiUil5ANdtd8vPwt9AgaMdnuUFnPft9F5oMvU/FHu5IHFic+p1dR/UOB7XU2mX2yHW+NcTch4DCh5Q=="],
+
+ "terser": ["terser@5.46.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg=="],
+
+ "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
+
+ "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
+
+ "ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="],
+
+ "@noble/curves/@noble/hashes": ["@noble/hashes@1.3.2", "", {}, "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="],
+
+ "@scure/bip32/@noble/curves": ["@noble/curves@1.1.0", "", { "dependencies": { "@noble/hashes": "1.3.1" } }, "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA=="],
+
+ "@scure/bip32/@noble/hashes": ["@noble/hashes@1.3.2", "", {}, "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="],
+
+ "@scure/bip39/@noble/hashes": ["@noble/hashes@1.3.2", "", {}, "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="],
+
+ "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
+
+ "code-red/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
+
+ "periscopic/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
+
+ "periscopic/is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
+
+ "readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
+
+ "rollup-plugin-svelte/@rollup/pluginutils": ["@rollup/pluginutils@4.2.1", "", { "dependencies": { "estree-walker": "^2.0.1", "picomatch": "^2.2.2" } }, "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ=="],
+
+ "svelte/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
+
+ "svelte/is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
+
+ "@scure/bip32/@noble/curves/@noble/hashes": ["@noble/hashes@1.3.1", "", {}, "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA=="],
+
+ "rollup-plugin-svelte/@rollup/pluginutils/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
+ }
+}
diff --git a/cmd/orly-launcher/web/dist/.gitkeep b/cmd/orly-launcher/web/dist/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/cmd/orly-launcher/web/dist/bundle.css b/cmd/orly-launcher/web/dist/bundle.css
new file mode 100644
index 0000000..2098df1
--- /dev/null
+++ b/cmd/orly-launcher/web/dist/bundle.css
@@ -0,0 +1,7 @@
+header.svelte-1bc06ax{background:var(--card-bg);border-bottom:1px solid var(--border-color);padding:0 20px}.header-content.svelte-1bc06ax{max-width:1200px;margin:0 auto;display:flex;align-items:center;justify-content:space-between;height:60px}h1.svelte-1bc06ax{font-size:1.25rem;font-weight:600;color:var(--text-color)}nav.svelte-1bc06ax{display:flex;gap:4px}.nav-btn.svelte-1bc06ax{padding:8px 16px;background:none;border:none;border-radius:4px;color:var(--muted-color);cursor:pointer;font-size:0.9rem}.nav-btn.svelte-1bc06ax:hover{background:var(--border-color);color:var(--text-color)}.nav-btn.active.svelte-1bc06ax{background:var(--primary);color:white}.user-section.svelte-1bc06ax{display:flex;align-items:center;gap:12px}.pubkey.svelte-1bc06ax{font-family:monospace;font-size:0.85rem;color:var(--muted-color)}.logout-btn.svelte-1bc06ax,.login-header-btn.svelte-1bc06ax{padding:6px 14px;font-size:0.85rem;border-radius:4px;cursor:pointer}.logout-btn.svelte-1bc06ax{background:none;border:1px solid var(--border-color);color:var(--text-color)}.logout-btn.svelte-1bc06ax:hover{background:var(--border-color)}.login-header-btn.svelte-1bc06ax{background:var(--primary);border:none;color:white}.login-header-btn.svelte-1bc06ax:hover{background:var(--primary-hover)}
+.modal-overlay.svelte-rhbu32.svelte-rhbu32{position:fixed;top:0;left:0;width:100%;height:100%;background-color:rgba(0, 0, 0, 0.5);display:flex;justify-content:center;align-items:center;z-index:1000}.modal.svelte-rhbu32.svelte-rhbu32{background:var(--card-bg, #fff);border-radius:8px;box-shadow:0 4px 20px rgba(0, 0, 0, 0.3);width:90%;max-width:450px;border:1px solid var(--border-color, #e0e0e0)}.modal-header.svelte-rhbu32.svelte-rhbu32{display:flex;justify-content:space-between;align-items:center;padding:20px;border-bottom:1px solid var(--border-color, #e0e0e0)}.modal-header.svelte-rhbu32 h2.svelte-rhbu32{margin:0;color:var(--text-color, #333);font-size:1.25rem}.close-btn.svelte-rhbu32.svelte-rhbu32{background:none;border:none;font-size:1.5rem;cursor:pointer;color:var(--text-color, #333);padding:0;width:30px;height:30px;display:flex;align-items:center;justify-content:center;border-radius:50%}.close-btn.svelte-rhbu32.svelte-rhbu32:hover{background-color:var(--border-color, #e0e0e0)}.tab-container.svelte-rhbu32.svelte-rhbu32{padding:20px}.tabs.svelte-rhbu32.svelte-rhbu32{display:flex;border-bottom:1px solid var(--border-color, #e0e0e0);margin-bottom:20px}.tab-btn.svelte-rhbu32.svelte-rhbu32{flex:1;padding:12px 16px;background:none;border:none;cursor:pointer;color:var(--text-color, #333);font-size:1rem;border-bottom:2px solid transparent}.tab-btn.svelte-rhbu32.svelte-rhbu32:hover{background-color:var(--border-color, #e0e0e0)}.tab-btn.active.svelte-rhbu32.svelte-rhbu32{border-bottom-color:var(--primary, #00bcd4);color:var(--primary, #00bcd4)}.tab-content.svelte-rhbu32.svelte-rhbu32{min-height:180px}.extension-login.svelte-rhbu32.svelte-rhbu32,.nsec-login.svelte-rhbu32.svelte-rhbu32{display:flex;flex-direction:column;gap:16px}.extension-login.svelte-rhbu32 p.svelte-rhbu32,.nsec-login.svelte-rhbu32 p.svelte-rhbu32{margin:0;color:var(--muted-color, #666);line-height:1.5}.login-btn.svelte-rhbu32.svelte-rhbu32{padding:12px 24px;background:var(--primary, #00bcd4);color:white;border:none;border-radius:6px;cursor:pointer;font-size:1rem}.login-btn.svelte-rhbu32.svelte-rhbu32:hover:not(:disabled){background:var(--primary-hover, #00acc1)}.login-btn.svelte-rhbu32.svelte-rhbu32:disabled{background:#ccc;cursor:not-allowed}.nsec-input.svelte-rhbu32.svelte-rhbu32{padding:12px;border:1px solid var(--border-color, #e0e0e0);border-radius:6px;font-size:1rem;background:var(--card-bg, #fff);color:var(--text-color, #333)}.nsec-input.svelte-rhbu32.svelte-rhbu32:focus{outline:none;border-color:var(--primary, #00bcd4)}.generate-btn.svelte-rhbu32.svelte-rhbu32{padding:10px 20px;background:var(--success, #4caf50);color:white;border:none;border-radius:6px;cursor:pointer;font-size:0.95rem}.generate-btn.svelte-rhbu32.svelte-rhbu32:hover:not(:disabled){opacity:0.9}.generate-btn.svelte-rhbu32.svelte-rhbu32:disabled{background:#ccc;cursor:not-allowed}.generated-info.svelte-rhbu32.svelte-rhbu32{background:var(--bg-color, #f5f5f5);padding:12px;border-radius:6px;border:1px solid var(--border-color, #e0e0e0)}.generated-info.svelte-rhbu32 label.svelte-rhbu32{display:block;font-size:0.85rem;color:var(--muted-color, #666);margin-bottom:6px}.generated-info.svelte-rhbu32 code.svelte-rhbu32{display:block;word-break:break-all;font-size:0.8rem;color:var(--text-color, #333)}.message.svelte-rhbu32.svelte-rhbu32{padding:10px;border-radius:4px;margin-top:16px;text-align:center}.error-message.svelte-rhbu32.svelte-rhbu32{background:#ffebee;color:#c62828;border:1px solid #ffcdd2}.success-message.svelte-rhbu32.svelte-rhbu32{background:#e8f5e9;color:#2e7d32;border:1px solid #c8e6c9}.dark-theme.svelte-rhbu32 .error-message.svelte-rhbu32{background:#4a2c2a;color:#ffcdd2}.dark-theme.svelte-rhbu32 .success-message.svelte-rhbu32{background:#2e4a2e;color:#a5d6a7}
+.process-card.svelte-xh5u5u{background:var(--card-bg);border:1px solid var(--border-color);border-radius:8px;padding:16px}.process-header.svelte-xh5u5u{display:flex;align-items:center;gap:8px;margin-bottom:12px}.status-indicator.svelte-xh5u5u{font-size:1.2rem}.process-name.svelte-xh5u5u{font-weight:600;font-size:1rem;color:var(--text-color)}.process-details.svelte-xh5u5u{display:flex;flex-direction:column;gap:6px}.detail-row.svelte-xh5u5u{display:flex;justify-content:space-between;font-size:0.85rem}.label.svelte-xh5u5u{color:var(--muted-color)}.value.svelte-xh5u5u{color:var(--text-color);font-family:monospace}.value.binary.svelte-xh5u5u{font-size:0.75rem;max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.value.warning.svelte-xh5u5u{color:var(--warning)}
+.dashboard.svelte-17dya06.svelte-17dya06{padding:20px 0}.page-header.svelte-17dya06.svelte-17dya06{display:flex;justify-content:space-between;align-items:center;margin-bottom:24px}.page-header.svelte-17dya06 h2.svelte-17dya06{font-size:1.5rem;color:var(--text-color)}.actions.svelte-17dya06.svelte-17dya06{display:flex;gap:8px}.refresh-btn.svelte-17dya06.svelte-17dya06,.restart-btn.svelte-17dya06.svelte-17dya06{padding:8px 16px;border-radius:4px;cursor:pointer;font-size:0.9rem}.refresh-btn.svelte-17dya06.svelte-17dya06{background:var(--card-bg);border:1px solid var(--border-color);color:var(--text-color)}.refresh-btn.svelte-17dya06.svelte-17dya06:hover:not(:disabled){background:var(--border-color)}.restart-btn.svelte-17dya06.svelte-17dya06{background:var(--warning);border:none;color:white}.restart-btn.svelte-17dya06.svelte-17dya06:hover:not(:disabled){opacity:0.9}.restart-btn.svelte-17dya06.svelte-17dya06:disabled,.refresh-btn.svelte-17dya06.svelte-17dya06:disabled{opacity:0.5;cursor:not-allowed}.error-banner.svelte-17dya06.svelte-17dya06{background:#ffebee;color:#c62828;padding:12px 16px;border-radius:6px;margin-bottom:20px;border:1px solid #ffcdd2}.status-summary.svelte-17dya06.svelte-17dya06{display:grid;grid-template-columns:repeat(auto-fit, minmax(150px, 1fr));gap:16px;margin-bottom:32px}.summary-card.svelte-17dya06.svelte-17dya06{background:var(--card-bg);border:1px solid var(--border-color);border-radius:8px;padding:16px;display:flex;flex-direction:column;gap:4px}.summary-card.svelte-17dya06 .label.svelte-17dya06{font-size:0.85rem;color:var(--muted-color)}.summary-card.svelte-17dya06 .value.svelte-17dya06{font-size:1.25rem;font-weight:600;color:var(--text-color)}h3.svelte-17dya06.svelte-17dya06{font-size:1.1rem;color:var(--text-color);margin-bottom:16px}.processes-grid.svelte-17dya06.svelte-17dya06{display:grid;grid-template-columns:repeat(auto-fill, minmax(280px, 1fr));gap:16px}.loading.svelte-17dya06.svelte-17dya06{text-align:center;color:var(--muted-color);padding:40px}
+.config-page.svelte-1kruta9.svelte-1kruta9{padding:20px 0}.page-header.svelte-1kruta9.svelte-1kruta9{display:flex;justify-content:space-between;align-items:center;margin-bottom:24px}.page-header.svelte-1kruta9 h2.svelte-1kruta9{font-size:1.5rem;color:var(--text-color)}.refresh-btn.svelte-1kruta9.svelte-1kruta9{padding:8px 16px;background:var(--card-bg);border:1px solid var(--border-color);color:var(--text-color);border-radius:4px;cursor:pointer;font-size:0.9rem}.refresh-btn.svelte-1kruta9.svelte-1kruta9:hover:not(:disabled){background:var(--border-color)}.refresh-btn.svelte-1kruta9.svelte-1kruta9:disabled{opacity:0.5;cursor:not-allowed}.error-banner.svelte-1kruta9.svelte-1kruta9{background:#ffebee;color:#c62828;padding:12px 16px;border-radius:6px;margin-bottom:20px;border:1px solid #ffcdd2}.config-sections.svelte-1kruta9.svelte-1kruta9{display:flex;flex-direction:column;gap:24px}.config-section.svelte-1kruta9.svelte-1kruta9{background:var(--card-bg);border:1px solid var(--border-color);border-radius:8px;padding:20px}.config-section.svelte-1kruta9 h3.svelte-1kruta9{font-size:1.1rem;color:var(--text-color);margin-bottom:16px;padding-bottom:8px;border-bottom:1px solid var(--border-color)}.config-grid.svelte-1kruta9.svelte-1kruta9{display:grid;grid-template-columns:repeat(auto-fill, minmax(250px, 1fr));gap:16px}.config-item.svelte-1kruta9.svelte-1kruta9{display:flex;flex-direction:column;gap:4px}.config-item.full-width.svelte-1kruta9.svelte-1kruta9{grid-column:1 / -1}.config-item.svelte-1kruta9 .label.svelte-1kruta9{font-size:0.85rem;color:var(--muted-color)}.config-item.svelte-1kruta9 .value.svelte-1kruta9{font-size:0.95rem;color:var(--text-color)}.config-item.svelte-1kruta9 .value.mono.svelte-1kruta9{font-family:monospace;font-size:0.85rem}.config-item.svelte-1kruta9 .value.bool.svelte-1kruta9{font-weight:500}.config-item.svelte-1kruta9 .value.bool.enabled.svelte-1kruta9{color:var(--success)}.owners-list.svelte-1kruta9.svelte-1kruta9{display:flex;flex-wrap:wrap;gap:8px;margin-top:4px}.owner.svelte-1kruta9.svelte-1kruta9{font-size:0.75rem;background:var(--bg-color);padding:4px 8px;border-radius:4px;word-break:break-all}.no-owners.svelte-1kruta9.svelte-1kruta9{color:var(--muted-color);font-style:italic}.config-note.svelte-1kruta9.svelte-1kruta9{margin-top:24px;padding:16px;background:var(--card-bg);border:1px solid var(--border-color);border-radius:8px}.config-note.svelte-1kruta9 p.svelte-1kruta9{color:var(--muted-color);font-size:0.9rem;margin:0}.loading.svelte-1kruta9.svelte-1kruta9{text-align:center;color:var(--muted-color);padding:40px}
+.update-page.svelte-1ig49gt.svelte-1ig49gt{padding:20px 0}.page-header.svelte-1ig49gt.svelte-1ig49gt{margin-bottom:24px}.page-header.svelte-1ig49gt h2.svelte-1ig49gt{font-size:1.5rem;color:var(--text-color)}.error-banner.svelte-1ig49gt.svelte-1ig49gt{background:#ffebee;color:#c62828;padding:12px 16px;border-radius:6px;margin-bottom:20px;border:1px solid #ffcdd2}.success-banner.svelte-1ig49gt.svelte-1ig49gt{background:#e8f5e9;color:#2e7d32;padding:12px 16px;border-radius:6px;margin-bottom:20px;border:1px solid #c8e6c9}.current-version.svelte-1ig49gt.svelte-1ig49gt,.update-form.svelte-1ig49gt.svelte-1ig49gt,.versions-list.svelte-1ig49gt.svelte-1ig49gt{background:var(--card-bg);border:1px solid var(--border-color);border-radius:8px;padding:20px;margin-bottom:24px}h3.svelte-1ig49gt.svelte-1ig49gt{font-size:1.1rem;color:var(--text-color);margin-bottom:16px}.version-info.svelte-1ig49gt.svelte-1ig49gt{display:flex;align-items:center;justify-content:space-between}.version.svelte-1ig49gt.svelte-1ig49gt{font-size:1.5rem;font-weight:600;font-family:monospace;color:var(--text-color)}.rollback-btn.svelte-1ig49gt.svelte-1ig49gt{padding:8px 16px;background:var(--warning);border:none;color:white;border-radius:4px;cursor:pointer}.rollback-btn.svelte-1ig49gt.svelte-1ig49gt:hover:not(:disabled){opacity:0.9}.rollback-btn.svelte-1ig49gt.svelte-1ig49gt:disabled{opacity:0.5;cursor:not-allowed}.form-group.svelte-1ig49gt.svelte-1ig49gt{margin-bottom:20px}.form-group.svelte-1ig49gt>label.svelte-1ig49gt{display:block;font-size:0.9rem;color:var(--text-color);margin-bottom:8px;font-weight:500}.form-group.svelte-1ig49gt input[type="text"].svelte-1ig49gt{width:100%;padding:10px 12px;border:1px solid var(--border-color);border-radius:4px;font-size:0.95rem;background:var(--bg-color);color:var(--text-color)}.form-group.svelte-1ig49gt input.svelte-1ig49gt:focus{outline:none;border-color:var(--primary)}.url-header.svelte-1ig49gt.svelte-1ig49gt{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}.url-header.svelte-1ig49gt label.svelte-1ig49gt{font-size:0.9rem;color:var(--text-color);font-weight:500}.helper-btn.svelte-1ig49gt.svelte-1ig49gt{padding:4px 12px;font-size:0.8rem;background:var(--card-bg);border:1px solid var(--border-color);border-radius:4px;color:var(--text-color);cursor:pointer}.helper-btn.svelte-1ig49gt.svelte-1ig49gt:hover:not(:disabled){background:var(--border-color)}.url-input.svelte-1ig49gt.svelte-1ig49gt{display:flex;gap:12px;align-items:center;margin-bottom:8px}.binary-name.svelte-1ig49gt.svelte-1ig49gt{width:140px;font-family:monospace;font-size:0.85rem;color:var(--muted-color)}.url-input.svelte-1ig49gt input.svelte-1ig49gt{flex:1;padding:8px 12px;border:1px solid var(--border-color);border-radius:4px;font-size:0.85rem;background:var(--bg-color);color:var(--text-color)}.update-btn.svelte-1ig49gt.svelte-1ig49gt{width:100%;padding:12px;background:var(--primary);border:none;color:white;border-radius:6px;font-size:1rem;cursor:pointer}.update-btn.svelte-1ig49gt.svelte-1ig49gt:hover:not(:disabled){background:var(--primary-hover)}.update-btn.svelte-1ig49gt.svelte-1ig49gt:disabled{opacity:0.5;cursor:not-allowed}table.svelte-1ig49gt.svelte-1ig49gt{width:100%;border-collapse:collapse}th.svelte-1ig49gt.svelte-1ig49gt,td.svelte-1ig49gt.svelte-1ig49gt{padding:10px 12px;text-align:left;border-bottom:1px solid var(--border-color)}th.svelte-1ig49gt.svelte-1ig49gt{font-size:0.85rem;color:var(--muted-color);font-weight:500}td.svelte-1ig49gt.svelte-1ig49gt{font-size:0.9rem;color:var(--text-color)}.version-cell.svelte-1ig49gt.svelte-1ig49gt{font-family:monospace}tr.current.svelte-1ig49gt.svelte-1ig49gt{background:rgba(0, 188, 212, 0.1)}.current-badge.svelte-1ig49gt.svelte-1ig49gt{background:var(--primary);color:white;padding:2px 8px;border-radius:4px;font-size:0.75rem}
+*{box-sizing:border-box;margin:0;padding:0}body{font-family:system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;background:var(--bg-color);color:var(--text-color);min-height:100vh}main.svelte-4k9oqz.svelte-4k9oqz{--bg-color:#f5f5f5;--card-bg:#ffffff;--text-color:#333333;--muted-color:#666666;--border-color:#e0e0e0;--primary:#00bcd4;--primary-hover:#00acc1;--success:#4caf50;--error:#f44336;--warning:#ff9800;min-height:100vh;background:var(--bg-color)}main.dark-theme.svelte-4k9oqz.svelte-4k9oqz{--bg-color:#1a1a1a;--card-bg:#2d2d2d;--text-color:#e0e0e0;--muted-color:#999999;--border-color:#444444}.content.svelte-4k9oqz.svelte-4k9oqz{max-width:1200px;margin:0 auto;padding:20px}.login-prompt.svelte-4k9oqz.svelte-4k9oqz{text-align:center;padding:60px 20px}.login-prompt.svelte-4k9oqz h2.svelte-4k9oqz{font-size:2rem;margin-bottom:16px;color:var(--text-color)}.login-prompt.svelte-4k9oqz p.svelte-4k9oqz{color:var(--muted-color);margin-bottom:24px}.login-btn.svelte-4k9oqz.svelte-4k9oqz{padding:12px 32px;font-size:1rem;background:var(--primary);color:white;border:none;border-radius:6px;cursor:pointer;transition:background 0.2s}.login-btn.svelte-4k9oqz.svelte-4k9oqz:hover{background:var(--primary-hover)}
diff --git a/cmd/orly-launcher/web/dist/bundle.js b/cmd/orly-launcher/web/dist/bundle.js
new file mode 100644
index 0000000..ba68016
--- /dev/null
+++ b/cmd/orly-launcher/web/dist/bundle.js
@@ -0,0 +1,14 @@
+var app=function(){"use strict";function t(){}function e(t){return t()}function n(){return Object.create(null)}function r(t){t.forEach(e)}function s(t){return"function"==typeof t}function o(t,e){return t!=t?e==e:t!==e||t&&"object"==typeof t||"function"==typeof t}function i(e,n,r){e.$$.on_destroy.push(function(e,...n){if(null==e){for(const t of n)t(void 0);return t}const r=e.subscribe(...n);return r.unsubscribe?()=>r.unsubscribe():r}(n,r))}function a(t,e,n){return t.set(n),e}const l="undefined"!=typeof window?window:"undefined"!=typeof globalThis?globalThis:global;function c(t,e){t.appendChild(e)}function u(t,e,n){t.insertBefore(e,n||null)}function d(t){t.parentNode&&t.parentNode.removeChild(t)}function f(t,e){for(let n=0;nt.removeEventListener(e,n,r)}function w(t){return function(e){return e.stopPropagation(),t.call(this,e)}}function b(t,e,n){null==n?t.removeAttribute(e):t.getAttribute(e)!==n&&t.setAttribute(e,n)}function m(t,e){e=""+e,t.data!==e&&(t.data=e)}function v(t,e){t.value=null==e?"":e}function E(t,e,n,r){null==n?t.style.removeProperty(e):t.style.setProperty(e,n,"")}function x(t,e,n){t.classList.toggle(e,!!n)}let k;function $(t){k=t}function A(){if(!k)throw new Error("Function called outside component initialization");return k}function B(t){A().$$.on_mount.push(t)}function I(){const t=A();return(e,n,{cancelable:r=!1}={})=>{const s=t.$$.callbacks[e];if(s){const o=function(t,e,{bubbles:n=!1,cancelable:r=!1}={}){return new CustomEvent(t,{detail:e,bubbles:n,cancelable:r})}(e,n,{cancelable:r});return s.slice().forEach(e=>{e.call(t,o)}),!o.defaultPrevented}return!0}}function _(t,e){const n=t.$$.callbacks[e.type];n&&n.slice().forEach(t=>t.call(this,e))}const L=[],S=[];let C=[];const U=[],T=Promise.resolve();let N=!1;function O(t){C.push(t)}const R=new Set;let P=0;function D(){if(0!==P)return;const t=k;do{try{for(;P{H.delete(t),r&&(n&&t.d(1),r())}),t.o(e)}else r&&r()}function M(t){return void 0!==t?.length?t:Array.from(t)}function Z(t){t&&t.c()}function G(t,n,o){const{fragment:i,after_update:a}=t.$$;i&&i.m(n,o),O(()=>{const n=t.$$.on_mount.map(e).filter(s);t.$$.on_destroy?t.$$.on_destroy.push(...n):r(n),t.$$.on_mount=[]}),a.forEach(O)}function W(t,e){const n=t.$$;null!==n.fragment&&(!function(t){const e=[],n=[];C.forEach(r=>-1===t.indexOf(r)?e.push(r):n.push(r)),n.forEach(t=>t()),C=e}(n.after_update),r(n.on_destroy),n.fragment&&n.fragment.d(e),n.on_destroy=n.fragment=null,n.ctx=[])}function Y(t,e){-1===t.$$.dirty[0]&&(L.push(t),N||(N=!0,T.then(D)),t.$$.dirty.fill(0)),t.$$.dirty[e/31|0]|=1<{const s=r.length?r[0]:n;return h.ctx&&a(h.ctx[t],h.ctx[t]=s)&&(!h.skip_bound&&h.bound[t]&&h.bound[t](s),g&&Y(e,t)),n}):[],h.update(),g=!0,r(h.before_update),h.fragment=!!i&&i(h.ctx),s.target){if(s.hydrate){const t=function(t){return Array.from(t.childNodes)}(s.target);h.fragment&&h.fragment.l(t),t.forEach(d)}else h.fragment&&h.fragment.c();s.intro&&z(e.$$.fragment),G(e,s.target,s.anchor),D()}$(f)}class Q{$$=void 0;$$set=void 0;$destroy(){W(this,1),this.$destroy=t}$on(e,n){if(!s(n))return t;const r=this.$$.callbacks[e]||(this.$$.callbacks[e]=[]);return r.push(n),()=>{const t=r.indexOf(n);-1!==t&&r.splice(t,1)}}$set(t){var e;this.$$set&&(e=t,0!==Object.keys(e).length)&&(this.$$.skip_bound=!0,this.$$set(t),this.$$.skip_bound=!1)}}function X(e){let n,r,s;return{c(){n=h("button"),n.textContent="Login",b(n,"class","login-header-btn svelte-1bc06ax")},m(t,o){u(t,n,o),r||(s=y(n,"click",e[9]),r=!0)},p:t,d(t){t&&d(n),r=!1,s()}}}function tt(t){let e,n,s,o,i,a,l,f,w,v,E,k,$,A,B=nt(t[2])+"";return{c(){e=h("nav"),n=h("button"),n.textContent="Dashboard",s=p(),o=h("button"),o.textContent="Config",i=p(),a=h("button"),a.textContent="Update",l=p(),f=h("div"),w=h("span"),v=g(B),E=p(),k=h("button"),k.textContent="Logout",b(n,"class","nav-btn svelte-1bc06ax"),x(n,"active","dashboard"===t[0]),b(o,"class","nav-btn svelte-1bc06ax"),x(o,"active","config"===t[0]),b(a,"class","nav-btn svelte-1bc06ax"),x(a,"active","update"===t[0]),b(e,"class","svelte-1bc06ax"),b(w,"class","pubkey svelte-1bc06ax"),b(k,"class","logout-btn svelte-1bc06ax"),b(f,"class","user-section svelte-1bc06ax")},m(r,d){u(r,e,d),c(e,n),c(e,s),c(e,o),c(e,i),c(e,a),u(r,l,d),u(r,f,d),c(f,w),c(w,v),c(f,E),c(f,k),$||(A=[y(n,"click",t[5]),y(o,"click",t[6]),y(a,"click",t[7]),y(k,"click",t[8])],$=!0)},p(t,e){1&e&&x(n,"active","dashboard"===t[0]),1&e&&x(o,"active","config"===t[0]),1&e&&x(a,"active","update"===t[0]),4&e&&B!==(B=nt(t[2])+"")&&m(v,B)},d(t){t&&(d(e),d(l),d(f)),$=!1,r(A)}}}function et(e){let n,r,s,o;function i(t,e){return t[1]?tt:X}let a=i(e),l=a(e);return{c(){n=h("header"),r=h("div"),s=h("h1"),s.textContent="ORLY Launcher",o=p(),l.c(),b(s,"class","svelte-1bc06ax"),b(r,"class","header-content svelte-1bc06ax"),b(n,"class","svelte-1bc06ax")},m(t,e){u(t,n,e),c(n,r),c(r,s),c(r,o),l.m(r,null)},p(t,[e]){a===(a=i(t))&&l?l.p(t,e):(l.d(1),l=a(t),l&&(l.c(),l.m(r,null)))},i:t,o:t,d(t){t&&d(n),l.d()}}}function nt(t){return t?t.slice(0,8)+"..."+t.slice(-4):""}function rt(t,e,n){const r=I();let{currentPage:s="dashboard"}=e,{isLoggedIn:o=!1}=e,{userPubkey:i=""}=e;function a(t){r("navigate",t)}return t.$$set=t=>{"currentPage"in t&&n(0,s=t.currentPage),"isLoggedIn"in t&&n(1,o=t.isLoggedIn),"userPubkey"in t&&n(2,i=t.userPubkey)},[s,o,i,r,a,()=>a("dashboard"),()=>a("config"),()=>a("update"),()=>r("logout"),()=>r("login")]}"undefined"!=typeof window&&(window.__svelte||(window.__svelte={v:new Set})).v.add("4");class st extends Q{constructor(t){super(),J(this,t,rt,et,o,{currentPage:0,isLoggedIn:1,userPubkey:2})}}function ot(t){if(!Number.isSafeInteger(t)||t<0)throw new Error(`Wrong positive integer: ${t}`)}function it(t,...e){if(!(t instanceof Uint8Array))throw new Error("Expected Uint8Array");if(e.length>0&&!e.includes(t.length))throw new Error(`Expected Uint8Array of length ${e}, not of length=${t.length}`)}function at(t,e=!0){if(t.destroyed)throw new Error("Hash instance has been destroyed");if(e&&t.finished)throw new Error("Hash#digest() has already been called")}const lt="object"==typeof globalThis&&"crypto"in globalThis?globalThis.crypto:void 0,ct=t=>t instanceof Uint8Array,ut=t=>new DataView(t.buffer,t.byteOffset,t.byteLength),dt=(t,e)=>t<<32-e|t>>>e;
+/*! noble-hashes - MIT License (c) 2022 Paul Miller (paulmillr.com) */if(!(68===new Uint8Array(new Uint32Array([287454020]).buffer)[0]))throw new Error("Non little-endian hardware is not supported");function ft(t){if("string"==typeof t&&(t=function(t){if("string"!=typeof t)throw new Error("utf8ToBytes expected string, got "+typeof t);return new Uint8Array((new TextEncoder).encode(t))}(t)),!ct(t))throw new Error("expected Uint8Array, got "+typeof t);return t}let ht=class{clone(){return this._cloneInto()}};function gt(t){const e=e=>t().update(ft(e)).digest(),n=t();return e.outputLen=n.outputLen,e.blockLen=n.blockLen,e.create=()=>t(),e}function pt(t=32){if(lt&&"function"==typeof lt.getRandomValues)return lt.getRandomValues(new Uint8Array(t));throw new Error("crypto.getRandomValues must be defined")}let yt=class extends ht{constructor(t,e,n,r){super(),this.blockLen=t,this.outputLen=e,this.padOffset=n,this.isLE=r,this.finished=!1,this.length=0,this.pos=0,this.destroyed=!1,this.buffer=new Uint8Array(t),this.view=ut(this.buffer)}update(t){at(this);const{view:e,buffer:n,blockLen:r}=this,s=(t=ft(t)).length;for(let o=0;or-o&&(this.process(n,0),o=0);for(let t=o;t>s&o),a=Number(n&o),l=r?4:0,c=r?0:4;t.setUint32(e+l,i,r),t.setUint32(e+c,a,r)}(n,r-8,BigInt(8*this.length),s),this.process(n,0);const i=ut(t),a=this.outputLen;if(a%4)throw new Error("_sha2: outputLen should be aligned to 32bit");const l=a/4,c=this.get();if(l>c.length)throw new Error("_sha2: outputLen bigger than state");for(let t=0;tt&e^~t&n,bt=(t,e,n)=>t&e^t&n^e&n,mt=new Uint32Array([1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,3329325298]),vt=new Uint32Array([1779033703,3144134277,1013904242,2773480762,1359893119,2600822924,528734635,1541459225]),Et=new Uint32Array(64);let xt=class extends yt{constructor(){super(64,32,8,!1),this.A=0|vt[0],this.B=0|vt[1],this.C=0|vt[2],this.D=0|vt[3],this.E=0|vt[4],this.F=0|vt[5],this.G=0|vt[6],this.H=0|vt[7]}get(){const{A:t,B:e,C:n,D:r,E:s,F:o,G:i,H:a}=this;return[t,e,n,r,s,o,i,a]}set(t,e,n,r,s,o,i,a){this.A=0|t,this.B=0|e,this.C=0|n,this.D=0|r,this.E=0|s,this.F=0|o,this.G=0|i,this.H=0|a}process(t,e){for(let n=0;n<16;n++,e+=4)Et[n]=t.getUint32(e,!1);for(let t=16;t<64;t++){const e=Et[t-15],n=Et[t-2],r=dt(e,7)^dt(e,18)^e>>>3,s=dt(n,17)^dt(n,19)^n>>>10;Et[t]=s+Et[t-7]+r+Et[t-16]|0}let{A:n,B:r,C:s,D:o,E:i,F:a,G:l,H:c}=this;for(let t=0;t<64;t++){const e=c+(dt(i,6)^dt(i,11)^dt(i,25))+wt(i,a,l)+mt[t]+Et[t]|0,u=(dt(n,2)^dt(n,13)^dt(n,22))+bt(n,r,s)|0;c=l,l=a,a=i,i=o+e|0,o=s,s=r,r=n,n=e+u|0}n=n+this.A|0,r=r+this.B|0,s=s+this.C|0,o=o+this.D|0,i=i+this.E|0,a=a+this.F|0,l=l+this.G|0,c=c+this.H|0,this.set(n,r,s,o,i,a,l,c)}roundClean(){Et.fill(0)}destroy(){this.set(0,0,0,0,0,0,0,0),this.buffer.fill(0)}};const kt=gt(()=>new xt);
+/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */BigInt(0);const $t=BigInt(1),At=BigInt(2),Bt=t=>t instanceof Uint8Array,It=Array.from({length:256},(t,e)=>e.toString(16).padStart(2,"0"));function _t(t){if(!Bt(t))throw new Error("Uint8Array expected");let e="";for(let n=0;nt+e.length,0));let n=0;return t.forEach(t=>{if(!Bt(t))throw new Error("Uint8Array expected");e.set(t,n),n+=t.length}),e}const Pt=t=>(At<new Uint8Array(t),qt=t=>Uint8Array.from(t);function Ht(t,e,n){if("number"!=typeof t||t<2)throw new Error("hashLen must be a number");if("number"!=typeof e||e<2)throw new Error("qByteLen must be a number");if("function"!=typeof n)throw new Error("hmacFn must be a function");let r=Dt(t),s=Dt(t),o=0;const i=()=>{r.fill(1),s.fill(0),o=0},a=(...t)=>n(s,r,...t),l=(t=Dt())=>{s=a(qt([0]),t),r=a(),0!==t.length&&(s=a(qt([1]),t),r=a())},c=()=>{if(o++>=1e3)throw new Error("drbg: tried 1000 values");let t=0;const n=[];for(;t{let n;for(i(),l(t);!(n=e(c()));)l();return i(),n}}const Ft={bigint:t=>"bigint"==typeof t,function:t=>"function"==typeof t,boolean:t=>"boolean"==typeof t,string:t=>"string"==typeof t,stringOrUint8Array:t=>"string"==typeof t||t instanceof Uint8Array,isSafeInteger:t=>Number.isSafeInteger(t),array:t=>Array.isArray(t),field:(t,e)=>e.Fp.isValid(t),hash:t=>"function"==typeof t&&Number.isSafeInteger(t.outputLen)};function Vt(t,e,n={}){const r=(e,n,r)=>{const s=Ft[n];if("function"!=typeof s)throw new Error(`Invalid validator "${n}", expected function`);const o=t[e];if(!(r&&void 0===o||s(o,t)))throw new Error(`Invalid param ${String(e)}=${o} (${typeof o}), expected ${n}`)};for(const[t,n]of Object.entries(e))r(t,n,!1);for(const[t,e]of Object.entries(n))r(t,e,!0);return t}var jt=Object.freeze({__proto__:null,bitMask:Pt,bytesToHex:_t,bytesToNumberBE:Ct,bytesToNumberLE:Ut,concatBytes:Rt,createHmacDrbg:Ht,ensureBytes:Ot,hexToBytes:St,hexToNumber:Lt,numberToBytesBE:Tt,numberToBytesLE:Nt,validateObject:Vt});
+/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */const zt=BigInt(0),Kt=BigInt(1),Mt=BigInt(2),Zt=BigInt(3),Gt=BigInt(4),Wt=BigInt(5),Yt=BigInt(8);function Jt(t,e){const n=t%e;return n>=zt?n:e+n}function Qt(t,e,n){if(n<=zt||e 0");if(n===Kt)return zt;let r=Kt;for(;e>zt;)e&Kt&&(r=r*t%n),t=t*t%n,e>>=Kt;return r}function Xt(t,e,n){let r=t;for(;e-- >zt;)r*=r,r%=n;return r}function te(t,e){if(t===zt||e<=zt)throw new Error(`invert: expected positive integers, got n=${t} mod=${e}`);let n=Jt(t,e),r=e,s=zt,o=Kt;for(;n!==zt;){const t=r%n,e=s-o*(r/n);r=n,n=t,s=o,o=e}if(r!==Kt)throw new Error("invert: does not exist");return Jt(s,e)}function ee(t){if(t%Gt===Zt){const e=(t+Kt)/Gt;return function(t,n){const r=t.pow(n,e);if(!t.eql(t.sqr(r),n))throw new Error("Cannot find square root");return r}}if(t%Yt===Wt){const e=(t-Wt)/Yt;return function(t,n){const r=t.mul(n,Mt),s=t.pow(r,e),o=t.mul(n,s),i=t.mul(t.mul(o,Mt),s),a=t.mul(o,t.sub(i,t.ONE));if(!t.eql(t.sqr(a),n))throw new Error("Cannot find square root");return a}}return function(t){const e=(t-Kt)/Mt;let n,r,s;for(n=t-Kt,r=0;n%Mt===zt;n/=Mt,r++);for(s=Mt;s(t[e]="function",t),{ORDER:"bigint",MASK:"bigint",BYTES:"isSafeInteger",BITS:"isSafeInteger"})),Vt(t,{n:"bigint",h:"bigint",Gx:"field",Gy:"field"},{nBitLength:"isSafeInteger",nByteLength:"isSafeInteger"}),Object.freeze({...re(t.n,t.nBitLength),...t,p:t.Fp.ORDER})}
+/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */const{bytesToNumberBE:ce,hexToBytes:ue}=jt,de={Err:class extends Error{constructor(t=""){super(t)}},_parseInt(t){const{Err:e}=de;if(t.length<2||2!==t[0])throw new e("Invalid signature integer tag");const n=t[1],r=t.subarray(2,n+2);if(!n||r.length!==n)throw new e("Invalid signature integer: wrong length");if(128&r[0])throw new e("Invalid signature integer: negative");if(0===r[0]&&!(128&r[1]))throw new e("Invalid signature integer: unnecessary leading zero");return{d:ce(r),l:t.subarray(n+2)}},toSig(t){const{Err:e}=de,n="string"==typeof t?ue(t):t;if(!(n instanceof Uint8Array))throw new Error("ui8a expected");let r=n.length;if(r<2||48!=n[0])throw new e("Invalid signature tag");if(n[1]!==r-2)throw new e("Invalid signature: incorrect length");const{d:s,l:o}=de._parseInt(n.subarray(2)),{d:i,l:a}=de._parseInt(o);if(a.length)throw new e("Invalid signature: left bytes after parsing");return{r:s,s:i}},hexFromSig(t){const e=t=>8&Number.parseInt(t[0],16)?"00"+t:t,n=t=>{const e=t.toString(16);return 1&e.length?`0${e}`:e},r=e(n(t.s)),s=e(n(t.r)),o=r.length/2,i=s.length/2,a=n(o),l=n(i);return`30${n(i+o+4)}02${l}${s}02${a}${r}`}},fe=BigInt(0),he=BigInt(1);BigInt(2);const ge=BigInt(3);function pe(t){const e=function(t){const e=le(t);Vt(e,{a:"field",b:"field"},{allowedPrivateKeyLengths:"array",wrapPrivateKey:"boolean",isTorsionFree:"function",clearCofactor:"function",allowInfinityPoint:"boolean",fromBytes:"function",toBytes:"function"});const{endo:n,Fp:r,a:s}=e;if(n){if(!r.eql(s,r.ZERO))throw new Error("Endomorphism can only be defined for Koblitz curves that have a=0");if("object"!=typeof n||"bigint"!=typeof n.beta||"function"!=typeof n.splitScalar)throw new Error("Expected endomorphism with beta: bigint and splitScalar: function")}return Object.freeze({...e})}(t),{Fp:n}=e,r=e.toBytes||((t,e,r)=>{const s=e.toAffine();return Rt(Uint8Array.from([4]),n.toBytes(s.x),n.toBytes(s.y))}),s=e.fromBytes||(t=>{const e=t.subarray(1);return{x:n.fromBytes(e.subarray(0,n.BYTES)),y:n.fromBytes(e.subarray(n.BYTES,2*n.BYTES))}});function o(t){const{a:r,b:s}=e,o=n.sqr(t),i=n.mul(o,t);return n.add(n.add(i,n.mul(t,r)),s)}if(!n.eql(n.sqr(e.Gy),o(e.Gx)))throw new Error("bad generator point: equation left != right");function i(t){return"bigint"==typeof t&&fen.eql(t,n.ZERO);return s(e)&&s(r)?d.ZERO:new d(e,r,n.ONE)}get x(){return this.toAffine().x}get y(){return this.toAffine().y}static normalizeZ(t){const e=n.invertBatch(t.map(t=>t.pz));return t.map((t,n)=>t.toAffine(e[n])).map(d.fromAffine)}static fromHex(t){const e=d.fromAffine(s(Ot("pointHex",t)));return e.assertValidity(),e}static fromPrivateKey(t){return d.BASE.multiply(l(t))}_setWindowSize(t){this._WINDOW_SIZE=t,c.delete(this)}assertValidity(){if(this.is0()){if(e.allowInfinityPoint&&!n.is0(this.py))return;throw new Error("bad point: ZERO")}const{x:t,y:r}=this.toAffine();if(!n.isValid(t)||!n.isValid(r))throw new Error("bad point: x or y not FE");const s=n.sqr(r),i=o(t);if(!n.eql(s,i))throw new Error("bad point: equation left != right");if(!this.isTorsionFree())throw new Error("bad point: not in prime-order subgroup")}hasEvenY(){const{y:t}=this.toAffine();if(n.isOdd)return!n.isOdd(t);throw new Error("Field doesn't support isOdd")}equals(t){u(t);const{px:e,py:r,pz:s}=this,{px:o,py:i,pz:a}=t,l=n.eql(n.mul(e,a),n.mul(o,s)),c=n.eql(n.mul(r,a),n.mul(i,s));return l&&c}negate(){return new d(this.px,n.neg(this.py),this.pz)}double(){const{a:t,b:r}=e,s=n.mul(r,ge),{px:o,py:i,pz:a}=this;let l=n.ZERO,c=n.ZERO,u=n.ZERO,f=n.mul(o,o),h=n.mul(i,i),g=n.mul(a,a),p=n.mul(o,i);return p=n.add(p,p),u=n.mul(o,a),u=n.add(u,u),l=n.mul(t,u),c=n.mul(s,g),c=n.add(l,c),l=n.sub(h,c),c=n.add(h,c),c=n.mul(l,c),l=n.mul(p,l),u=n.mul(s,u),g=n.mul(t,g),p=n.sub(f,g),p=n.mul(t,p),p=n.add(p,u),u=n.add(f,f),f=n.add(u,f),f=n.add(f,g),f=n.mul(f,p),c=n.add(c,f),g=n.mul(i,a),g=n.add(g,g),f=n.mul(g,p),l=n.sub(l,f),u=n.mul(g,h),u=n.add(u,u),u=n.add(u,u),new d(l,c,u)}add(t){u(t);const{px:r,py:s,pz:o}=this,{px:i,py:a,pz:l}=t;let c=n.ZERO,f=n.ZERO,h=n.ZERO;const g=e.a,p=n.mul(e.b,ge);let y=n.mul(r,i),w=n.mul(s,a),b=n.mul(o,l),m=n.add(r,s),v=n.add(i,a);m=n.mul(m,v),v=n.add(y,w),m=n.sub(m,v),v=n.add(r,o);let E=n.add(i,l);return v=n.mul(v,E),E=n.add(y,b),v=n.sub(v,E),E=n.add(s,o),c=n.add(a,l),E=n.mul(E,c),c=n.add(w,b),E=n.sub(E,c),h=n.mul(g,v),c=n.mul(p,b),h=n.add(c,h),c=n.sub(w,h),h=n.add(w,h),f=n.mul(c,h),w=n.add(y,y),w=n.add(w,y),b=n.mul(g,b),v=n.mul(p,v),w=n.add(w,b),b=n.sub(y,b),b=n.mul(g,b),v=n.add(v,b),y=n.mul(w,v),f=n.add(f,y),y=n.mul(E,v),c=n.mul(m,c),c=n.sub(c,y),y=n.mul(m,w),h=n.mul(E,h),h=n.add(h,y),new d(c,f,h)}subtract(t){return this.add(t.negate())}is0(){return this.equals(d.ZERO)}wNAF(t){return h.wNAFCached(this,c,t,t=>{const e=n.invertBatch(t.map(t=>t.pz));return t.map((t,n)=>t.toAffine(e[n])).map(d.fromAffine)})}multiplyUnsafe(t){const r=d.ZERO;if(t===fe)return r;if(a(t),t===he)return this;const{endo:s}=e;if(!s)return h.unsafeLadder(this,t);let{k1neg:o,k1:i,k2neg:l,k2:c}=s.splitScalar(t),u=r,f=r,g=this;for(;i>fe||c>fe;)i&he&&(u=u.add(g)),c&he&&(f=f.add(g)),g=g.double(),i>>=he,c>>=he;return o&&(u=u.negate()),l&&(f=f.negate()),f=new d(n.mul(f.px,s.beta),f.py,f.pz),u.add(f)}multiply(t){a(t);let r,s,o=t;const{endo:i}=e;if(i){const{k1neg:t,k1:e,k2neg:a,k2:l}=i.splitScalar(o);let{p:c,f:u}=this.wNAF(e),{p:f,f:g}=this.wNAF(l);c=h.constTimeNegate(t,c),f=h.constTimeNegate(a,f),f=new d(n.mul(f.px,i.beta),f.py,f.pz),r=c.add(f),s=u.add(g)}else{const{p:t,f:e}=this.wNAF(o);r=t,s=e}return d.normalizeZ([r,s])[0]}multiplyAndAddUnsafe(t,e,n){const r=d.BASE,s=(t,e)=>e!==fe&&e!==he&&t.equals(r)?t.multiply(e):t.multiplyUnsafe(e),o=s(this,e).add(s(t,n));return o.is0()?void 0:o}toAffine(t){const{px:e,py:r,pz:s}=this,o=this.is0();null==t&&(t=o?n.ONE:n.inv(s));const i=n.mul(e,t),a=n.mul(r,t),l=n.mul(s,t);if(o)return{x:n.ZERO,y:n.ZERO};if(!n.eql(l,n.ONE))throw new Error("invZ was invalid");return{x:i,y:a}}isTorsionFree(){const{h:t,isTorsionFree:n}=e;if(t===he)return!0;if(n)return n(d,this);throw new Error("isTorsionFree() has not been declared for the elliptic curve")}clearCofactor(){const{h:t,clearCofactor:n}=e;return t===he?this:n?n(d,this):this.multiplyUnsafe(e.h)}toRawBytes(t=!0){return this.assertValidity(),r(d,this,t)}toHex(t=!0){return _t(this.toRawBytes(t))}}d.BASE=new d(e.Gx,e.Gy,n.ONE),d.ZERO=new d(n.ZERO,n.ONE,n.ZERO);const f=e.nBitLength,h=function(t,e){const n=(t,e)=>{const n=e.negate();return t?n:e},r=t=>({windows:Math.ceil(e/t)+1,windowSize:2**(t-1)});return{constTimeNegate:n,unsafeLadder(e,n){let r=t.ZERO,s=e;for(;n>ie;)n&ae&&(r=r.add(s)),s=s.double(),n>>=ae;return r},precomputeWindow(t,e){const{windows:n,windowSize:s}=r(e),o=[];let i=t,a=i;for(let t=0;t>=f,r>a&&(r-=d,o+=ae);const i=e,h=e+Math.abs(r)-1,g=t%2!=0,p=r<0;0===r?c=c.add(n(g,s[i])):l=l.add(n(p,s[h]))}return{p:l,f:c}},wNAFCached(t,e,n,r){const s=t._WINDOW_SIZE||1;let o=e.get(t);return o||(o=this.precomputeWindow(t,s),1!==s&&e.set(t,r(o))),this.wNAF(s,o,n)}}}(d,e.endo?Math.ceil(f/2):f);return{CURVE:e,ProjectivePoint:d,normPrivateKeyToScalar:l,weierstrassEquation:o,isWithinCurveOrder:i}}function ye(t){const e=function(t){const e=le(t);return Vt(e,{hash:"hash",hmac:"function",randomBytes:"function"},{bits2int:"function",bits2int_modN:"function",lowS:"boolean"}),Object.freeze({lowS:!0,...e})}(t),{Fp:n,n:r}=e,s=n.BYTES+1,o=2*n.BYTES+1;function i(t){return Jt(t,r)}function a(t){return te(t,r)}const{ProjectivePoint:l,normPrivateKeyToScalar:c,weierstrassEquation:u,isWithinCurveOrder:d}=pe({...e,toBytes(t,e,r){const s=e.toAffine(),o=n.toBytes(s.x),i=Rt;return r?i(Uint8Array.from([e.hasEvenY()?2:3]),o):i(Uint8Array.from([4]),o,n.toBytes(s.y))},fromBytes(t){const e=t.length,r=t[0],i=t.subarray(1);if(e!==s||2!==r&&3!==r){if(e===o&&4===r){return{x:n.fromBytes(i.subarray(0,n.BYTES)),y:n.fromBytes(i.subarray(n.BYTES,2*n.BYTES))}}throw new Error(`Point of length ${e} was invalid. Expected ${s} compressed bytes or ${o} uncompressed bytes`)}{const t=Ct(i);if(!(fe<(a=t)&&a_t(Tt(t,e.nByteLength));function h(t){return t>r>>he}const g=(t,e,n)=>Ct(t.slice(e,n));class p{constructor(t,e,n){this.r=t,this.s=e,this.recovery=n,this.assertValidity()}static fromCompact(t){const n=e.nByteLength;return t=Ot("compactSignature",t,2*n),new p(g(t,0,n),g(t,n,2*n))}static fromDER(t){const{r:e,s:n}=de.toSig(Ot("DER",t));return new p(e,n)}assertValidity(){if(!d(this.r))throw new Error("r must be 0 < r < CURVE.n");if(!d(this.s))throw new Error("s must be 0 < s < CURVE.n")}addRecoveryBit(t){return new p(this.r,this.s,t)}recoverPublicKey(t){const{r:r,s:s,recovery:o}=this,c=m(Ot("msgHash",t));if(null==o||![0,1,2,3].includes(o))throw new Error("recovery id invalid");const u=2===o||3===o?r+e.n:r;if(u>=n.ORDER)throw new Error("recovery id 2 or 3 invalid");const d=1&o?"03":"02",h=l.fromHex(d+f(u)),g=a(u),p=i(-c*g),y=i(s*g),w=l.BASE.multiplyAndAddUnsafe(h,p,y);if(!w)throw new Error("point at infinify");return w.assertValidity(),w}hasHighS(){return h(this.s)}normalizeS(){return this.hasHighS()?new p(this.r,i(-this.s),this.recovery):this}toDERRawBytes(){return St(this.toDERHex())}toDERHex(){return de.hexFromSig({r:this.r,s:this.s})}toCompactRawBytes(){return St(this.toCompactHex())}toCompactHex(){return f(this.r)+f(this.s)}}const y={isValidPrivateKey(t){try{return c(t),!0}catch(t){return!1}},normPrivateKeyToScalar:c,randomPrivateKey:()=>{const t=oe(e.n);return function(t,e,n=!1){const r=t.length,s=se(e),o=oe(e);if(r<16||r1024)throw new Error(`expected ${o}-1024 bytes of input, got ${r}`);const i=Jt(n?Ct(t):Ut(t),e-Kt)+Kt;return n?Nt(i,s):Tt(i,s)}(e.randomBytes(t),e.n)},precompute:(t=8,e=l.BASE)=>(e._setWindowSize(t),e.multiply(BigInt(3)),e)};function w(t){const e=t instanceof Uint8Array,n="string"==typeof t,r=(e||n)&&t.length;return e?r===s||r===o:n?r===2*s||r===2*o:t instanceof l}const b=e.bits2int||function(t){const n=Ct(t),r=8*t.length-e.nBitLength;return r>0?n>>BigInt(r):n},m=e.bits2int_modN||function(t){return i(b(t))},v=Pt(e.nBitLength);function E(t){if("bigint"!=typeof t)throw new Error("bigint expected");if(!(fe<=t&&tt in s))throw new Error("sign() legacy options not supported");const{hash:o,randomBytes:u}=e;let{lowS:f,prehash:g,extraEntropy:y}=s;null==f&&(f=!0),t=Ot("msgHash",t),g&&(t=Ot("prehashed msgHash",o(t)));const w=m(t),v=c(r),x=[E(v),E(w)];if(null!=y){const t=!0===y?u(n.BYTES):y;x.push(Ot("extraEntropy",t))}const $=Rt(...x),A=w;return{seed:$,k2sig:function(t){const e=b(t);if(!d(e))return;const n=a(e),r=l.BASE.multiply(e).toAffine(),s=i(r.x);if(s===fe)return;const o=i(n*i(A+s*v));if(o===fe)return;let c=(r.x===s?0:2)|Number(r.y&he),u=o;return f&&h(o)&&(u=function(t){return h(t)?i(-t):t}(o),c^=1),new p(s,u,c)}}}const k={lowS:e.lowS,prehash:!1},$={lowS:e.lowS,prehash:!1};return l.BASE._setWindowSize(8),{CURVE:e,getPublicKey:function(t,e=!0){return l.fromPrivateKey(t).toRawBytes(e)},getSharedSecret:function(t,e,n=!0){if(w(t))throw new Error("first arg must be private key");if(!w(e))throw new Error("second arg must be public key");return l.fromHex(e).multiply(c(t)).toRawBytes(n)},sign:function(t,n,r=k){const{seed:s,k2sig:o}=x(t,n,r),i=e;return Ht(i.hash.outputLen,i.nByteLength,i.hmac)(s,o)},verify:function(t,n,r,s=$){const o=t;if(n=Ot("msgHash",n),r=Ot("publicKey",r),"strict"in s)throw new Error("options.strict was renamed to lowS");const{lowS:c,prehash:u}=s;let d,f;try{if("string"==typeof o||o instanceof Uint8Array)try{d=p.fromDER(o)}catch(t){if(!(t instanceof de.Err))throw t;d=p.fromCompact(o)}else{if("object"!=typeof o||"bigint"!=typeof o.r||"bigint"!=typeof o.s)throw new Error("PARSE");{const{r:t,s:e}=o;d=new p(t,e)}}f=l.fromHex(r)}catch(t){if("PARSE"===t.message)throw new Error("signature must be Signature instance, Uint8Array or hex string");return!1}if(c&&d.hasHighS())return!1;u&&(n=e.hash(n));const{r:h,s:g}=d,y=m(n),w=a(g),b=i(y*w),v=i(h*w),E=l.BASE.multiplyAndAddUnsafe(f,b,v)?.toAffine();return!!E&&i(E.x)===h},ProjectivePoint:l,Signature:p,utils:y}}BigInt(4);class we extends ht{constructor(t,e){super(),this.finished=!1,this.destroyed=!1,function(t){if("function"!=typeof t||"function"!=typeof t.create)throw new Error("Hash should be wrapped by utils.wrapConstructor");ot(t.outputLen),ot(t.blockLen)}(t);const n=ft(e);if(this.iHash=t.create(),"function"!=typeof this.iHash.update)throw new Error("Expected instance of class which extends utils.Hash");this.blockLen=this.iHash.blockLen,this.outputLen=this.iHash.outputLen;const r=this.blockLen,s=new Uint8Array(r);s.set(n.length>r?t.create().update(n).digest():n);for(let t=0;tnew we(t,e).update(n).digest();
+/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
+function me(t){return{hash:t,hmac:(e,...n)=>be(t,e,function(...t){const e=new Uint8Array(t.reduce((t,e)=>t+e.length,0));let n=0;return t.forEach(t=>{if(!ct(t))throw new Error("Uint8Array expected");e.set(t,n),n+=t.length}),e}(...n)),randomBytes:pt}}be.create=(t,e)=>new we(t,e);
+/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
+const ve=BigInt("0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f"),Ee=BigInt("0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141"),xe=BigInt(1),ke=BigInt(2),$e=(t,e)=>(t+e/ke)/e;function Ae(t){const e=ve,n=BigInt(3),r=BigInt(6),s=BigInt(11),o=BigInt(22),i=BigInt(23),a=BigInt(44),l=BigInt(88),c=t*t*t%e,u=c*c*t%e,d=Xt(u,n,e)*u%e,f=Xt(d,n,e)*u%e,h=Xt(f,ke,e)*c%e,g=Xt(h,s,e)*h%e,p=Xt(g,o,e)*g%e,y=Xt(p,a,e)*p%e,w=Xt(y,l,e)*y%e,b=Xt(w,a,e)*p%e,m=Xt(b,n,e)*u%e,v=Xt(m,i,e)*g%e,E=Xt(v,r,e)*c%e,x=Xt(E,ke,e);if(!Be.eql(Be.sqr(x),t))throw new Error("Cannot find square root");return x}const Be=function(t,e,n=!1,r={}){if(t<=zt)throw new Error(`Expected Field ORDER > 0, got ${t}`);const{nBitLength:s,nByteLength:o}=re(t,e);if(o>2048)throw new Error("Field lengths over 2048 bytes are not supported");const i=ee(t),a=Object.freeze({ORDER:t,BITS:s,BYTES:o,MASK:Pt(s),ZERO:zt,ONE:Kt,create:e=>Jt(e,t),isValid:e=>{if("bigint"!=typeof e)throw new Error("Invalid field element: expected bigint, got "+typeof e);return zt<=e&&et===zt,isOdd:t=>(t&Kt)===Kt,neg:e=>Jt(-e,t),eql:(t,e)=>t===e,sqr:e=>Jt(e*e,t),add:(e,n)=>Jt(e+n,t),sub:(e,n)=>Jt(e-n,t),mul:(e,n)=>Jt(e*n,t),pow:(t,e)=>function(t,e,n){if(n 0");if(n===zt)return t.ONE;if(n===Kt)return e;let r=t.ONE,s=e;for(;n>zt;)n&Kt&&(r=t.mul(r,s)),s=t.sqr(s),n>>=Kt;return r}(a,t,e),div:(e,n)=>Jt(e*te(n,t),t),sqrN:t=>t*t,addN:(t,e)=>t+e,subN:(t,e)=>t-e,mulN:(t,e)=>t*e,inv:e=>te(e,t),sqrt:r.sqrt||(t=>i(a,t)),invertBatch:t=>function(t,e){const n=new Array(e.length),r=e.reduce((e,r,s)=>t.is0(r)?e:(n[s]=e,t.mul(e,r)),t.ONE),s=t.inv(r);return e.reduceRight((e,r,s)=>t.is0(r)?e:(n[s]=t.mul(e,n[s]),t.mul(e,r)),s),n}(a,t),cmov:(t,e,n)=>n?e:t,toBytes:t=>n?Nt(t,o):Tt(t,o),fromBytes:t=>{if(t.length!==o)throw new Error(`Fp.fromBytes: expected ${o}, got ${t.length}`);return n?Ut(t):Ct(t)}});return Object.freeze(a)}(ve,void 0,void 0,{sqrt:Ae}),Ie=function(t,e){const n=e=>ye({...t,...me(e)});return Object.freeze({...n(e),create:n})}({a:BigInt(0),b:BigInt(7),Fp:Be,n:Ee,Gx:BigInt("55066263022277343669578718895168534326250603453777594175500187360389116729240"),Gy:BigInt("32670510020758816978083085130507043184471273380659243275938904335757337482424"),h:BigInt(1),lowS:!0,endo:{beta:BigInt("0x7ae96a2b657c07106e64479eac3434e99cf0497512f58995c1396c28719501ee"),splitScalar:t=>{const e=Ee,n=BigInt("0x3086d221a7d46bcde86c90e49284eb15"),r=-xe*BigInt("0xe4437ed6010e88286f547fa90abfe4c3"),s=BigInt("0x114ca50f7a8e2f3f657c1108d9d44cfd8"),o=n,i=BigInt("0x100000000000000000000000000000000"),a=$e(o*t,e),l=$e(-r*t,e);let c=Jt(t-a*n-l*s,e),u=Jt(-a*r-l*o,e);const d=c>i,f=u>i;if(d&&(c=e-c),f&&(u=e-u),c>i||u>i)throw new Error("splitScalar: Endomorphism failed, k="+t);return{k1neg:d,k1:c,k2neg:f,k2:u}}}},kt),_e=BigInt(0),Le=t=>"bigint"==typeof t&&_et.charCodeAt(0)));n=Rt(e,e),Se[t]=n}return kt(Rt(n,...e))}const Ue=t=>t.toRawBytes(!0).slice(1),Te=t=>Tt(t,32),Ne=t=>Jt(t,ve),Oe=t=>Jt(t,Ee),Re=Ie.ProjectivePoint;function Pe(t){let e=Ie.utils.normPrivateKeyToScalar(t),n=Re.fromPrivateKey(e);return{scalar:n.hasEvenY()?e:Oe(-e),bytes:Ue(n)}}function De(t){if(!Le(t))throw new Error("bad x: need 0 < x < p");const e=Ne(t*t);let n=Ae(Ne(e*t+BigInt(7)));n%ke!==_e&&(n=Ne(-n));const r=new Re(t,n,xe);return r.assertValidity(),r}function qe(...t){return Oe(Ct(Ce("BIP0340/challenge",...t)))}function He(t){return Pe(t).bytes}function Fe(t,e,n=pt(32)){const r=Ot("message",t),{bytes:s,scalar:o}=Pe(e),i=Ot("auxRand",n,32),a=Te(o^Ct(Ce("BIP0340/aux",i))),l=Ce("BIP0340/nonce",a,s,r),c=Oe(Ct(l));if(c===_e)throw new Error("sign failed: k is zero");const{bytes:u,scalar:d}=Pe(c),f=qe(u,s,r),h=new Uint8Array(64);if(h.set(u,0),h.set(Te(Oe(d+f*o)),32),!Ve(h,r,s))throw new Error("sign: Invalid signature produced");return h}function Ve(t,e,n){const r=Ot("signature",t,64),s=Ot("message",e),o=Ot("publicKey",n,32);try{const t=De(Ct(o)),e=Ct(r.subarray(0,32));if(!Le(e))return!1;const n=Ct(r.subarray(32,64));if(!("bigint"==typeof(c=n)&&_e({getPublicKey:He,sign:Fe,verify:Ve,utils:{randomPrivateKey:Ie.utils.randomPrivateKey,lift_x:De,pointToBytes:Ue,numberToBytesBE:Tt,bytesToNumberBE:Ct,taggedHash:Ce,mod:Jt}}))(),ze=t=>t instanceof Uint8Array,Ke=t=>new DataView(t.buffer,t.byteOffset,t.byteLength),Me=(t,e)=>t<<32-e|t>>>e;
+/*! noble-hashes - MIT License (c) 2022 Paul Miller (paulmillr.com) */if(!(68===new Uint8Array(new Uint32Array([287454020]).buffer)[0]))throw new Error("Non little-endian hardware is not supported");const Ze=Array.from({length:256},(t,e)=>e.toString(16).padStart(2,"0"));function Ge(t){if(!ze(t))throw new Error("Uint8Array expected");let e="";for(let n=0;nt().update(We(e)).digest(),n=t();return e.outputLen=n.outputLen,e.blockLen=n.blockLen,e.create=()=>t(),e}function Qe(t){if(!Number.isSafeInteger(t)||t<0)throw new Error(`Wrong positive integer: ${t}`)}function Xe(t,...e){if(!(t instanceof Uint8Array))throw new Error("Expected Uint8Array");if(e.length>0&&!e.includes(t.length))throw new Error(`Expected Uint8Array of length ${e}, not of length=${t.length}`)}const tn={number:Qe,bool:function(t){if("boolean"!=typeof t)throw new Error(`Expected boolean, not ${t}`)},bytes:Xe,hash:function(t){if("function"!=typeof t||"function"!=typeof t.create)throw new Error("Hash should be wrapped by utils.wrapConstructor");Qe(t.outputLen),Qe(t.blockLen)},exists:function(t,e=!0){if(t.destroyed)throw new Error("Hash instance has been destroyed");if(e&&t.finished)throw new Error("Hash#digest() has already been called")},output:function(t,e){Xe(t);const n=e.outputLen;if(t.lengthr-o&&(this.process(n,0),o=0);for(let t=o;t>s&o),a=Number(n&o),l=r?4:0,c=r?0:4;t.setUint32(e+l,i,r),t.setUint32(e+c,a,r)}(n,r-8,BigInt(8*this.length),s),this.process(n,0);const i=Ke(t),a=this.outputLen;if(a%4)throw new Error("_sha2: outputLen should be aligned to 32bit");const l=a/4,c=this.get();if(l>c.length)throw new Error("_sha2: outputLen bigger than state");for(let t=0;tt&e^~t&n,rn=(t,e,n)=>t&e^t&n^e&n,sn=new Uint32Array([1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,3329325298]),on=new Uint32Array([1779033703,3144134277,1013904242,2773480762,1359893119,2600822924,528734635,1541459225]),an=new Uint32Array(64);class ln extends en{constructor(){super(64,32,8,!1),this.A=0|on[0],this.B=0|on[1],this.C=0|on[2],this.D=0|on[3],this.E=0|on[4],this.F=0|on[5],this.G=0|on[6],this.H=0|on[7]}get(){const{A:t,B:e,C:n,D:r,E:s,F:o,G:i,H:a}=this;return[t,e,n,r,s,o,i,a]}set(t,e,n,r,s,o,i,a){this.A=0|t,this.B=0|e,this.C=0|n,this.D=0|r,this.E=0|s,this.F=0|o,this.G=0|i,this.H=0|a}process(t,e){for(let n=0;n<16;n++,e+=4)an[n]=t.getUint32(e,!1);for(let t=16;t<64;t++){const e=an[t-15],n=an[t-2],r=Me(e,7)^Me(e,18)^e>>>3,s=Me(n,17)^Me(n,19)^n>>>10;an[t]=s+an[t-7]+r+an[t-16]|0}let{A:n,B:r,C:s,D:o,E:i,F:a,G:l,H:c}=this;for(let t=0;t<64;t++){const e=c+(Me(i,6)^Me(i,11)^Me(i,25))+nn(i,a,l)+sn[t]+an[t]|0,u=(Me(n,2)^Me(n,13)^Me(n,22))+rn(n,r,s)|0;c=l,l=a,a=i,i=o+e|0,o=s,s=r,r=n,n=e+u|0}n=n+this.A|0,r=r+this.B|0,s=s+this.C|0,o=o+this.D|0,i=i+this.E|0,a=a+this.F|0,l=l+this.G|0,c=c+this.H|0,this.set(n,r,s,o,i,a,l,c)}roundClean(){an.fill(0)}destroy(){this.set(0,0,0,0,0,0,0,0),this.buffer.fill(0)}}class cn extends ln{constructor(){super(),this.A=-1056596264,this.B=914150663,this.C=812702999,this.D=-150054599,this.E=-4191439,this.F=1750603025,this.G=1694076839,this.H=-1090891868,this.outputLen=28}}const un=Je(()=>new ln);Je(()=>new cn);var dn=Symbol("verified");function fn(t){if(!(t instanceof Object))return!1;if("number"!=typeof t.kind)return!1;if("string"!=typeof t.content)return!1;if("number"!=typeof t.created_at)return!1;if("string"!=typeof t.pubkey)return!1;if(!t.pubkey.match(/^[a-f0-9]{64}$/))return!1;if(!Array.isArray(t.tags))return!1;for(let e=0;en=>t(e(n)),n=Array.from(t).reverse().reduce((t,n)=>t?e(t,n.encode):n.encode,void 0),r=t.reduce((t,n)=>t?e(t,n.decode):n.decode,void 0);return{encode:n,decode:r}}function En(t){return{encode:e=>{if(!Array.isArray(e)||e.length&&"number"!=typeof e[0])throw new Error("alphabet.encode input should be an array of numbers");return e.map(e=>{if(mn(e),e<0||e>=t.length)throw new Error(`Digit index outside alphabet: ${e} (alphabet: ${t.length})`);return t[e]})},decode:e=>{if(!Array.isArray(e)||e.length&&"string"!=typeof e[0])throw new Error("alphabet.decode input should be array of strings");return e.map(e=>{if("string"!=typeof e)throw new Error(`alphabet.decode: not string element=${e}`);const n=t.indexOf(e);if(-1===n)throw new Error(`Unknown letter: "${e}". Allowed: ${t}`);return n})}}}function xn(t=""){if("string"!=typeof t)throw new Error("join separator should be string");return{encode:e=>{if(!Array.isArray(e)||e.length&&"string"!=typeof e[0])throw new Error("join.encode input should be array of strings");for(let t of e)if("string"!=typeof t)throw new Error(`join.encode: non-string input=${t}`);return e.join(t)},decode:e=>{if("string"!=typeof e)throw new Error("join.decode input should be string");return e.split(t)}}}function kn(t,e="="){if(mn(t),"string"!=typeof e)throw new Error("padding chr should be string");return{encode(n){if(!Array.isArray(n)||n.length&&"string"!=typeof n[0])throw new Error("padding.encode input should be array of strings");for(let t of n)if("string"!=typeof t)throw new Error(`padding.encode: non-string input=${t}`);for(;n.length*t%8;)n.push(e);return n},decode(n){if(!Array.isArray(n)||n.length&&"string"!=typeof n[0])throw new Error("padding.encode input should be array of strings");for(let t of n)if("string"!=typeof t)throw new Error(`padding.decode: non-string input=${t}`);let r=n.length;if(r*t%8)throw new Error("Invalid padding: string should have whole number of bytes");for(;r>0&&n[r-1]===e;r--)if(!((r-1)*t%8))throw new Error("Invalid padding: string has too much padding");return n.slice(0,r)}}}function $n(t){if("function"!=typeof t)throw new Error("normalize fn should be function");return{encode:t=>t,decode:e=>t(e)}}function An(t,e,n){if(e<2)throw new Error(`convertRadix: wrong from=${e}, base cannot be less than 2`);if(n<2)throw new Error(`convertRadix: wrong to=${n}, base cannot be less than 2`);if(!Array.isArray(t))throw new Error("convertRadix: data should be array");if(!t.length)return[];let r=0;const s=[],o=Array.from(t);for(o.forEach(t=>{if(mn(t),t<0||t>=e)throw new Error(`Wrong integer: ${t}`)});;){let t=0,i=!0;for(let s=r;se?Bn(e,t%e):t,In=(t,e)=>t+(e-Bn(t,e));function _n(t,e,n,r){if(!Array.isArray(t))throw new Error("convertRadix2: data should be array");if(e<=0||e>32)throw new Error(`convertRadix2: wrong from=${e}`);if(n<=0||n>32)throw new Error(`convertRadix2: wrong to=${n}`);if(In(e,n)>32)throw new Error(`convertRadix2: carry overflow from=${e} to=${n} carryBits=${In(e,n)}`);let s=0,o=0;const i=2**n-1,a=[];for(const r of t){if(mn(r),r>=2**e)throw new Error(`convertRadix2: invalid data word=${r} from=${e}`);if(s=s<32)throw new Error(`convertRadix2: carry overflow pos=${o} from=${e}`);for(o+=e;o>=n;o-=n)a.push((s>>o-n&i)>>>0);s&=2**o-1}if(s=s<=e)throw new Error("Excess padding");if(!r&&s)throw new Error(`Non-zero padding: ${s}`);return r&&o>0&&a.push(s>>>0),a}function Ln(t,e=!1){if(mn(t),t<=0||t>32)throw new Error("radix2: bits should be in (0..32]");if(In(8,t)>32||In(t,8)>32)throw new Error("radix2: carry overflow");return{encode:n=>{if(!(n instanceof Uint8Array))throw new Error("radix2.encode input should be Uint8Array");return _n(Array.from(n),8,t,!e)},decode:n=>{if(!Array.isArray(n)||n.length&&"number"!=typeof n[0])throw new Error("radix2.decode input should be array of strings");return Uint8Array.from(_n(n,t,8,e))}}}function Sn(t){if("function"!=typeof t)throw new Error("unsafeWrapper fn should be function");return function(...e){try{return t.apply(null,e)}catch(t){}}}const Cn=vn(Ln(4),En("0123456789ABCDEF"),xn("")),Un=vn(Ln(5),En("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"),kn(5),xn(""));vn(Ln(5),En("0123456789ABCDEFGHIJKLMNOPQRSTUV"),kn(5),xn("")),vn(Ln(5),En("0123456789ABCDEFGHJKMNPQRSTVWXYZ"),xn(""),$n(t=>t.toUpperCase().replace(/O/g,"0").replace(/[IL]/g,"1")));const Tn=vn(Ln(6),En("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"),kn(6),xn("")),Nn=vn(Ln(6),En("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"),kn(6),xn("")),On=t=>{return vn((mn(e=58),{encode:t=>{if(!(t instanceof Uint8Array))throw new Error("radix.encode input should be Uint8Array");return An(Array.from(t),256,e)},decode:t=>{if(!Array.isArray(t)||t.length&&"number"!=typeof t[0])throw new Error("radix.decode input should be array of strings");return Uint8Array.from(An(t,e,256))}}),En(t),xn(""));var e},Rn=On("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz");On("123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ"),On("rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz");const Pn=[0,2,3,5,6,7,9,10,11],Dn={encode(t){let e="";for(let n=0;n>25;let n=(33554431&t)<<5;for(let t=0;t>t&1)&&(n^=Hn[t]);return n}function Vn(t,e,n=1){const r=t.length;let s=1;for(let e=0;e126)throw new Error(`Invalid prefix (${t})`);s=Fn(s)^n>>5}s=Fn(s);for(let e=0;en)throw new TypeError(`Wrong string length: ${t.length} (${t}). Expected (8..${n})`);const r=t.toLowerCase();if(t!==r&&t!==t.toUpperCase())throw new Error("String must be lowercase or uppercase");const s=(t=r).lastIndexOf("1");if(0===s||-1===s)throw new Error('Letter "1" must be present between prefix and data only');const o=t.slice(0,s),i=t.slice(s+1);if(i.length<6)throw new Error("Data must be at least 6 characters long");const a=qn.decode(i).slice(0,-6),l=Vn(o,a,e);if(!i.endsWith(l))throw new Error(`Invalid checksum in ${t}: expected "${l}"`);return{prefix:o,words:a}}return{encode:function(t,n,r=90){if("string"!=typeof t)throw new Error("bech32.encode prefix should be string, not "+typeof t);if(!Array.isArray(n)||n.length&&"number"!=typeof n[0])throw new Error("bech32.encode words should be array of numbers, not "+typeof n);const s=t.length+7+n.length;if(!1!==r&&s>r)throw new TypeError(`Length ${s} exceeds limit ${r}`);return`${t=t.toLowerCase()}1${qn.encode(n)}${Vn(t,n,e)}`},decode:i,decodeToBytes:function(t){const{prefix:e,words:n}=i(t,!1);return{prefix:e,words:n,bytes:r(n)}},decodeUnsafe:Sn(i),fromWords:r,fromWordsUnsafe:o,toWords:s}}const zn=jn("bech32");jn("bech32m");const Kn={utf8:{encode:t=>(new TextDecoder).decode(t),decode:t=>(new TextEncoder).encode(t)},hex:vn(Ln(4),En("0123456789abcdef"),xn(""),$n(t=>{if("string"!=typeof t||t.length%2)throw new TypeError(`hex.decode: expected string, got ${typeof t} with length ${t.length}`);return t.toLowerCase()})),base16:Cn,base32:Un,base64:Tn,base64url:Nn,base58:Rn,base58xmr:Dn};Object.keys(Kn).join(", ");var Mn=new TextDecoder("utf-8");new TextEncoder;function Zn(t){let e={},n=t;for(;n.length>0;){let t=n[0],r=n[1],s=n.slice(2,2+r);if(n=n.slice(2+r),s.lengthMn.decode(t)):[]}}}case"nevent":{let t=Zn(r);if(!t[0]?.[0])throw new Error("missing TLV 0 for nevent");if(32!==t[0][0].length)throw new Error("TLV 0 should be 32 bytes");if(t[2]&&32!==t[2][0].length)throw new Error("TLV 2 should be 32 bytes");if(t[3]&&4!==t[3][0].length)throw new Error("TLV 3 should be 4 bytes");return{type:"nevent",data:{id:Ge(t[0][0]),relays:t[1]?t[1].map(t=>Mn.decode(t)):[],author:t[2]?.[0]?Ge(t[2][0]):void 0,kind:t[3]?.[0]?parseInt(Ge(t[3][0]),16):void 0}}}case"naddr":{let t=Zn(r);if(!t[0]?.[0])throw new Error("missing TLV 0 for naddr");if(!t[2]?.[0])throw new Error("missing TLV 2 for naddr");if(32!==t[2][0].length)throw new Error("TLV 2 should be 32 bytes");if(!t[3]?.[0])throw new Error("missing TLV 3 for naddr");if(4!==t[3][0].length)throw new Error("TLV 3 should be 4 bytes");return{type:"naddr",data:{identifier:Mn.decode(t[0][0]),pubkey:Ge(t[2][0]),kind:parseInt(Ge(t[3][0]),16),relays:t[1]?t[1].map(t=>Mn.decode(t)):[]}}}case"nsec":return{type:e,data:r};case"npub":case"note":return{type:e,data:Ge(r)};default:throw new Error(`unknown prefix ${e}`)}}(t)}catch{throw new Error("Invalid nsec format")}if("nsec"!==e.type)throw new Error("Please enter an nsec (private key)");const s=e.data,o=wn(s),i={getPublicKey:async()=>o,signEvent:async t=>bn(t,s)};n(6,u="Successfully logged in!"),r("login",{method:"nsec",pubkey:o,privateKey:t,signer:i}),setTimeout(h,500)}catch(t){n(5,c=t.message)}finally{n(4,l=!1)}}return t.$$set=t=>{"showModal"in t&&n(0,s=t.showModal),"isDarkTheme"in t&&n(1,o=t.isDarkTheme)},[s,o,i,a,l,c,u,f,h,g,async function(){n(5,c=""),n(6,u="");try{const t=yn(),e=Wn("nsec",t),r=Gn(wn(t));d=e,n(7,f=r),n(3,a=e),n(6,u="New key generated!")}catch(t){n(5,c="Failed to generate key: "+t.message)}},async function(){n(4,l=!0),n(5,c=""),n(6,u="");try{if(!window.nostr)throw new Error("No Nostr extension found. Please install nos2x or Alby.");const t=await window.nostr.getPublicKey();t&&(n(6,u="Successfully logged in with extension!"),r("login",{method:"extension",pubkey:t,signer:window.nostr}),setTimeout(h,500))}catch(t){n(5,c=t.message)}finally{n(4,l=!1)}},p,function(t){"Escape"===t.key&&h(),"Enter"===t.key&&"nsec"===i&&p()},function(e){_.call(this,t,e)},function(e){_.call(this,t,e)},()=>g("extension"),()=>g("nsec"),function(){a=this.value,n(3,a)},t=>"Escape"===t.key&&h()]}class or extends Q{constructor(t){super(),J(this,t,sr,rr,o,{showModal:0,isDarkTheme:1})}}const ir=[];function ar(e,n=t){let r;const s=new Set;function i(t){if(o(e,t)&&(e=t,r)){const t=!ir.length;for(const t of s)t[1](),ir.push(t,e);if(t){for(let t=0;t{s.delete(c),0===s.size&&r&&(r(),r=null)}}}}const lr=ar(!1),cr=ar(""),ur=ar(null),dr=ar(""),fr=ar(null),hr=ar(null),gr=ar(null),pr=ar(!1),yr=ar("");async function wr(t,e={},n,r){const s=`${window.location.origin}${t}`,o=e.method||"GET",i=await async function(t,e,n,r){if(!t||!e)return null;try{const e={kind:27235,created_at:Math.floor(Date.now()/1e3),tags:[["u",r],["method",n.toUpperCase()]],content:""},s=await t.signEvent(e),o=JSON.stringify(s);return btoa(o).replace(/\+/g,"-").replace(/\//g,"_")}catch(t){return console.error("createNIP98Auth error:",t),null}}(n,r,o,s),a={...e.headers};return i&&(a.Authorization=`Nostr ${i}`),fetch(s,{...e,headers:a})}function br(t){let e,n,r,s,o,i=t[0].pid+"";return{c(){e=h("div"),n=h("span"),n.textContent="PID:",r=p(),s=h("span"),o=g(i),b(n,"class","label svelte-xh5u5u"),b(s,"class","value svelte-xh5u5u"),b(e,"class","detail-row svelte-xh5u5u")},m(t,i){u(t,e,i),c(e,n),c(e,r),c(e,s),c(s,o)},p(t,e){1&e&&i!==(i=t[0].pid+"")&&m(o,i)},d(t){t&&d(e)}}}function mr(t){let e,n,r,s,o,i=t[0].restarts+"";return{c(){e=h("div"),n=h("span"),n.textContent="Restarts:",r=p(),s=h("span"),o=g(i),b(n,"class","label svelte-xh5u5u"),b(s,"class","value warning svelte-xh5u5u"),b(e,"class","detail-row svelte-xh5u5u")},m(t,i){u(t,e,i),c(e,n),c(e,r),c(e,s),c(s,o)},p(t,e){1&e&&i!==(i=t[0].restarts+"")&&m(o,i)},d(t){t&&d(e)}}}function vr(e){let n,r,s,o,i,a,l,f,y,w,v,x,k,$,A,B,I,_,L,S,C,U,T=xr(e[0].status)+"",N=e[0].name+"",O=e[0].status+"",R=e[0].binary+"",P=e[0].pid>0&&br(e),D=e[0].restarts>0&&mr(e);return{c(){n=h("div"),r=h("div"),s=h("span"),o=g(T),i=p(),a=h("span"),l=g(N),f=p(),y=h("div"),w=h("div"),v=h("span"),v.textContent="Status:",x=p(),k=h("span"),$=g(O),A=p(),P&&P.c(),B=p(),I=h("div"),_=h("span"),_.textContent="Binary:",L=p(),S=h("span"),C=g(R),U=p(),D&&D.c(),b(s,"class","status-indicator svelte-xh5u5u"),E(s,"color",Er(e[0].status)),b(a,"class","process-name svelte-xh5u5u"),b(r,"class","process-header svelte-xh5u5u"),b(v,"class","label svelte-xh5u5u"),b(k,"class","value svelte-xh5u5u"),E(k,"color",Er(e[0].status)),b(w,"class","detail-row svelte-xh5u5u"),b(_,"class","label svelte-xh5u5u"),b(S,"class","value binary svelte-xh5u5u"),b(I,"class","detail-row svelte-xh5u5u"),b(y,"class","process-details svelte-xh5u5u"),b(n,"class","process-card svelte-xh5u5u")},m(t,e){u(t,n,e),c(n,r),c(r,s),c(s,o),c(r,i),c(r,a),c(a,l),c(n,f),c(n,y),c(y,w),c(w,v),c(w,x),c(w,k),c(k,$),c(y,A),P&&P.m(y,null),c(y,B),c(y,I),c(I,_),c(I,L),c(I,S),c(S,C),c(y,U),D&&D.m(y,null)},p(t,[e]){1&e&&T!==(T=xr(t[0].status)+"")&&m(o,T),1&e&&E(s,"color",Er(t[0].status)),1&e&&N!==(N=t[0].name+"")&&m(l,N),1&e&&O!==(O=t[0].status+"")&&m($,O),1&e&&E(k,"color",Er(t[0].status)),t[0].pid>0?P?P.p(t,e):(P=br(t),P.c(),P.m(y,B)):P&&(P.d(1),P=null),1&e&&R!==(R=t[0].binary+"")&&m(C,R),t[0].restarts>0?D?D.p(t,e):(D=mr(t),D.c(),D.m(y,null)):D&&(D.d(1),D=null)},i:t,o:t,d(t){t&&d(n),P&&P.d(),D&&D.d()}}}function Er(t){switch(t){case"running":return"var(--success)";case"stopped":default:return"var(--muted-color)";case"crashed":return"var(--error)"}}function xr(t){switch(t){case"running":return"●";case"stopped":return"○";case"crashed":return"✗";default:return"?"}}function kr(t,e,n){let{process:r}=e;return t.$$set=t=>{"process"in t&&n(0,r=t.process)},[r]}class $r extends Q{constructor(t){super(),J(this,t,kr,vr,o,{process:0})}}function Ar(t,e,n){const r=t.slice();return r[8]=e[n],r}function Br(t){let e,n;return{c(){e=h("div"),n=g(t[1]),b(e,"class","error-banner svelte-17dya06")},m(t,r){u(t,e,r),c(e,n)},p(t,e){2&e&&m(n,t[1])},d(t){t&&d(e)}}}function Ir(e){let n;return{c(){n=h("div"),n.textContent="Loading status...",b(n,"class","loading svelte-17dya06")},m(t,e){u(t,n,e)},p:t,i:t,o:t,d(t){t&&d(n)}}}function _r(t){let e,n,r,s,o,i,a,l,y,w,v,E,x,k,$,A,B,I,_,L,S,C,U,T=t[2].version+"",N=t[2].uptime+"",O=(t[2].processes?.length||0)+"",R=M(t[2].processes||[]),P=[];for(let e=0;eK(P[t],1,1,()=>{P[t]=null});return{c(){e=h("div"),n=h("div"),r=h("span"),r.textContent="Version",s=p(),o=h("span"),i=g(T),a=p(),l=h("div"),y=h("span"),y.textContent="Uptime",w=p(),v=h("span"),E=g(N),x=p(),k=h("div"),$=h("span"),$.textContent="Processes",A=p(),B=h("span"),I=g(O),_=p(),L=h("h3"),L.textContent="Managed Processes",S=p(),C=h("div");for(let t=0;t{L[r]=null}),j()),~x?(k=L[x],k?k.p(t,n):(k=L[x]=_[x](t),k.c()),z(k,1),k.m(e,null)):k=null)},i(t){$||(z(k),$=!0)},o(t){K(k),$=!1},d(t){t&&d(e),I&&I.d(),~x&&L[x].d(),A=!1,r(B)}}}function Cr(t,e,n){let r,s,o,l,c,u;var d;async function f(){try{a(fr,c=await async function(t,e){const n=await wr("/api/status",{},t,e);if(!n.ok)throw new Error(`Failed to fetch status: ${n.statusText}`);return n.json()}(l,o),c),a(yr,s="",s)}catch(t){a(yr,s=t.message,s)}}return i(t,pr,t=>n(0,r=t)),i(t,yr,t=>n(1,s=t)),i(t,cr,t=>n(6,o=t)),i(t,ur,t=>n(7,l=t)),i(t,fr,t=>n(2,c=t)),B(async()=>{await f(),u=setInterval(f,5e3)}),d=()=>{u&&clearInterval(u)},A().$$.on_destroy.push(d),[r,s,c,f,async function(){if(confirm("Are you sure you want to restart all services?")){a(pr,r=!0,r);try{await async function(t,e){const n=await wr("/api/restart",{method:"POST"},t,e);if(!n.ok)throw new Error(`Restart failed: ${n.statusText}`);return n.json()}(l,o),setTimeout(f,2e3)}catch(t){a(yr,s=t.message,s)}finally{a(pr,r=!1,r)}}}]}class Ur extends Q{constructor(t){super(),J(this,t,Cr,Sr,o,{})}}function Tr(t,e,n){const r=t.slice();return r[6]=e[n],r}function Nr(t){let e,n;return{c(){e=h("div"),n=g(t[1]),b(e,"class","error-banner svelte-1kruta9")},m(t,r){u(t,e,r),c(e,n)},p(t,e){2&e&&m(n,t[1])},d(t){t&&d(e)}}}function Or(e){let n;return{c(){n=h("div"),n.textContent="Loading configuration...",b(n,"class","loading svelte-1kruta9")},m(t,e){u(t,n,e)},p:t,d(t){t&&d(n)}}}function Rr(t){let e,n,r,s,o,i,a,l,y,w,v,E,k,$,A,B,I,_,L,S,C,U,T,N,O,R,P,D,q,H,F,V,j,z,K,Z,G,W,Y,J,Q,X,tt,et,nt,rt,st,ot,it,at,lt,ct,ut,dt,ft,ht,gt,pt,yt,wt,bt,mt,vt,Et,xt,kt,$t,At,Bt,It,_t,Lt,St,Ct,Ut,Tt,Nt,Ot,Rt,Pt,Dt,qt,Ht,Ft,Vt,jt,zt,Kt,Mt,Zt,Gt,Wt,Yt,Jt,Qt,Xt,te,ee,ne,re,se,oe,ie,ae,le,ce,ue,de,fe,he,ge,pe,ye,we,be,me,ve,Ee=t[2].db_backend+"",xe=t[2].db_binary+"",ke=t[2].db_listen+"",$e=t[2].data_dir+"",Ae=t[2].acl_enabled?"Yes":"No",Be=t[2].acl_mode+"",Ie=t[2].acl_binary+"",_e=t[2].acl_listen+"",Le=t[2].relay_binary+"",Se=t[2].log_level+"",Ce=t[2].distributed_sync_enabled?"Enabled":"Disabled",Ue=t[2].cluster_sync_enabled?"Enabled":"Disabled",Te=t[2].relay_group_enabled?"Enabled":"Disabled",Ne=t[2].negentropy_enabled?"Enabled":"Disabled",Oe=t[2].bin_dir+"",Re=M(t[2].admin_owners||[]),Pe=[];for(let e=0;eConfiguration is loaded from environment variables. To change settings, update the environment and restart the launcher.
',b(r,"class","svelte-1kruta9"),b(a,"class","label svelte-1kruta9"),b(y,"class","value svelte-1kruta9"),b(i,"class","config-item svelte-1kruta9"),b(k,"class","label svelte-1kruta9"),b(A,"class","value mono svelte-1kruta9"),b(E,"class","config-item svelte-1kruta9"),b(L,"class","label svelte-1kruta9"),b(C,"class","value mono svelte-1kruta9"),b(_,"class","config-item svelte-1kruta9"),b(O,"class","label svelte-1kruta9"),b(P,"class","value mono svelte-1kruta9"),b(N,"class","config-item svelte-1kruta9"),b(o,"class","config-grid svelte-1kruta9"),b(n,"class","config-section svelte-1kruta9"),b(F,"class","svelte-1kruta9"),b(K,"class","label svelte-1kruta9"),b(G,"class","value bool svelte-1kruta9"),x(G,"enabled",t[2].acl_enabled),b(z,"class","config-item svelte-1kruta9"),b(Q,"class","label svelte-1kruta9"),b(tt,"class","value svelte-1kruta9"),b(J,"class","config-item svelte-1kruta9"),b(st,"class","label svelte-1kruta9"),b(it,"class","value mono svelte-1kruta9"),b(rt,"class","config-item svelte-1kruta9"),b(ut,"class","label svelte-1kruta9"),b(ft,"class","value mono svelte-1kruta9"),b(ct,"class","config-item svelte-1kruta9"),b(j,"class","config-grid svelte-1kruta9"),b(H,"class","config-section svelte-1kruta9"),b(yt,"class","svelte-1kruta9"),b(vt,"class","label svelte-1kruta9"),b(xt,"class","value mono svelte-1kruta9"),b(mt,"class","config-item svelte-1kruta9"),b(Bt,"class","label svelte-1kruta9"),b(_t,"class","value svelte-1kruta9"),b(At,"class","config-item svelte-1kruta9"),b(bt,"class","config-grid svelte-1kruta9"),b(pt,"class","config-section svelte-1kruta9"),b(Ut,"class","svelte-1kruta9"),b(Rt,"class","label svelte-1kruta9"),b(Dt,"class","value bool svelte-1kruta9"),x(Dt,"enabled",t[2].distributed_sync_enabled),b(Ot,"class","config-item svelte-1kruta9"),b(Vt,"class","label svelte-1kruta9"),b(zt,"class","value bool svelte-1kruta9"),x(zt,"enabled",t[2].cluster_sync_enabled),b(Ft,"class","config-item svelte-1kruta9"),b(Gt,"class","label svelte-1kruta9"),b(Yt,"class","value bool svelte-1kruta9"),x(Yt,"enabled",t[2].relay_group_enabled),b(Zt,"class","config-item svelte-1kruta9"),b(te,"class","label svelte-1kruta9"),b(ne,"class","value bool svelte-1kruta9"),x(ne,"enabled",t[2].negentropy_enabled),b(Xt,"class","config-item svelte-1kruta9"),b(Nt,"class","config-grid svelte-1kruta9"),b(Ct,"class","config-section svelte-1kruta9"),b(ie,"class","svelte-1kruta9"),b(ue,"class","label svelte-1kruta9"),b(fe,"class","value mono svelte-1kruta9"),b(ce,"class","config-item svelte-1kruta9"),b(ye,"class","label svelte-1kruta9"),b(be,"class","owners-list svelte-1kruta9"),b(pe,"class","config-item full-width svelte-1kruta9"),b(le,"class","config-grid svelte-1kruta9"),b(oe,"class","config-section svelte-1kruta9"),b(e,"class","config-sections svelte-1kruta9"),b(ve,"class","config-note svelte-1kruta9")},m(t,d){u(t,e,d),c(e,n),c(n,r),c(n,s),c(n,o),c(o,i),c(i,a),c(i,l),c(i,y),c(y,w),c(o,v),c(o,E),c(E,k),c(E,$),c(E,A),c(A,B),c(o,I),c(o,_),c(_,L),c(_,S),c(_,C),c(C,U),c(o,T),c(o,N),c(N,O),c(N,R),c(N,P),c(P,D),c(e,q),c(e,H),c(H,F),c(H,V),c(H,j),c(j,z),c(z,K),c(z,Z),c(z,G),c(G,W),c(j,Y),c(j,J),c(J,Q),c(J,X),c(J,tt),c(tt,et),c(j,nt),c(j,rt),c(rt,st),c(rt,ot),c(rt,it),c(it,at),c(j,lt),c(j,ct),c(ct,ut),c(ct,dt),c(ct,ft),c(ft,ht),c(e,gt),c(e,pt),c(pt,yt),c(pt,wt),c(pt,bt),c(bt,mt),c(mt,vt),c(mt,Et),c(mt,xt),c(xt,kt),c(bt,$t),c(bt,At),c(At,Bt),c(At,It),c(At,_t),c(_t,Lt),c(e,St),c(e,Ct),c(Ct,Ut),c(Ct,Tt),c(Ct,Nt),c(Nt,Ot),c(Ot,Rt),c(Ot,Pt),c(Ot,Dt),c(Dt,qt),c(Nt,Ht),c(Nt,Ft),c(Ft,Vt),c(Ft,jt),c(Ft,zt),c(zt,Kt),c(Nt,Mt),c(Nt,Zt),c(Zt,Gt),c(Zt,Wt),c(Zt,Yt),c(Yt,Jt),c(Nt,Qt),c(Nt,Xt),c(Xt,te),c(Xt,ee),c(Xt,ne),c(ne,re),c(e,se),c(e,oe),c(oe,ie),c(oe,ae),c(oe,le),c(le,ce),c(ce,ue),c(ce,de),c(ce,fe),c(fe,he),c(le,ge),c(le,pe),c(pe,ye),c(pe,we),c(pe,be);for(let t=0;tn(0,r=t)),i(t,yr,t=>n(1,s=t)),i(t,cr,t=>n(4,o=t)),i(t,ur,t=>n(5,l=t)),i(t,hr,t=>n(2,c=t)),B(async()=>{await u()}),[r,s,c,u]}class Fr extends Q{constructor(t){super(),J(this,t,Hr,qr,o,{})}}function Vr(t,e,n){const r=t.slice();return r[15]=e[n],r}function jr(t,e,n){const r=t.slice();return r[18]=e[n],r[19]=e,r[20]=n,r}function zr(t){let e,n;return{c(){e=h("div"),n=g(t[4]),b(e,"class","error-banner svelte-1ig49gt")},m(t,r){u(t,e,r),c(e,n)},p(t,e){16&e&&m(n,t[4])},d(t){t&&d(e)}}}function Kr(t){let e,n,r,s=t[2].message+"",o=t[2].downloaded_files?.length&&Mr(t);return{c(){e=h("div"),n=g(s),r=p(),o&&o.c(),b(e,"class","success-banner svelte-1ig49gt")},m(t,s){u(t,e,s),c(e,n),c(e,r),o&&o.m(e,null)},p(t,r){4&r&&s!==(s=t[2].message+"")&&m(n,s),t[2].downloaded_files?.length?o?o.p(t,r):(o=Mr(t),o.c(),o.m(e,null)):o&&(o.d(1),o=null)},d(t){t&&d(e),o&&o.d()}}}function Mr(t){let e,n,r,s=t[2].downloaded_files.join(", ")+"";return{c(){e=h("br"),n=g("Downloaded: "),r=g(s)},m(t,s){u(t,e,s),u(t,n,s),u(t,r,s)},p(t,e){4&e&&s!==(s=t[2].downloaded_files.join(", ")+"")&&m(r,s)},d(t){t&&(d(e),d(n),d(r))}}}function Zr(t){let e,n,r,s,o,i,a,l,f=t[18]+"";function w(){t[10].call(o,t[18])}return{c(){e=h("div"),n=h("span"),r=g(f),s=p(),o=h("input"),i=p(),b(n,"class","binary-name svelte-1ig49gt"),b(o,"type","text"),b(o,"placeholder","https://..."),o.disabled=t[3],b(o,"class","svelte-1ig49gt"),b(e,"class","url-input svelte-1ig49gt")},m(d,f){u(d,e,f),c(e,n),c(n,r),c(e,s),c(e,o),v(o,t[1][t[18]]),c(e,i),a||(l=y(o,"input",w),a=!0)},p(e,n){t=e,2&n&&f!==(f=t[18]+"")&&m(r,f),8&n&&(o.disabled=t[3]),2&n&&o.value!==t[1][t[18]]&&v(o,t[1][t[18]])},d(t){t&&d(e),a=!1,l()}}}function Gr(t){let e,n,r,s,o,i,a,l=M(t[5].available_versions),g=[];for(let e=0;eVersion | Installed | Binaries | Status | ',i=p(),a=h("tbody");for(let t=0;tUpdate Binaries',o=p(),tt&&tt.c(),i=p(),et&&et.c(),a=p(),l=h("div"),w=h("h3"),w.textContent="Current Version",E=p(),x=h("div"),k=h("span"),$=g(Q),A=p(),B=h("button"),I=g("Rollback"),L=p(),S=h("div"),C=h("h3"),C.textContent="Install New Version",U=p(),T=h("div"),N=h("label"),N.textContent="Version",O=p(),R=h("input"),P=p(),D=h("div"),q=h("div"),H=h("label"),H.textContent="Binary URLs",F=p(),V=h("button"),j=g("Fill from Release"),z=p();for(let t=0;tn(4,r=t)),i(t,cr,t=>n(11,s=t)),i(t,ur,t=>n(12,o=t)),i(t,pr,t=>n(13,l=t)),i(t,gr,t=>n(5,c=t));let u="",d={orly:"","orly-db-badger":"","orly-acl-follows":"","orly-launcher":""},f=null,h=!1;async function g(){a(pr,l=!0,l);try{a(gr,c=await async function(t,e){const n=await wr("/api/binaries",{},t,e);if(!n.ok)throw new Error(`Failed to fetch binaries: ${n.statusText}`);return n.json()}(o,s),c),a(yr,r="",r)}catch(t){a(yr,r=t.message,r)}finally{a(pr,l=!1,l)}}return B(async()=>{await g()}),[u,d,f,h,r,c,async function(){const t={};for(const[e,n]of Object.entries(d))n.trim()&&(t[e]=n.trim());if(u.trim())if(0!==Object.keys(t).length){n(3,h=!0),n(2,f=null),a(yr,r="",r);try{n(2,f=await async function(t,e,n,r){const s=await wr("/api/update",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({version:n,urls:r})},t,e);if(!s.ok){const t=await s.json();throw new Error(t.message||`Update failed: ${s.statusText}`)}return s.json()}(o,s,u.trim(),t)),await g()}catch(t){a(yr,r=t.message,r)}finally{n(3,h=!1)}}else a(yr,r="At least one binary URL is required",r);else a(yr,r="Version is required",r)},async function(){if(confirm("Are you sure you want to rollback to the previous version?")){n(3,h=!0),a(yr,r="",r);try{const t=await async function(t,e){const n=await wr("/api/rollback",{method:"POST"},t,e);if(!n.ok){const t=await n.json();throw new Error(t.message||`Rollback failed: ${n.statusText}`)}return n.json()}(o,s);n(2,f={success:!0,message:`Rolled back from ${t.previous_version} to ${t.current_version}. Restart services to apply.`}),await g()}catch(t){a(yr,r=t.message,r)}finally{n(3,h=!1)}}},function(){const t=prompt("Enter release base URL (e.g., https://git.mleku.dev/mleku/next.orly.dev/releases/download/v0.55.11):");if(!t)return;const e=t.replace(/\/$/,""),r=prompt("Enter architecture (amd64 or arm64):","amd64");if(!r)return;const s=u.trim()||t.split("/").pop();n(1,d.orly=`${e}/orly-${s.replace("v","")}-linux-${r}`,d),n(1,d["orly-db-badger"]=`${e}/orly-db-badger-${s.replace("v","")}-linux-${r}`,d),n(1,d["orly-acl-follows"]=`${e}/orly-acl-follows-${s.replace("v","")}-linux-${r}`,d),n(1,d["orly-launcher"]=`${e}/orly-launcher-${s.replace("v","")}-linux-${r}`,d),u.trim()||n(0,u=s)},function(){u=this.value,n(0,u)},function(t){d[t]=this.value,n(1,d)}]}class Xr extends Q{constructor(t){super(),J(this,t,Qr,Jr,o,{})}}function ts(e){let n,r;return n=new Xr({}),{c(){Z(n.$$.fragment)},m(t,e){G(n,t,e),r=!0},p:t,i(t){r||(z(n.$$.fragment,t),r=!0)},o(t){K(n.$$.fragment,t),r=!1},d(t){W(n,t)}}}function es(e){let n,r;return n=new Fr({}),{c(){Z(n.$$.fragment)},m(t,e){G(n,t,e),r=!0},p:t,i(t){r||(z(n.$$.fragment,t),r=!0)},o(t){K(n.$$.fragment,t),r=!1},d(t){W(n,t)}}}function ns(e){let n,r;return n=new Ur({}),{c(){Z(n.$$.fragment)},m(t,e){G(n,t,e),r=!0},p:t,i(t){r||(z(n.$$.fragment,t),r=!0)},o(t){K(n.$$.fragment,t),r=!1},d(t){W(n,t)}}}function rs(e){let n,r,s,o,i,a,l,f;return{c(){n=h("div"),r=h("h2"),r.textContent="ORLY Launcher Admin",s=p(),o=h("p"),o.textContent="Please login to manage the relay services.",i=p(),a=h("button"),a.textContent="Login with Nostr",b(r,"class","svelte-4k9oqz"),b(o,"class","svelte-4k9oqz"),b(a,"class","login-btn svelte-4k9oqz"),b(n,"class","login-prompt svelte-4k9oqz")},m(t,d){u(t,n,d),c(n,r),c(n,s),c(n,o),c(n,i),c(n,a),l||(f=y(a,"click",e[10]),l=!0)},p:t,i:t,o:t,d(t){t&&d(n),l=!1,f()}}}function ss(t){let e,n,r,s,o,i,a,l,f,g;n=new st({props:{currentPage:t[0],isLoggedIn:t[4],userPubkey:t[3]}}),n.$on("navigate",t[8]),n.$on("login",t[9]),n.$on("logout",t[6]);const y=[rs,ns,es,ts],w=[];function m(t,e){return t[4]?"dashboard"===t[0]?1:"config"===t[0]?2:"update"===t[0]?3:-1:0}function v(e){t[11](e)}~(o=m(t))&&(i=w[o]=y[o](t));let E={isDarkTheme:t[2]};return void 0!==t[1]&&(E.showModal=t[1]),l=new or({props:E}),S.push(()=>function(t,e,n){const r=t.$$.props[e];void 0!==r&&(t.$$.bound[r]=n,n(t.$$.ctx[r]))}(l,"showModal",v)),l.$on("login",t[5]),l.$on("close",t[12]),{c(){e=h("main"),Z(n.$$.fragment),r=p(),s=h("div"),i&&i.c(),a=p(),Z(l.$$.fragment),b(s,"class","content svelte-4k9oqz"),b(e,"class","svelte-4k9oqz"),x(e,"dark-theme",t[2])},m(t,i){u(t,e,i),G(n,e,null),c(e,r),c(e,s),~o&&w[o].m(s,null),c(e,a),G(l,e,null),g=!0},p(t,[r]){const a={};1&r&&(a.currentPage=t[0]),16&r&&(a.isLoggedIn=t[4]),8&r&&(a.userPubkey=t[3]),n.$set(a);let c=o;o=m(t),o===c?~o&&w[o].p(t,r):(i&&(V(),K(w[c],1,1,()=>{w[c]=null}),j()),~o?(i=w[o],i?i.p(t,r):(i=w[o]=y[o](t),i.c()),z(i,1),i.m(s,null)):i=null);const u={};var d;4&r&&(u.isDarkTheme=t[2]),!f&&2&r&&(f=!0,u.showModal=t[1],d=()=>f=!1,U.push(d)),l.$set(u),(!g||4&r)&&x(e,"dark-theme",t[2])},i(t){g||(z(n.$$.fragment,t),z(i),z(l.$$.fragment,t),g=!0)},o(t){K(n.$$.fragment,t),K(i),K(l.$$.fragment,t),g=!1},d(t){t&&d(e),W(n),~o&&w[o].d(),W(l)}}}function os(t,e,n){let r,s,o,l;i(t,dr,t=>n(13,r=t)),i(t,ur,t=>n(14,s=t)),i(t,cr,t=>n(3,o=t)),i(t,lr,t=>n(4,l=t));let c="dashboard",u=!1,d=!1;function f(t){n(0,c=t)}B(()=>{const t=localStorage.getItem("launcher_auth_method"),e=localStorage.getItem("launcher_pubkey");"extension"===t&&e&&window.nostr&&window.nostr.getPublicKey().then(t=>{t===e&&(a(lr,l=!0,l),a(cr,o=t,o),a(ur,s=window.nostr,s),a(dr,r="extension",r))}).catch(()=>{localStorage.removeItem("launcher_auth_method"),localStorage.removeItem("launcher_pubkey")}),n(2,d=window.matchMedia("(prefers-color-scheme: dark)").matches)});return[c,u,d,o,l,function(t){const{method:e,pubkey:i,signer:c,privateKey:d}=t.detail;a(lr,l=!0,l),a(cr,o=i,o),a(ur,s=c,s),a(dr,r=e,r),localStorage.setItem("launcher_auth_method",e),localStorage.setItem("launcher_pubkey",i),n(1,u=!1)},function(){a(lr,l=!1,l),a(cr,o="",o),a(ur,s=null,s),a(dr,r="",r),localStorage.removeItem("launcher_auth_method"),localStorage.removeItem("launcher_pubkey"),localStorage.removeItem("launcher_privkey_encrypted")},f,t=>f(t.detail),()=>n(1,u=!0),()=>n(1,u=!0),function(t){u=t,n(1,u)},()=>n(1,u=!1)]}return new class extends Q{constructor(t){super(),J(this,t,os,ss,o,{})}}({target:document.body})}();
diff --git a/cmd/orly-launcher/web/dist/index.html b/cmd/orly-launcher/web/dist/index.html
new file mode 100644
index 0000000..6644a0c
--- /dev/null
+++ b/cmd/orly-launcher/web/dist/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ ORLY Launcher Admin
+
+
+
+
+
+
diff --git a/cmd/orly-launcher/web/package.json b/cmd/orly-launcher/web/package.json
new file mode 100644
index 0000000..c4a5f8e
--- /dev/null
+++ b/cmd/orly-launcher/web/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "orly-launcher-admin",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "rollup -c -w",
+ "build": "rollup -c"
+ },
+ "devDependencies": {
+ "@rollup/plugin-commonjs": "^25.0.7",
+ "@rollup/plugin-node-resolve": "^15.2.3",
+ "@rollup/plugin-terser": "^0.4.4",
+ "rollup": "^4.9.0",
+ "rollup-plugin-css-only": "^4.5.2",
+ "rollup-plugin-livereload": "^2.0.5",
+ "rollup-plugin-svelte": "^7.1.6",
+ "svelte": "^4.2.8"
+ },
+ "dependencies": {
+ "nostr-tools": "^2.1.4",
+ "argon2-browser": "^1.18.0"
+ }
+}
diff --git a/cmd/orly-launcher/web/public/index.html b/cmd/orly-launcher/web/public/index.html
new file mode 100644
index 0000000..6644a0c
--- /dev/null
+++ b/cmd/orly-launcher/web/public/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ ORLY Launcher Admin
+
+
+
+
+
+
diff --git a/cmd/orly-launcher/web/rollup.config.js b/cmd/orly-launcher/web/rollup.config.js
new file mode 100644
index 0000000..edbff83
--- /dev/null
+++ b/cmd/orly-launcher/web/rollup.config.js
@@ -0,0 +1,35 @@
+import svelte from 'rollup-plugin-svelte';
+import commonjs from '@rollup/plugin-commonjs';
+import resolve from '@rollup/plugin-node-resolve';
+import terser from '@rollup/plugin-terser';
+import css from 'rollup-plugin-css-only';
+
+const production = !process.env.ROLLUP_WATCH;
+
+export default {
+ input: 'src/main.js',
+ output: {
+ sourcemap: !production,
+ format: 'iife',
+ name: 'app',
+ file: 'dist/bundle.js'
+ },
+ plugins: [
+ svelte({
+ compilerOptions: {
+ dev: !production
+ }
+ }),
+ css({ output: 'bundle.css' }),
+ resolve({
+ browser: true,
+ dedupe: ['svelte'],
+ exportConditions: ['svelte']
+ }),
+ commonjs(),
+ production && terser()
+ ],
+ watch: {
+ clearScreen: false
+ }
+};
diff --git a/cmd/orly-launcher/web/src/App.svelte b/cmd/orly-launcher/web/src/App.svelte
new file mode 100644
index 0000000..adf7ba2
--- /dev/null
+++ b/cmd/orly-launcher/web/src/App.svelte
@@ -0,0 +1,185 @@
+
+
+
+
+
+
diff --git a/cmd/orly-launcher/web/src/LoginModal.svelte b/cmd/orly-launcher/web/src/LoginModal.svelte
new file mode 100644
index 0000000..8ee5044
--- /dev/null
+++ b/cmd/orly-launcher/web/src/LoginModal.svelte
@@ -0,0 +1,454 @@
+
+
+
+
+{#if showModal}
+ e.key === 'Escape' && closeModal()}
+ role="button"
+ tabindex="0"
+ >
+
+
+
+
+
+
+
+
+
+
+ {#if activeTab === 'extension'}
+
+
Login using a NIP-07 browser extension like nos2x or Alby.
+
+
+ {:else}
+
+
Enter your nsec or generate a new key pair.
+
+
+
+ {#if generatedNpub}
+
+
+ {generatedNpub}
+
+ {/if}
+
+
+
+
+
+ {/if}
+
+ {#if errorMessage}
+
{errorMessage}
+ {/if}
+
+ {#if successMessage}
+
{successMessage}
+ {/if}
+
+
+
+
+{/if}
+
+
diff --git a/cmd/orly-launcher/web/src/api.js b/cmd/orly-launcher/web/src/api.js
new file mode 100644
index 0000000..014dee0
--- /dev/null
+++ b/cmd/orly-launcher/web/src/api.js
@@ -0,0 +1,151 @@
+/**
+ * API helper functions for ORLY Launcher admin endpoints
+ */
+
+/**
+ * Get the API base URL (same as current page)
+ */
+export function getApiBase() {
+ return window.location.origin;
+}
+
+/**
+ * Create NIP-98 authentication header
+ * @param {object} signer - The signer instance
+ * @param {string} pubkey - User's pubkey
+ * @param {string} method - HTTP method
+ * @param {string} url - Request URL
+ * @returns {Promise} Base64 encoded auth header or null
+ */
+export async function createNIP98Auth(signer, pubkey, method, url) {
+ if (!signer || !pubkey) {
+ return null;
+ }
+
+ try {
+ // Create unsigned auth event
+ const authEvent = {
+ kind: 27235,
+ created_at: Math.floor(Date.now() / 1000),
+ tags: [
+ ["u", url],
+ ["method", method.toUpperCase()],
+ ],
+ content: "",
+ };
+
+ // Sign using the signer
+ const signedEvent = await signer.signEvent(authEvent);
+
+ // Use URL-safe base64 encoding
+ const json = JSON.stringify(signedEvent);
+ const base64 = btoa(json).replace(/\+/g, '-').replace(/\//g, '_');
+ return base64;
+ } catch (error) {
+ console.error("createNIP98Auth error:", error);
+ return null;
+ }
+}
+
+/**
+ * Make an authenticated API request
+ * @param {string} path - API path
+ * @param {object} options - Fetch options
+ * @param {object} signer - Signer instance
+ * @param {string} pubkey - User's pubkey
+ * @returns {Promise}
+ */
+async function authFetch(path, options = {}, signer, pubkey) {
+ const url = `${getApiBase()}${path}`;
+ const method = options.method || 'GET';
+ const authHeader = await createNIP98Auth(signer, pubkey, method, url);
+
+ const headers = {
+ ...options.headers,
+ };
+
+ if (authHeader) {
+ headers['Authorization'] = `Nostr ${authHeader}`;
+ }
+
+ return fetch(url, { ...options, headers });
+}
+
+/**
+ * Fetch launcher status
+ */
+export async function fetchStatus(signer, pubkey) {
+ const response = await authFetch('/api/status', {}, signer, pubkey);
+ if (!response.ok) {
+ throw new Error(`Failed to fetch status: ${response.statusText}`);
+ }
+ return response.json();
+}
+
+/**
+ * Fetch launcher configuration
+ */
+export async function fetchConfig(signer, pubkey) {
+ const response = await authFetch('/api/config', {}, signer, pubkey);
+ if (!response.ok) {
+ throw new Error(`Failed to fetch config: ${response.statusText}`);
+ }
+ return response.json();
+}
+
+/**
+ * Fetch available binaries
+ */
+export async function fetchBinaries(signer, pubkey) {
+ const response = await authFetch('/api/binaries', {}, signer, pubkey);
+ if (!response.ok) {
+ throw new Error(`Failed to fetch binaries: ${response.statusText}`);
+ }
+ return response.json();
+}
+
+/**
+ * Update binaries from URLs
+ */
+export async function updateBinaries(signer, pubkey, version, urls) {
+ const response = await authFetch('/api/update', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ version, urls }),
+ }, signer, pubkey);
+
+ if (!response.ok) {
+ const data = await response.json();
+ throw new Error(data.message || `Update failed: ${response.statusText}`);
+ }
+ return response.json();
+}
+
+/**
+ * Restart all services
+ */
+export async function restartServices(signer, pubkey) {
+ const response = await authFetch('/api/restart', {
+ method: 'POST',
+ }, signer, pubkey);
+
+ if (!response.ok) {
+ throw new Error(`Restart failed: ${response.statusText}`);
+ }
+ return response.json();
+}
+
+/**
+ * Rollback to previous version
+ */
+export async function rollbackVersion(signer, pubkey) {
+ const response = await authFetch('/api/rollback', {
+ method: 'POST',
+ }, signer, pubkey);
+
+ if (!response.ok) {
+ const data = await response.json();
+ throw new Error(data.message || `Rollback failed: ${response.statusText}`);
+ }
+ return response.json();
+}
diff --git a/cmd/orly-launcher/web/src/components/Header.svelte b/cmd/orly-launcher/web/src/components/Header.svelte
new file mode 100644
index 0000000..129d5c3
--- /dev/null
+++ b/cmd/orly-launcher/web/src/components/Header.svelte
@@ -0,0 +1,149 @@
+
+
+
+
+
diff --git a/cmd/orly-launcher/web/src/components/ProcessCard.svelte b/cmd/orly-launcher/web/src/components/ProcessCard.svelte
new file mode 100644
index 0000000..0df5fe0
--- /dev/null
+++ b/cmd/orly-launcher/web/src/components/ProcessCard.svelte
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
+ Status:
+
+ {process.status}
+
+
+
+ {#if process.pid > 0}
+
+ PID:
+ {process.pid}
+
+ {/if}
+
+
+ Binary:
+ {process.binary}
+
+
+ {#if process.restarts > 0}
+
+ Restarts:
+ {process.restarts}
+
+ {/if}
+
+
+
+
diff --git a/cmd/orly-launcher/web/src/main.js b/cmd/orly-launcher/web/src/main.js
new file mode 100644
index 0000000..8cdf8d6
--- /dev/null
+++ b/cmd/orly-launcher/web/src/main.js
@@ -0,0 +1,7 @@
+import App from './App.svelte';
+
+const app = new App({
+ target: document.body,
+});
+
+export default app;
diff --git a/cmd/orly-launcher/web/src/pages/Config.svelte b/cmd/orly-launcher/web/src/pages/Config.svelte
new file mode 100644
index 0000000..bc1de84
--- /dev/null
+++ b/cmd/orly-launcher/web/src/pages/Config.svelte
@@ -0,0 +1,300 @@
+
+
+
+
+
+ {#if $error}
+
{$error}
+ {/if}
+
+ {#if $configData}
+
+
+ Database
+
+
+ Backend
+ {$configData.db_backend}
+
+
+ Binary
+ {$configData.db_binary}
+
+
+ Listen Address
+ {$configData.db_listen}
+
+
+ Data Directory
+ {$configData.data_dir}
+
+
+
+
+
+ ACL
+
+
+ Enabled
+
+ {$configData.acl_enabled ? 'Yes' : 'No'}
+
+
+
+ Mode
+ {$configData.acl_mode}
+
+
+ Binary
+ {$configData.acl_binary}
+
+
+ Listen Address
+ {$configData.acl_listen}
+
+
+
+
+
+ Relay
+
+
+ Binary
+ {$configData.relay_binary}
+
+
+ Log Level
+ {$configData.log_level}
+
+
+
+
+
+ Sync Services
+
+
+ Distributed Sync
+
+ {$configData.distributed_sync_enabled ? 'Enabled' : 'Disabled'}
+
+
+
+ Cluster Sync
+
+ {$configData.cluster_sync_enabled ? 'Enabled' : 'Disabled'}
+
+
+
+ Relay Group
+
+ {$configData.relay_group_enabled ? 'Enabled' : 'Disabled'}
+
+
+
+ Negentropy
+
+ {$configData.negentropy_enabled ? 'Enabled' : 'Disabled'}
+
+
+
+
+
+
+ Admin
+
+
+ Binary Directory
+ {$configData.bin_dir}
+
+
+
Admin Owners
+
+ {#each $configData.admin_owners || [] as owner}
+ {owner}
+ {:else}
+ No owners configured
+ {/each}
+
+
+
+
+
+
+
+
Configuration is loaded from environment variables. To change settings, update the environment and restart the launcher.
+
+ {:else if !$error}
+
Loading configuration...
+ {/if}
+
+
+
diff --git a/cmd/orly-launcher/web/src/pages/Dashboard.svelte b/cmd/orly-launcher/web/src/pages/Dashboard.svelte
new file mode 100644
index 0000000..b9b21bb
--- /dev/null
+++ b/cmd/orly-launcher/web/src/pages/Dashboard.svelte
@@ -0,0 +1,202 @@
+
+
+
+
+
+ {#if $error}
+
{$error}
+ {/if}
+
+ {#if $statusData}
+
+
+ Version
+ {$statusData.version}
+
+
+ Uptime
+ {$statusData.uptime}
+
+
+ Processes
+ {$statusData.processes?.length || 0}
+
+
+
+
Managed Processes
+
+ {#each $statusData.processes || [] as process}
+
+ {/each}
+
+ {:else if !$error}
+
Loading status...
+ {/if}
+
+
+
diff --git a/cmd/orly-launcher/web/src/pages/Update.svelte b/cmd/orly-launcher/web/src/pages/Update.svelte
new file mode 100644
index 0000000..467cba1
--- /dev/null
+++ b/cmd/orly-launcher/web/src/pages/Update.svelte
@@ -0,0 +1,430 @@
+
+
+
+
+
+ {#if $error}
+
{$error}
+ {/if}
+
+ {#if updateResult?.success}
+
+ {updateResult.message}
+ {#if updateResult.downloaded_files?.length}
+
Downloaded: {updateResult.downloaded_files.join(', ')}
+ {/if}
+
+ {/if}
+
+
+
Current Version
+
+ {$binariesData?.current_version || 'unknown'}
+
+
+
+
+
+
+ {#if $binariesData?.available_versions?.length}
+
+
Installed Versions
+
+
+
+ | Version |
+ Installed |
+ Binaries |
+ Status |
+
+
+
+ {#each $binariesData.available_versions as ver}
+
+ | {ver.version} |
+ {new Date(ver.installed_at).toLocaleString()} |
+ {ver.binaries?.length || 0} files |
+
+ {#if ver.is_current}
+ Current
+ {/if}
+ |
+
+ {/each}
+
+
+
+ {/if}
+
+
+
diff --git a/cmd/orly-launcher/web/src/stores.js b/cmd/orly-launcher/web/src/stores.js
new file mode 100644
index 0000000..31260da
--- /dev/null
+++ b/cmd/orly-launcher/web/src/stores.js
@@ -0,0 +1,16 @@
+import { writable } from 'svelte/store';
+
+// Authentication state
+export const isLoggedIn = writable(false);
+export const userPubkey = writable('');
+export const userSigner = writable(null);
+export const authMethod = writable(''); // 'extension' or 'nsec'
+
+// Status data
+export const statusData = writable(null);
+export const configData = writable(null);
+export const binariesData = writable(null);
+
+// Loading states
+export const isLoading = writable(false);
+export const error = writable('');
diff --git a/pkg/version/version b/pkg/version/version
index 9974c2c..fefa9a2 100644
--- a/pkg/version/version
+++ b/pkg/version/version
@@ -1 +1 @@
-v0.55.10
+v0.55.11