Browse Source

Add launcher service control and fix release URLs (v0.56.2)

- Add ServicesEnabled config to run admin UI without starting services
- Add start/stop services API endpoints and dashboard controls
- Add IsRunning() method to supervisor for service state tracking
- Fix release download URLs to use git.nostrdev.com instead of git.mleku.dev
- Change Makefile to use go install (except main relay uses go build for name)
- Add orly-certs DNS-01 wildcard certificate manager
- Remove libsecp256k1.so from repo (runtime dependency only)

Files modified:
- cmd/orly-launcher/config.go: Add ServicesEnabled option
- cmd/orly-launcher/main.go: Skip services when disabled, update help
- cmd/orly-launcher/server.go: Add start/stop endpoints, fix tags API URL
- cmd/orly-launcher/supervisor.go: Add IsRunning(), allow restart after stop
- cmd/orly-launcher/web/src/api.js: Add startServices/stopServices functions
- cmd/orly-launcher/web/src/pages/Dashboard.svelte: Add start/stop buttons
- cmd/orly-launcher/web/src/pages/Update.svelte: Fix release base URL
- cmd/orly-certs/: New DNS-01 certificate manager
- Makefile: Use go install, keep go build for main relay
- pkg/version/version: Bump to v0.56.2

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
main
woikos 4 months ago
parent
commit
c1713f42ef
No known key found for this signature in database
  1. 2
      .gitea/workflows/go.yml
  2. 121
      Makefile
  3. 74
      cmd/orly-certs/config.go
  4. 112
      cmd/orly-certs/main.go
  5. 304
      cmd/orly-certs/manager.go
  6. 55
      cmd/orly-certs/providers.go
  7. 23
      cmd/orly-launcher/config.go
  8. 28
      cmd/orly-launcher/main.go
  9. 232
      cmd/orly-launcher/server.go
  10. 263
      cmd/orly-launcher/supervisor.go
  11. 4
      cmd/orly-launcher/web/dist/bundle.css
  12. 16
      cmd/orly-launcher/web/dist/bundle.js
  13. 59
      cmd/orly-launcher/web/src/api.js
  14. 91
      cmd/orly-launcher/web/src/pages/Dashboard.svelte
  15. 661
      cmd/orly-launcher/web/src/pages/Update.svelte
  16. 190
      go.mod
  17. 1406
      go.sum
  18. BIN
      libsecp256k1.so
  19. 2
      pkg/version/version

2
.gitea/workflows/go.yml

@ -127,6 +127,7 @@ jobs:
"orly-acl-curation:./cmd/orly-acl-curation" "orly-acl-curation:./cmd/orly-acl-curation"
"orly-launcher:./cmd/orly-launcher" "orly-launcher:./cmd/orly-launcher"
"orly-sync-negentropy:./cmd/orly-sync-negentropy" "orly-sync-negentropy:./cmd/orly-sync-negentropy"
"orly-certs:./cmd/orly-certs"
) )
# Build for AMD64 # Build for AMD64
@ -218,6 +219,7 @@ jobs:
- **orly-acl-curation** - Curation ACL server - **orly-acl-curation** - Curation ACL server
- **orly-launcher** - Process supervisor with admin UI - **orly-launcher** - Process supervisor with admin UI
- **orly-sync-negentropy** - Negentropy sync service - **orly-sync-negentropy** - Negentropy sync service
- **orly-certs** - DNS-01 wildcard certificate manager
- **libsecp256k1** - Required shared library - **libsecp256k1** - Required shared library
### Architectures ### Architectures

121
Makefile

@ -1,10 +1,11 @@
# ORLY Nostr Relay Build System # ORLY Nostr Relay Build System
.PHONY: all orly orly-db orly-acl orly-launcher proto clean test deploy web install help .PHONY: all orly orly-db orly-acl orly-launcher proto clean test deploy web help
.PHONY: orly-db-badger orly-db-neo4j orly-acl-follows orly-acl-managed orly-acl-curation .PHONY: orly-db-badger orly-db-neo4j orly-acl-follows orly-acl-managed orly-acl-curation
.PHONY: all-split arm64-split install-split .PHONY: all-split arm64-split
.PHONY: orly-sync-negentropy all-sync arm64-sync .PHONY: orly-sync-negentropy all-sync arm64-sync
.PHONY: orly-certs
.PHONY: quick-deploy quick-deploy-restart deploy-both deploy-both-restart deploy-new list-releases rollback .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: orly-unified arm64-unified
.PHONY: launcher-web orly-launcher-no-web .PHONY: launcher-web orly-launcher-no-web
# Build flags # Build flags
@ -13,86 +14,71 @@ GOOS ?= $(shell go env GOOS)
GOARCH ?= $(shell go env GOARCH) GOARCH ?= $(shell go env GOARCH)
BUILD_FLAGS = CGO_ENABLED=$(CGO_ENABLED) GOOS=$(GOOS) GOARCH=$(GOARCH) BUILD_FLAGS = CGO_ENABLED=$(CGO_ENABLED) GOOS=$(GOOS) GOARCH=$(GOARCH)
# Binaries # GOBIN for installed binaries (defaults to ~/go/bin)
BIN_DIR = . GOBIN ?= $(shell go env GOBIN)
ORLY = $(BIN_DIR)/orly ifeq ($(GOBIN),)
ORLY_LAUNCHER = $(BIN_DIR)/orly-launcher GOBIN = $(shell go env GOPATH)/bin
endif
# Legacy monolithic binaries (for backwards compatibility)
ORLY_DB = $(BIN_DIR)/orly-db
ORLY_ACL = $(BIN_DIR)/orly-acl
# Split database backends
ORLY_DB_BADGER = $(BIN_DIR)/orly-db-badger
ORLY_DB_NEO4J = $(BIN_DIR)/orly-db-neo4j
# Split ACL modes
ORLY_ACL_FOLLOWS = $(BIN_DIR)/orly-acl-follows
ORLY_ACL_MANAGED = $(BIN_DIR)/orly-acl-managed
ORLY_ACL_CURATION = $(BIN_DIR)/orly-acl-curation
# Unified binary (new architecture)
ORLY_UNIFIED = $(BIN_DIR)/orly-unified
# === Default Targets (Legacy) === # === Default Targets (Legacy) ===
# Default target: build everything (legacy monolithic) # Default target: build everything (legacy monolithic)
all: orly orly-db orly-launcher all: orly orly-db orly-launcher
@echo "All binaries built successfully" @echo "All binaries installed to $(GOBIN)"
# Build everything including ACL (when proto exists) # Build everything including ACL (when proto exists)
all-acl: proto orly orly-db orly-acl orly-launcher all-acl: proto orly orly-db orly-acl orly-launcher
@echo "All binaries (including ACL) built successfully" @echo "All binaries (including ACL) installed to $(GOBIN)"
# === Split Binaries (New) === # === Split Binaries (New) ===
# Build split mode: orly + orly-db-badger + orly-acl-follows + launcher # Build split mode: orly + orly-db-badger + orly-acl-follows + launcher
all-split: proto orly orly-db-badger orly-acl-follows orly-launcher all-split: proto orly orly-db-badger orly-acl-follows orly-launcher
@echo "Split mode binaries built successfully" @echo "Split mode binaries installed to $(GOBIN)"
# Build all split binaries (all backends and all ACL modes) # Build all split binaries (all backends and all ACL modes)
all-backends: proto orly orly-db-badger orly-db-neo4j orly-acl-follows orly-acl-managed orly-acl-curation orly-launcher all-backends: proto orly orly-db-badger orly-db-neo4j orly-acl-follows orly-acl-managed orly-acl-curation orly-launcher
@echo "All backend and ACL mode binaries built successfully" @echo "All backend and ACL mode binaries installed to $(GOBIN)"
# Main relay binary # Main relay binary (uses go build to control output name since module is next.orly.dev)
orly: orly:
$(BUILD_FLAGS) go build -o $(ORLY) . $(BUILD_FLAGS) go build -o $(GOBIN)/orly .
# === Database Backends === # === Database Backends ===
# Legacy monolithic database server # Legacy monolithic database server
orly-db: orly-db:
$(BUILD_FLAGS) go build -o $(ORLY_DB) ./cmd/orly-db $(BUILD_FLAGS) go install ./cmd/orly-db
# Badger database server # Badger database server
orly-db-badger: orly-db-badger:
$(BUILD_FLAGS) go build -o $(ORLY_DB_BADGER) ./cmd/orly-db-badger $(BUILD_FLAGS) go install ./cmd/orly-db-badger
# Neo4j database server # Neo4j database server
orly-db-neo4j: orly-db-neo4j:
$(BUILD_FLAGS) go build -o $(ORLY_DB_NEO4J) ./cmd/orly-db-neo4j $(BUILD_FLAGS) go install ./cmd/orly-db-neo4j
# === ACL Modes === # === ACL Modes ===
# Legacy monolithic ACL server (requires proto generation first) # Legacy monolithic ACL server (requires proto generation first)
orly-acl: orly-acl:
$(BUILD_FLAGS) go build -o $(ORLY_ACL) ./cmd/orly-acl $(BUILD_FLAGS) go install ./cmd/orly-acl
# Follows ACL server (whitelist based on admin follows) # Follows ACL server (whitelist based on admin follows)
orly-acl-follows: orly-acl-follows:
$(BUILD_FLAGS) go build -o $(ORLY_ACL_FOLLOWS) ./cmd/orly-acl-follows $(BUILD_FLAGS) go install ./cmd/orly-acl-follows
# Managed ACL server (NIP-86 fine-grained control) # Managed ACL server (NIP-86 fine-grained control)
orly-acl-managed: orly-acl-managed:
$(BUILD_FLAGS) go build -o $(ORLY_ACL_MANAGED) ./cmd/orly-acl-managed $(BUILD_FLAGS) go install ./cmd/orly-acl-managed
# Curation ACL server (rate-limited trust tiers) # Curation ACL server (rate-limited trust tiers)
orly-acl-curation: orly-acl-curation:
$(BUILD_FLAGS) go build -o $(ORLY_ACL_CURATION) ./cmd/orly-acl-curation $(BUILD_FLAGS) go install ./cmd/orly-acl-curation
# Process supervisor/launcher # Process supervisor/launcher
orly-launcher: launcher-web orly-launcher: launcher-web
$(BUILD_FLAGS) go build -o $(ORLY_LAUNCHER) ./cmd/orly-launcher $(BUILD_FLAGS) go install ./cmd/orly-launcher
# Build launcher admin web UI # Build launcher admin web UI
launcher-web: launcher-web:
@ -100,23 +86,18 @@ launcher-web:
# Build launcher without web UI (for development) # Build launcher without web UI (for development)
orly-launcher-no-web: orly-launcher-no-web:
$(BUILD_FLAGS) go build -o $(ORLY_LAUNCHER) ./cmd/orly-launcher $(BUILD_FLAGS) go install ./cmd/orly-launcher
# === Unified Binary (New Architecture) === # === Unified Binary (New Architecture) ===
# Unified binary with Badger driver (minimal, for most deployments) # Unified binary with Badger driver (minimal, for most deployments)
orly-unified: orly-unified:
$(BUILD_FLAGS) go build -o $(ORLY_UNIFIED) ./cmd/orly $(BUILD_FLAGS) go install ./cmd/orly
# Build unified binary for ARM64 # Build unified binary for ARM64
arm64-unified: arm64-unified:
$(MAKE) GOOS=linux GOARCH=arm64 orly-unified $(MAKE) GOOS=linux GOARCH=arm64 orly-unified
# Install unified binary
install-unified: orly-unified
mkdir -p ~/.local/bin
cp $(ORLY_UNIFIED) ~/.local/bin/
# Generate protobuf code # Generate protobuf code
proto: proto:
cd proto && buf generate cd proto && buf generate
@ -145,15 +126,10 @@ arm64-backends:
web: web:
./scripts/update-embedded-web.sh ./scripts/update-embedded-web.sh
# Clean build artifacts # Clean build artifacts (note: binaries are in GOBIN)
clean: clean:
rm -f $(ORLY) $(ORLY_DB) $(ORLY_ACL) $(ORLY_LAUNCHER) go clean -i ./...
rm -f $(ORLY_DB_BADGER) $(ORLY_DB_NEO4J) @echo "Cleaned. Note: installed binaries are in $(GOBIN)"
rm -f $(ORLY_ACL_FOLLOWS) $(ORLY_ACL_MANAGED) $(ORLY_ACL_CURATION)
rm -f $(ORLY_SYNC_NEGENTROPY)
rm -f $(ORLY_UNIFIED)
rm -f orly-db-arm64 orly-acl-arm64 orly-launcher-arm64 next.orly.dev
rm -rf build-arm64
# Run tests # Run tests
test: test:
@ -161,47 +137,28 @@ test:
# Deploy to relay.orly.dev (builds on remote) - legacy # Deploy to relay.orly.dev (builds on remote) - legacy
deploy: deploy:
ssh relay.orly.dev 'cd ~/src/next.orly.dev && git pull origin main && make all && sudo /usr/sbin/setcap cap_net_bind_service=+ep ~/.local/bin/next.orly.dev && sudo systemctl restart orly' ssh relay.orly.dev 'cd ~/src/next.orly.dev && git pull origin main && make all && sudo /usr/sbin/setcap cap_net_bind_service=+ep ~/go/bin/next.orly.dev && sudo systemctl restart orly'
# Deploy with ACL server - legacy # Deploy with ACL server - legacy
deploy-acl: deploy-acl:
ssh relay.orly.dev 'cd ~/src/next.orly.dev && git pull origin main && make all-acl && sudo /usr/sbin/setcap cap_net_bind_service=+ep ~/.local/bin/next.orly.dev && sudo systemctl restart orly' ssh relay.orly.dev 'cd ~/src/next.orly.dev && git pull origin main && make all-acl && sudo /usr/sbin/setcap cap_net_bind_service=+ep ~/go/bin/next.orly.dev && sudo systemctl restart orly'
# Deploy split mode (recommended) # Deploy split mode (recommended)
deploy-split: deploy-split:
ssh relay.orly.dev 'cd ~/src/next.orly.dev && git pull origin main && make all-split && sudo /usr/sbin/setcap cap_net_bind_service=+ep ~/.local/bin/next.orly.dev && sudo systemctl restart orly' ssh relay.orly.dev 'cd ~/src/next.orly.dev && git pull origin main && make all-split && sudo /usr/sbin/setcap cap_net_bind_service=+ep ~/go/bin/next.orly.dev && sudo systemctl restart orly'
# Install all binaries locally - legacy
install: all
mkdir -p ~/.local/bin
cp $(ORLY) $(ORLY_DB) $(ORLY_LAUNCHER) ~/.local/bin/
# Install including ACL - legacy
install-acl: all-acl
mkdir -p ~/.local/bin
cp $(ORLY) $(ORLY_DB) $(ORLY_ACL) $(ORLY_LAUNCHER) ~/.local/bin/
# Install split mode binaries
install-split: all-split
mkdir -p ~/.local/bin
cp $(ORLY) $(ORLY_DB_BADGER) $(ORLY_ACL_FOLLOWS) $(ORLY_LAUNCHER) ~/.local/bin/
# Install all backends and modes
install-backends: all-backends
mkdir -p ~/.local/bin
cp $(ORLY) $(ORLY_DB_BADGER) $(ORLY_DB_NEO4J) $(ORLY_ACL_FOLLOWS) $(ORLY_ACL_MANAGED) $(ORLY_ACL_CURATION) $(ORLY_LAUNCHER) ~/.local/bin/
# === Symlink-Based Deployment === # === Symlink-Based Deployment ===
# Sync binaries # Certificate service
ORLY_SYNC_NEGENTROPY = $(BIN_DIR)/orly-sync-negentropy orly-certs:
$(BUILD_FLAGS) go install ./cmd/orly-certs
orly-sync-negentropy: orly-sync-negentropy:
$(BUILD_FLAGS) go build -o $(ORLY_SYNC_NEGENTROPY) ./cmd/orly-sync-negentropy $(BUILD_FLAGS) go install ./cmd/orly-sync-negentropy
# Build all sync services # Build all sync services
all-sync: proto orly orly-db orly-acl orly-launcher orly-sync-negentropy all-sync: proto orly orly-db orly-acl orly-launcher orly-sync-negentropy
@echo "All sync service binaries built successfully" @echo "All sync service binaries installed to $(GOBIN)"
# ARM64 for sync services # ARM64 for sync services
arm64-sync: arm64-sync:
@ -239,12 +196,13 @@ rollback:
help: help:
@echo "ORLY Build Targets:" @echo "ORLY Build Targets:"
@echo "" @echo ""
@echo " Binaries are installed to GOBIN ($(GOBIN))"
@echo ""
@echo " Split Mode (Recommended):" @echo " Split Mode (Recommended):"
@echo " all-split - Build orly + orly-db-badger + orly-acl-follows + launcher" @echo " all-split - Build orly + orly-db-badger + orly-acl-follows + launcher"
@echo " all-backends - Build all database backends and ACL modes" @echo " all-backends - Build all database backends and ACL modes"
@echo " arm64-split - Cross-compile split mode for ARM64" @echo " arm64-split - Cross-compile split mode for ARM64"
@echo " deploy-split - Deploy split mode to relay.orly.dev" @echo " deploy-split - Deploy split mode to relay.orly.dev"
@echo " install-split - Install split mode to ~/.local/bin/"
@echo "" @echo ""
@echo " Database Backends:" @echo " Database Backends:"
@echo " orly-db-badger - Build Badger database server" @echo " orly-db-badger - Build Badger database server"
@ -269,7 +227,7 @@ help:
@echo " web - Rebuild main embedded web UI" @echo " web - Rebuild main embedded web UI"
@echo " launcher-web - Rebuild launcher admin web UI" @echo " launcher-web - Rebuild launcher admin web UI"
@echo " test - Run test suite" @echo " test - Run test suite"
@echo " clean - Remove build artifacts" @echo " clean - Clean build artifacts"
@echo " help - Show this help" @echo " help - Show this help"
@echo "" @echo ""
@echo " Sync Services:" @echo " Sync Services:"
@ -280,7 +238,6 @@ help:
@echo " Unified Binary (New Architecture):" @echo " Unified Binary (New Architecture):"
@echo " orly-unified - Build unified binary with subcommands" @echo " orly-unified - Build unified binary with subcommands"
@echo " arm64-unified - Cross-compile unified binary for ARM64" @echo " arm64-unified - Cross-compile unified binary for ARM64"
@echo " install-unified - Install unified binary to ~/.local/bin/"
@echo "" @echo ""
@echo " Unified Binary Usage:" @echo " Unified Binary Usage:"
@echo " orly-unified db --driver=badger - Run Badger database server" @echo " orly-unified db --driver=badger - Run Badger database server"

74
cmd/orly-certs/config.go

@ -0,0 +1,74 @@
package main
import (
"os"
"time"
"go-simpler.org/env"
"lol.mleku.dev/chk"
"lol.mleku.dev/log"
)
// Config holds the configuration for the certificate manager.
type Config struct {
// Domain is the wildcard domain to obtain a certificate for (e.g., "*.myapp.com")
Domain string `env:"ORLY_CERTS_DOMAIN" required:"true" usage:"wildcard domain (e.g., *.myapp.com)"`
// Email is the email address for the Let's Encrypt account
Email string `env:"ORLY_CERTS_EMAIL" required:"true" usage:"email for Let's Encrypt account"`
// DNSProvider is the name of the DNS provider (cloudflare, route53, hetzner, etc.)
DNSProvider string `env:"ORLY_CERTS_DNS_PROVIDER" required:"true" usage:"DNS provider name (cloudflare, route53, hetzner, etc.)"`
// OutputDir is the directory where certificates will be stored
OutputDir string `env:"ORLY_CERTS_OUTPUT_DIR" default:"/var/cache/orly-certs" usage:"certificate output directory"`
// RenewDays is the number of days before expiry to trigger renewal
RenewDays int `env:"ORLY_CERTS_RENEW_DAYS" default:"30" usage:"renew certificate when expiring within N days"`
// CheckInterval is how often to check for renewal
CheckInterval time.Duration `env:"ORLY_CERTS_CHECK_INTERVAL" default:"12h" usage:"how often to check for renewal"`
// ACMEServer is the ACME server URL (empty for production Let's Encrypt)
ACMEServer string `env:"ORLY_CERTS_ACME_SERVER" default:"" usage:"ACME server URL (empty for production)"`
// LogLevel is the log level
LogLevel string `env:"ORLY_CERTS_LOG_LEVEL" default:"info" usage:"log level (trace, debug, info, warn, error)"`
// AccountKeyPath is the path to store the ACME account private key
AccountKeyPath string `env:"ORLY_CERTS_ACCOUNT_KEY" default:"" usage:"path to ACME account key (auto-generated if empty)"`
}
// ProductionACMEServer is the Let's Encrypt production ACME server
const ProductionACMEServer = "https://acme-v02.api.letsencrypt.org/directory"
// StagingACMEServer is the Let's Encrypt staging ACME server (for testing)
const StagingACMEServer = "https://acme-staging-v02.api.letsencrypt.org/directory"
// loadConfig loads configuration from environment variables.
func loadConfig() *Config {
cfg := &Config{}
if err := env.Load(cfg, nil); chk.E(err) {
log.E.F("failed to load config: %v", err)
os.Exit(1)
}
return cfg
}
// ACMEServerURL returns the ACME server URL to use.
func (c *Config) ACMEServerURL() string {
if c.ACMEServer != "" {
return c.ACMEServer
}
return ProductionACMEServer
}
// BaseDomain extracts the base domain from the wildcard domain.
// e.g., "*.myapp.com" -> "myapp.com"
func (c *Config) BaseDomain() string {
domain := c.Domain
if len(domain) > 2 && domain[:2] == "*." {
return domain[2:]
}
return domain
}

112
cmd/orly-certs/main.go

@ -0,0 +1,112 @@
// orly-certs is a certificate management service that obtains and renews
// wildcard SSL certificates from Let's Encrypt using DNS-01 challenges.
//
// It supports multiple DNS providers via the lego library and stores
// certificates at a conventional file path for web apps to consume.
//
// Configuration is via environment variables:
// - ORLY_CERTS_DOMAIN: Wildcard domain (e.g., "*.myapp.com")
// - ORLY_CERTS_EMAIL: Email for Let's Encrypt account
// - ORLY_CERTS_DNS_PROVIDER: DNS provider name (cloudflare, route53, etc.)
// - ORLY_CERTS_OUTPUT_DIR: Certificate output directory (default: /var/cache/orly-certs)
//
// Provider-specific credentials are set via standard lego environment variables.
// See https://go-acme.github.io/lego/dns/ for documentation.
package main
import (
"context"
"fmt"
"os"
"os/signal"
"syscall"
"time"
"lol.mleku.dev"
"lol.mleku.dev/chk"
"lol.mleku.dev/log"
)
func main() {
cfg := loadConfig()
lol.SetLogLevel(cfg.LogLevel)
log.I.F("orly-certs starting")
log.I.F(" domain: %s", cfg.Domain)
log.I.F(" email: %s", cfg.Email)
log.I.F(" dns provider: %s", cfg.DNSProvider)
log.I.F(" output dir: %s", cfg.OutputDir)
log.I.F(" acme server: %s", cfg.ACMEServerURL())
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Set up signal handling
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigs
log.I.F("shutdown signal received")
cancel()
}()
// Create certificate manager
manager, err := NewCertManager(cfg)
if chk.E(err) {
log.F.F("failed to create certificate manager: %v", err)
}
// Initial certificate check/obtain
if err := manager.EnsureCertificate(); chk.E(err) {
log.F.F("failed to ensure certificate: %v", err)
}
// Start renewal loop
log.I.F("starting renewal check loop (interval: %s)", cfg.CheckInterval)
ticker := time.NewTicker(cfg.CheckInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if err := manager.CheckRenewal(); chk.E(err) {
log.E.F("renewal check failed: %v", err)
}
case <-ctx.Done():
log.I.F("orly-certs shutting down")
return
}
}
}
func usage() {
fmt.Fprintf(os.Stderr, `orly-certs - DNS-01 wildcard certificate manager
Usage: orly-certs [options]
Environment Variables:
ORLY_CERTS_DOMAIN Wildcard domain (e.g., *.myapp.com) [required]
ORLY_CERTS_EMAIL Email for Let's Encrypt account [required]
ORLY_CERTS_DNS_PROVIDER DNS provider name [required]
ORLY_CERTS_OUTPUT_DIR Certificate output directory [default: /var/cache/orly-certs]
ORLY_CERTS_RENEW_DAYS Renew when expiring within N days [default: 30]
ORLY_CERTS_CHECK_INTERVAL Renewal check interval [default: 12h]
ORLY_CERTS_ACME_SERVER ACME server URL [default: production Let's Encrypt]
ORLY_CERTS_LOG_LEVEL Log level [default: info]
Supported DNS Providers:
cloudflare, route53, hetzner, digitalocean, google, namecheap, godaddy,
ovh, vultr, linode, gandi, dnsimple, duckdns, azure, alidns, and 80+ more.
Provider credentials are set via standard lego environment variables.
See https://go-acme.github.io/lego/dns/ for documentation.
Example:
export CF_API_TOKEN="your-cloudflare-api-token"
export ORLY_CERTS_DOMAIN="*.myapp.com"
export ORLY_CERTS_EMAIL="admin@myapp.com"
export ORLY_CERTS_DNS_PROVIDER="cloudflare"
./orly-certs
`)
}

304
cmd/orly-certs/manager.go

@ -0,0 +1,304 @@
package main
import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"os"
"path/filepath"
"time"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/certificate"
"github.com/go-acme/lego/v4/lego"
"github.com/go-acme/lego/v4/registration"
"lol.mleku.dev/chk"
"lol.mleku.dev/log"
)
// CertManager handles certificate acquisition and renewal.
type CertManager struct {
cfg *Config
client *lego.Client
user *User
certPath string
keyPath string
metaPath string
}
// User implements the lego registration.User interface.
type User struct {
Email string
Registration *registration.Resource
key crypto.PrivateKey
}
func (u *User) GetEmail() string {
return u.Email
}
func (u *User) GetRegistration() *registration.Resource {
return u.Registration
}
func (u *User) GetPrivateKey() crypto.PrivateKey {
return u.key
}
// CertMetadata stores certificate metadata.
type CertMetadata struct {
Domain string `json:"domain"`
Domains []string `json:"domains"`
NotBefore time.Time `json:"not_before"`
NotAfter time.Time `json:"not_after"`
Issuer string `json:"issuer"`
RenewedAt time.Time `json:"renewed_at"`
}
// NewCertManager creates a new certificate manager.
func NewCertManager(cfg *Config) (*CertManager, error) {
// Create output directory
domainDir := filepath.Join(cfg.OutputDir, cfg.BaseDomain())
if err := os.MkdirAll(domainDir, 0755); chk.E(err) {
return nil, fmt.Errorf("failed to create output directory: %w", err)
}
// Generate or load account private key
privateKey, err := loadOrCreateAccountKey(cfg)
if chk.E(err) {
return nil, fmt.Errorf("failed to load/create account key: %w", err)
}
user := &User{
Email: cfg.Email,
key: privateKey,
}
// Create lego config
legoCfg := lego.NewConfig(user)
legoCfg.CADirURL = cfg.ACMEServerURL()
legoCfg.Certificate.KeyType = certcrypto.EC256
// Create lego client
client, err := lego.NewClient(legoCfg)
if chk.E(err) {
return nil, fmt.Errorf("failed to create ACME client: %w", err)
}
// Set up DNS provider
dnsProvider, err := NewDNSProvider(cfg.DNSProvider)
if chk.E(err) {
return nil, err
}
if err := client.Challenge.SetDNS01Provider(dnsProvider); chk.E(err) {
return nil, fmt.Errorf("failed to set DNS provider: %w", err)
}
// Register account if needed
reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
if err != nil {
// Try to recover existing registration
reg, err = client.Registration.ResolveAccountByKey()
if chk.E(err) {
return nil, fmt.Errorf("failed to register account: %w", err)
}
}
user.Registration = reg
return &CertManager{
cfg: cfg,
client: client,
user: user,
certPath: filepath.Join(domainDir, "cert.pem"),
keyPath: filepath.Join(domainDir, "key.pem"),
metaPath: filepath.Join(domainDir, "metadata.json"),
}, nil
}
// EnsureCertificate obtains a certificate if none exists or if it needs renewal.
func (m *CertManager) EnsureCertificate() error {
// Check if certificate exists and is valid
if m.certificateExists() {
needsRenewal, err := m.needsRenewal()
if chk.E(err) {
log.W.F("failed to check renewal status, will obtain new cert: %v", err)
} else if !needsRenewal {
log.I.F("certificate is valid, no renewal needed")
return nil
}
log.I.F("certificate needs renewal")
}
return m.obtainCertificate()
}
// CheckRenewal checks if the certificate needs renewal and renews if needed.
func (m *CertManager) CheckRenewal() error {
if !m.certificateExists() {
return m.obtainCertificate()
}
needsRenewal, err := m.needsRenewal()
if chk.E(err) {
return err
}
if needsRenewal {
log.I.F("certificate expiring soon, renewing...")
return m.obtainCertificate()
}
log.D.F("certificate still valid, no renewal needed")
return nil
}
func (m *CertManager) certificateExists() bool {
_, err := os.Stat(m.certPath)
return err == nil
}
func (m *CertManager) needsRenewal() (bool, error) {
certPEM, err := os.ReadFile(m.certPath)
if chk.E(err) {
return true, err
}
block, _ := pem.Decode(certPEM)
if block == nil {
return true, fmt.Errorf("failed to decode certificate PEM")
}
cert, err := x509.ParseCertificate(block.Bytes)
if chk.E(err) {
return true, err
}
// Check if certificate expires within RenewDays
renewTime := time.Now().Add(time.Duration(m.cfg.RenewDays) * 24 * time.Hour)
return cert.NotAfter.Before(renewTime), nil
}
func (m *CertManager) obtainCertificate() error {
log.I.F("obtaining certificate for %s", m.cfg.Domain)
request := certificate.ObtainRequest{
Domains: []string{m.cfg.Domain, m.cfg.BaseDomain()},
Bundle: true,
}
certificates, err := m.client.Certificate.Obtain(request)
if chk.E(err) {
return fmt.Errorf("failed to obtain certificate: %w", err)
}
// Write certificate chain
if err := os.WriteFile(m.certPath, certificates.Certificate, 0644); chk.E(err) {
return fmt.Errorf("failed to write certificate: %w", err)
}
// Write private key with restricted permissions
if err := os.WriteFile(m.keyPath, certificates.PrivateKey, 0600); chk.E(err) {
return fmt.Errorf("failed to write private key: %w", err)
}
// Write issuer certificate if available
if len(certificates.IssuerCertificate) > 0 {
issuerPath := filepath.Join(filepath.Dir(m.certPath), "issuer.pem")
if err := os.WriteFile(issuerPath, certificates.IssuerCertificate, 0644); chk.E(err) {
log.W.F("failed to write issuer certificate: %v", err)
}
}
// Write metadata
if err := m.writeMetadata(certificates.Certificate); chk.E(err) {
log.W.F("failed to write metadata: %v", err)
}
log.I.F("certificate obtained successfully for %s", m.cfg.Domain)
log.I.F(" cert: %s", m.certPath)
log.I.F(" key: %s", m.keyPath)
return nil
}
func (m *CertManager) writeMetadata(certPEM []byte) error {
block, _ := pem.Decode(certPEM)
if block == nil {
return fmt.Errorf("failed to decode certificate for metadata")
}
cert, err := x509.ParseCertificate(block.Bytes)
if chk.E(err) {
return err
}
meta := CertMetadata{
Domain: m.cfg.Domain,
Domains: cert.DNSNames,
NotBefore: cert.NotBefore,
NotAfter: cert.NotAfter,
Issuer: cert.Issuer.CommonName,
RenewedAt: time.Now(),
}
data, err := json.MarshalIndent(meta, "", " ")
if chk.E(err) {
return err
}
return os.WriteFile(m.metaPath, data, 0644)
}
func loadOrCreateAccountKey(cfg *Config) (crypto.PrivateKey, error) {
keyPath := cfg.AccountKeyPath
if keyPath == "" {
keyPath = filepath.Join(cfg.OutputDir, "account.key")
}
// Try to load existing key
if data, err := os.ReadFile(keyPath); err == nil {
block, _ := pem.Decode(data)
if block != nil {
key, err := x509.ParseECPrivateKey(block.Bytes)
if err == nil {
log.D.F("loaded existing account key from %s", keyPath)
return key, nil
}
}
}
// Generate new key
log.I.F("generating new account key")
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if chk.E(err) {
return nil, err
}
// Save key
keyBytes, err := x509.MarshalECPrivateKey(key)
if chk.E(err) {
return nil, err
}
keyPEM := pem.EncodeToMemory(&pem.Block{
Type: "EC PRIVATE KEY",
Bytes: keyBytes,
})
if err := os.MkdirAll(filepath.Dir(keyPath), 0755); chk.E(err) {
return nil, err
}
if err := os.WriteFile(keyPath, keyPEM, 0600); chk.E(err) {
return nil, err
}
log.I.F("saved new account key to %s", keyPath)
return key, nil
}

55
cmd/orly-certs/providers.go

@ -0,0 +1,55 @@
package main
import (
"fmt"
"github.com/go-acme/lego/v4/challenge"
"github.com/go-acme/lego/v4/providers/dns"
)
// NewDNSProvider creates a DNS challenge provider by name.
// The provider will be configured using standard environment variables
// as documented by lego for each provider.
//
// Common providers and their environment variables:
// - cloudflare: CF_API_TOKEN or CF_API_EMAIL + CF_API_KEY
// - route53: AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY + AWS_REGION
// - hetzner: HETZNER_API_KEY
// - digitalocean: DO_AUTH_TOKEN
// - google: GCE_PROJECT + GCE_SERVICE_ACCOUNT_FILE
// - namecheap: NAMECHEAP_API_USER + NAMECHEAP_API_KEY
// - godaddy: GODADDY_API_KEY + GODADDY_API_SECRET
// - ovh: OVH_ENDPOINT + OVH_APPLICATION_KEY + OVH_APPLICATION_SECRET + OVH_CONSUMER_KEY
// - vultr: VULTR_API_KEY
// - linode: LINODE_TOKEN
//
// See https://go-acme.github.io/lego/dns/ for full list and documentation.
func NewDNSProvider(name string) (challenge.Provider, error) {
provider, err := dns.NewDNSChallengeProviderByName(name)
if err != nil {
return nil, fmt.Errorf("failed to create DNS provider '%s': %w", name, err)
}
return provider, nil
}
// SupportedProviders returns a list of commonly used DNS providers.
// This is not exhaustive - lego supports 100+ providers.
func SupportedProviders() []string {
return []string{
"cloudflare",
"route53",
"hetzner",
"digitalocean",
"google",
"namecheap",
"godaddy",
"ovh",
"vultr",
"linode",
"gandi",
"dnsimple",
"duckdns",
"azure",
"alidns",
}
}

23
cmd/orly-launcher/config.go

@ -39,6 +39,10 @@ type ConfigFile struct {
NegentropyEnabled *bool `json:"negentropy_enabled,omitempty"` NegentropyEnabled *bool `json:"negentropy_enabled,omitempty"`
NegentropyBinary string `json:"negentropy_binary,omitempty"` NegentropyBinary string `json:"negentropy_binary,omitempty"`
NegentropyListen string `json:"negentropy_listen,omitempty"` NegentropyListen string `json:"negentropy_listen,omitempty"`
// Certificate service
CertsEnabled *bool `json:"certs_enabled,omitempty"`
CertsBinary string `json:"certs_binary,omitempty"`
} }
// configFilePath returns the path to the config file. // configFilePath returns the path to the config file.
@ -104,6 +108,8 @@ func ConfigToFile(cfg *Config) *ConfigFile {
NegentropyEnabled: &cfg.NegentropyEnabled, NegentropyEnabled: &cfg.NegentropyEnabled,
NegentropyBinary: cfg.NegentropyBinary, NegentropyBinary: cfg.NegentropyBinary,
NegentropyListen: cfg.NegentropyListen, NegentropyListen: cfg.NegentropyListen,
CertsEnabled: &cfg.CertsEnabled,
CertsBinary: cfg.CertsBinary,
} }
} }
@ -182,6 +188,16 @@ type Config struct {
// SyncReadyTimeout is how long to wait for sync services to be ready // SyncReadyTimeout is how long to wait for sync services to be ready
SyncReadyTimeout time.Duration SyncReadyTimeout time.Duration
// Certificate service configuration
// CertsEnabled enables the certificate service
CertsEnabled bool
// CertsBinary is the path to the certificate service binary
CertsBinary string
// ServicesEnabled controls whether to start the DB, relay, and other services
// When false, only the admin UI runs (useful for initial setup/updates)
ServicesEnabled bool
// Admin UI configuration // Admin UI configuration
// AdminEnabled controls whether to run the admin HTTP server // AdminEnabled controls whether to run the admin HTTP server
AdminEnabled bool AdminEnabled bool
@ -252,6 +268,13 @@ func loadConfig() (*Config, error) {
SyncReadyTimeout: parseDuration("ORLY_LAUNCHER_SYNC_READY_TIMEOUT", 30*time.Second), SyncReadyTimeout: parseDuration("ORLY_LAUNCHER_SYNC_READY_TIMEOUT", 30*time.Second),
// Certificate service configuration
CertsEnabled: boolEnvOrFile("ORLY_LAUNCHER_CERTS_ENABLED", cf.CertsEnabled, false),
CertsBinary: envOrFileOrDefault("ORLY_LAUNCHER_CERTS_BINARY", cf.CertsBinary, "orly-certs"),
// Services enabled (default true for backwards compatibility)
ServicesEnabled: getEnvOrDefault("ORLY_LAUNCHER_SERVICES_ENABLED", "true") == "true",
// Admin UI configuration // Admin UI configuration
AdminEnabled: getEnvOrDefault("ORLY_LAUNCHER_ADMIN_ENABLED", "true") == "true", AdminEnabled: getEnvOrDefault("ORLY_LAUNCHER_ADMIN_ENABLED", "true") == "true",
AdminPort: intEnvOrFile("ORLY_LAUNCHER_ADMIN_PORT", cf.AdminPort, 8080), AdminPort: intEnvOrFile("ORLY_LAUNCHER_ADMIN_PORT", cf.AdminPort, 8080),

28
cmd/orly-launcher/main.go

@ -50,9 +50,13 @@ func main() {
}() }()
log.I.F("starting orly-launcher %s", version.V) log.I.F("starting orly-launcher %s", version.V)
log.I.F("database binary: %s", cfg.DBBinary) if cfg.ServicesEnabled {
log.I.F("relay binary: %s", cfg.RelayBinary) log.I.F("database binary: %s", cfg.DBBinary)
log.I.F("database listen: %s", cfg.DBListen) log.I.F("relay binary: %s", cfg.RelayBinary)
log.I.F("database listen: %s", cfg.DBListen)
} else {
log.I.F("services disabled - running admin UI only")
}
// Start admin server if enabled // Start admin server if enabled
var adminServer *AdminServer var adminServer *AdminServer
@ -78,17 +82,22 @@ func main() {
} }
} }
if err := supervisor.Start(); chk.E(err) { // Only start services if enabled
fmt.Fprintf(os.Stderr, "failed to start: %v\n", err) if cfg.ServicesEnabled {
os.Exit(1) if err := supervisor.Start(); chk.E(err) {
fmt.Fprintf(os.Stderr, "failed to start: %v\n", err)
os.Exit(1)
}
} }
// Wait for context cancellation (signal received) // Wait for context cancellation (signal received)
<-ctx.Done() <-ctx.Done()
log.I.F("stopping supervisor...") if cfg.ServicesEnabled {
if err := supervisor.Stop(); chk.E(err) { log.I.F("stopping supervisor...")
log.E.F("error during shutdown: %v", err) if err := supervisor.Stop(); chk.E(err) {
log.E.F("error during shutdown: %v", err)
}
} }
log.I.F("orly-launcher stopped") log.I.F("orly-launcher stopped")
@ -107,6 +116,7 @@ Commands:
Environment Variables: Environment Variables:
Process Management: Process Management:
ORLY_LAUNCHER_SERVICES_ENABLED Start DB/relay on launch (default: true)
ORLY_LAUNCHER_DB_BINARY Path to orly-db binary (default: orly-db-{backend}) 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_RELAY_BINARY Path to orly binary (default: orly)
ORLY_LAUNCHER_ACL_BINARY Path to orly-acl binary (default: orly-acl-{mode}) ORLY_LAUNCHER_ACL_BINARY Path to orly-acl binary (default: orly-acl-{mode})

232
cmd/orly-launcher/server.go

@ -4,6 +4,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"net/http" "net/http"
"time" "time"
@ -45,8 +46,12 @@ func (s *AdminServer) Start(ctx context.Context) error {
mux.HandleFunc("/api/config", s.auth.RequireAuth(s.handleConfig)) mux.HandleFunc("/api/config", s.auth.RequireAuth(s.handleConfig))
mux.HandleFunc("/api/binaries", s.auth.RequireAuth(s.handleBinaries)) mux.HandleFunc("/api/binaries", s.auth.RequireAuth(s.handleBinaries))
mux.HandleFunc("/api/update", s.auth.RequireAuth(s.handleUpdate)) mux.HandleFunc("/api/update", s.auth.RequireAuth(s.handleUpdate))
mux.HandleFunc("/api/releases", s.auth.RequireAuth(s.handleReleases))
mux.HandleFunc("/api/restart", s.auth.RequireAuth(s.handleRestart)) mux.HandleFunc("/api/restart", s.auth.RequireAuth(s.handleRestart))
mux.HandleFunc("/api/restart-service", s.auth.RequireAuth(s.handleRestartService))
mux.HandleFunc("/api/rollback", s.auth.RequireAuth(s.handleRollback)) mux.HandleFunc("/api/rollback", s.auth.RequireAuth(s.handleRollback))
mux.HandleFunc("/api/start-services", s.auth.RequireAuth(s.handleStartServices))
mux.HandleFunc("/api/stop-services", s.auth.RequireAuth(s.handleStopServices))
addr := fmt.Sprintf(":%d", s.cfg.AdminPort) addr := fmt.Sprintf(":%d", s.cfg.AdminPort)
s.server = &http.Server{ s.server = &http.Server{
@ -68,9 +73,10 @@ func (s *AdminServer) Start(ctx context.Context) error {
// StatusResponse is the response for GET /api/status // StatusResponse is the response for GET /api/status
type StatusResponse struct { type StatusResponse struct {
Version string `json:"version"` Version string `json:"version"`
Uptime string `json:"uptime"` Uptime string `json:"uptime"`
Processes []ProcessStatus `json:"processes"` ServicesRunning bool `json:"services_running"`
Processes []ProcessStatus `json:"processes"`
} }
// ProcessStatus represents the status of a single managed process. // ProcessStatus represents the status of a single managed process.
@ -94,9 +100,10 @@ func (s *AdminServer) handleStatus(w http.ResponseWriter, r *http.Request) {
processes := s.supervisor.GetProcessStatuses() processes := s.supervisor.GetProcessStatuses()
response := StatusResponse{ response := StatusResponse{
Version: s.updater.CurrentVersion(), Version: s.updater.CurrentVersion(),
Uptime: uptime, Uptime: uptime,
Processes: processes, ServicesRunning: s.supervisor.IsRunning(),
Processes: processes,
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
@ -307,6 +314,83 @@ func (s *AdminServer) handleBinaries(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(response) json.NewEncoder(w).Encode(response)
} }
// ReleasesResponse is the response for GET /api/releases
type ReleasesResponse struct {
Releases []ReleaseInfo `json:"releases"`
}
// ReleaseInfo represents a single release/tag
type ReleaseInfo struct {
Tag string `json:"tag"`
Message string `json:"message"`
}
const tagsAPIURL = "https://git.nostrdev.com/api/v1/repos/mleku/next.orly.dev/tags"
func (s *AdminServer) handleReleases(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Fetch tags from upstream
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Get(tagsAPIURL)
if err != nil {
log.E.F("failed to fetch tags: %v", err)
http.Error(w, "Failed to fetch releases", http.StatusBadGateway)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
http.Error(w, "Failed to fetch releases", http.StatusBadGateway)
return
}
body, err := io.ReadAll(resp.Body)
if err != nil {
http.Error(w, "Failed to read response", http.StatusInternalServerError)
return
}
// Parse the tags response
var tags []struct {
Name string `json:"name"`
Message string `json:"message"`
}
if err := json.Unmarshal(body, &tags); chk.E(err) {
http.Error(w, "Failed to parse response", http.StatusInternalServerError)
return
}
// Filter and transform to our response format
var releases []ReleaseInfo
for _, tag := range tags {
if len(tag.Name) > 0 && tag.Name[0] == 'v' {
msg := tag.Message
// Get first line only
for i, c := range msg {
if c == '\n' {
msg = msg[:i]
break
}
}
releases = append(releases, ReleaseInfo{
Tag: tag.Name,
Message: msg,
})
}
if len(releases) >= 15 {
break
}
}
response := ReleasesResponse{Releases: releases}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
// UpdateRequest is the request body for POST /api/update // UpdateRequest is the request body for POST /api/update
type UpdateRequest struct { type UpdateRequest struct {
Version string `json:"version"` Version string `json:"version"`
@ -396,6 +480,62 @@ func (s *AdminServer) handleRestart(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(response) json.NewEncoder(w).Encode(response)
} }
// RestartServiceRequest is the request body for POST /api/restart-service
type RestartServiceRequest struct {
Service string `json:"service"`
}
// RestartServiceResponse is the response for POST /api/restart-service
type RestartServiceResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
Restarted []string `json:"restarted"`
}
func (s *AdminServer) handleRestartService(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req RestartServiceRequest
if err := json.NewDecoder(r.Body).Decode(&req); chk.E(err) {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
if req.Service == "" {
http.Error(w, "Service name is required", http.StatusBadRequest)
return
}
// Map binary names to service names
serviceName := req.Service
switch req.Service {
case "orly-db-badger", "orly-db-neo4j":
serviceName = "orly-db"
case "orly-acl-follows", "orly-acl-managed", "orly-acl-curation":
serviceName = "orly-acl"
}
// Perform the restart in a goroutine to avoid blocking
go func() {
if restarted, err := s.supervisor.RestartService(serviceName); chk.E(err) {
log.E.F("restart service %s failed: %v", serviceName, err)
} else {
log.I.F("restart service completed: %v", restarted)
}
}()
response := RestartServiceResponse{
Success: true,
Message: fmt.Sprintf("Restart of %s initiated", serviceName),
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
// RollbackResponse is the response for POST /api/rollback // RollbackResponse is the response for POST /api/rollback
type RollbackResponse struct { type RollbackResponse struct {
Success bool `json:"success"` Success bool `json:"success"`
@ -434,6 +574,86 @@ func (s *AdminServer) handleRollback(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(response) json.NewEncoder(w).Encode(response)
} }
// StartServicesResponse is the response for POST /api/start-services
type StartServicesResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
}
func (s *AdminServer) handleStartServices(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Check if services are already running
if s.supervisor.IsRunning() {
response := StartServicesResponse{
Success: false,
Message: "Services are already running",
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusConflict)
json.NewEncoder(w).Encode(response)
return
}
// Start services in a goroutine
go func() {
if err := s.supervisor.Start(); chk.E(err) {
log.E.F("failed to start services: %v", err)
}
}()
response := StartServicesResponse{
Success: true,
Message: "Services starting...",
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
// StopServicesResponse is the response for POST /api/stop-services
type StopServicesResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
}
func (s *AdminServer) handleStopServices(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Check if services are running
if !s.supervisor.IsRunning() {
response := StopServicesResponse{
Success: false,
Message: "Services are not running",
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusConflict)
json.NewEncoder(w).Encode(response)
return
}
// Stop services in a goroutine
go func() {
if err := s.supervisor.Stop(); chk.E(err) {
log.E.F("failed to stop services: %v", err)
}
}()
response := StopServicesResponse{
Success: true,
Message: "Services stopping...",
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
func (s *AdminServer) serveUI(w http.ResponseWriter, r *http.Request) { func (s *AdminServer) serveUI(w http.ResponseWriter, r *http.Request) {
s.serveAdminUI(w, r) s.serveAdminUI(w, r)
} }

263
cmd/orly-launcher/supervisor.go

@ -34,6 +34,9 @@ type Supervisor struct {
relayGroupProc *Process relayGroupProc *Process
negentropyProc *Process negentropyProc *Process
// Certificate service process
certsProc *Process
wg sync.WaitGroup wg sync.WaitGroup
mu sync.Mutex mu sync.Mutex
closed bool closed bool
@ -57,8 +60,37 @@ func NewSupervisor(ctx context.Context, cancel context.CancelFunc, cfg *Config)
} }
} }
// IsRunning returns true if any managed processes are running.
func (s *Supervisor) IsRunning() bool {
s.mu.Lock()
defer s.mu.Unlock()
// Check if any process is running
if s.dbProc != nil {
select {
case <-s.dbProc.exited:
// Process has exited
default:
return true
}
}
if s.relayProc != nil {
select {
case <-s.relayProc.exited:
// Process has exited
default:
return true
}
}
return false
}
// Start starts the database, optional ACL server, sync services, and relay processes. // Start starts the database, optional ACL server, sync services, and relay processes.
func (s *Supervisor) Start() error { func (s *Supervisor) Start() error {
s.mu.Lock()
s.closed = false
s.mu.Unlock()
// 1. Start database server // 1. Start database server
if err := s.startDB(); err != nil { if err := s.startDB(); err != nil {
return fmt.Errorf("failed to start database: %w", err) return fmt.Errorf("failed to start database: %w", err)
@ -109,7 +141,17 @@ func (s *Supervisor) Start() error {
return fmt.Errorf("failed to start relay: %w", err) return fmt.Errorf("failed to start relay: %w", err)
} }
// 6. Start monitoring goroutines // 6. Start certificate service if enabled (independent of other services)
if s.cfg.CertsEnabled {
if err := s.startCerts(); err != nil {
log.W.F("failed to start certificate service: %v", err)
// Don't fail startup - certs are independent
} else {
log.I.F("certificate service started")
}
}
// 7. Start monitoring goroutines
monitorCount := 2 // db + relay monitorCount := 2 // db + relay
if s.cfg.ACLEnabled { if s.cfg.ACLEnabled {
monitorCount++ monitorCount++
@ -126,6 +168,9 @@ func (s *Supervisor) Start() error {
if s.cfg.NegentropyEnabled { if s.cfg.NegentropyEnabled {
monitorCount++ monitorCount++
} }
if s.cfg.CertsEnabled {
monitorCount++
}
s.wg.Add(monitorCount) s.wg.Add(monitorCount)
go s.monitorProcess(s.dbProc, "db", s.startDB) go s.monitorProcess(s.dbProc, "db", s.startDB)
@ -144,6 +189,9 @@ func (s *Supervisor) Start() error {
if s.cfg.NegentropyEnabled { if s.cfg.NegentropyEnabled {
go s.monitorProcess(s.negentropyProc, "negentropy", s.startNegentropy) go s.monitorProcess(s.negentropyProc, "negentropy", s.startNegentropy)
} }
if s.cfg.CertsEnabled {
go s.monitorProcess(s.certsProc, "certs", s.startCerts)
}
go s.monitorProcess(s.relayProc, "relay", s.startRelay) go s.monitorProcess(s.relayProc, "relay", s.startRelay)
return nil return nil
@ -159,7 +207,13 @@ func (s *Supervisor) Stop() error {
s.closed = true s.closed = true
s.mu.Unlock() s.mu.Unlock()
// Stop relay first (it depends on sync services, ACL, and DB) // Stop certificate service first (independent, nothing depends on it)
if s.cfg.CertsEnabled && s.certsProc != nil {
log.I.F("stopping certificate service...")
s.stopProcess(s.certsProc, 5*time.Second)
}
// Stop relay (it depends on sync services, ACL, and DB)
log.I.F("stopping relay...") log.I.F("stopping relay...")
s.stopProcess(s.relayProc, 5*time.Second) s.stopProcess(s.relayProc, 5*time.Second)
@ -520,6 +574,8 @@ func (s *Supervisor) monitorProcess(p *Process, procType string, restart func()
p = s.relayGroupProc p = s.relayGroupProc
case "negentropy": case "negentropy":
p = s.negentropyProc p = s.negentropyProc
case "certs":
p = s.certsProc
default: default:
p = s.relayProc p = s.relayProc
} }
@ -827,6 +883,40 @@ func (s *Supervisor) startNegentropy() error {
return nil return nil
} }
func (s *Supervisor) startCerts() error {
s.mu.Lock()
defer s.mu.Unlock()
// Certificate service uses its own environment variables
// ORLY_CERTS_DOMAIN, ORLY_CERTS_EMAIL, ORLY_CERTS_DNS_PROVIDER, etc.
env := os.Environ()
env = append(env, fmt.Sprintf("ORLY_CERTS_LOG_LEVEL=%s", s.cfg.LogLevel))
cmd := exec.CommandContext(s.ctx, s.cfg.CertsBinary)
cmd.Env = env
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Start(); chk.E(err) {
return err
}
exited := make(chan struct{})
s.certsProc = &Process{
name: "orly-certs",
cmd: cmd,
exited: exited,
}
go func() {
cmd.Wait()
close(exited)
}()
log.I.F("started certificate service (pid %d)", cmd.Process.Pid)
return nil
}
// GetProcessStatuses returns the status of all managed processes. // GetProcessStatuses returns the status of all managed processes.
func (s *Supervisor) GetProcessStatuses() []ProcessStatus { func (s *Supervisor) GetProcessStatuses() []ProcessStatus {
s.mu.Lock() s.mu.Lock()
@ -858,6 +948,11 @@ func (s *Supervisor) GetProcessStatuses() []ProcessStatus {
statuses = append(statuses, s.getProcessStatus(s.negentropyProc, s.cfg.NegentropyBinary)) statuses = append(statuses, s.getProcessStatus(s.negentropyProc, s.cfg.NegentropyBinary))
} }
// Certificate service
if s.cfg.CertsEnabled && s.certsProc != nil {
statuses = append(statuses, s.getProcessStatus(s.certsProc, s.cfg.CertsBinary))
}
// Relay process // Relay process
if s.relayProc != nil { if s.relayProc != nil {
statuses = append(statuses, s.getProcessStatus(s.relayProc, s.cfg.RelayBinary)) statuses = append(statuses, s.getProcessStatus(s.relayProc, s.cfg.RelayBinary))
@ -894,6 +989,170 @@ func (s *Supervisor) getProcessStatus(p *Process, binaryPath string) ProcessStat
} }
} }
// RestartService restarts a specific service with dependency handling.
// If a service's dependencies need to restart, they are handled appropriately.
// Returns the list of services that were restarted.
func (s *Supervisor) RestartService(serviceName string) ([]string, error) {
log.I.F("restarting service: %s", serviceName)
var restarted []string
// Determine which services need to restart based on dependencies
// db → acl, sync services, relay all depend on db
// acl → relay depends on acl
// sync services → relay may depend on sync services
// relay → nothing depends on relay
switch serviceName {
case "orly-db", "db":
// Restart db and all dependent services
// First stop in reverse order: relay, sync, acl, db
s.stopProcess(s.relayProc, 5*time.Second)
s.stopSyncServices()
if s.cfg.ACLEnabled && s.aclProc != nil {
s.stopProcess(s.aclProc, 5*time.Second)
}
s.stopProcess(s.dbProc, s.cfg.StopTimeout)
time.Sleep(500 * time.Millisecond)
// Start in dependency order
if err := s.startDB(); err != nil {
return restarted, fmt.Errorf("failed to restart db: %w", err)
}
restarted = append(restarted, "orly-db")
if err := s.waitForDBReady(s.cfg.DBReadyTimeout); err != nil {
return restarted, fmt.Errorf("db not ready: %w", err)
}
if s.cfg.ACLEnabled {
if err := s.startACL(); err != nil {
return restarted, fmt.Errorf("failed to restart acl: %w", err)
}
restarted = append(restarted, "orly-acl")
if err := s.waitForACLReady(s.cfg.ACLReadyTimeout); err != nil {
return restarted, fmt.Errorf("acl not ready: %w", err)
}
}
if err := s.startSyncServices(); err != nil {
return restarted, fmt.Errorf("failed to restart sync services: %w", err)
}
if s.cfg.DistributedSyncEnabled {
restarted = append(restarted, "orly-sync-distributed")
}
if s.cfg.ClusterSyncEnabled {
restarted = append(restarted, "orly-sync-cluster")
}
if s.cfg.RelayGroupEnabled {
restarted = append(restarted, "orly-sync-relaygroup")
}
if s.cfg.NegentropyEnabled {
restarted = append(restarted, "orly-sync-negentropy")
}
if err := s.startRelay(); err != nil {
return restarted, fmt.Errorf("failed to restart relay: %w", err)
}
restarted = append(restarted, "orly")
case "orly-acl", "acl":
if !s.cfg.ACLEnabled {
return restarted, fmt.Errorf("ACL is not enabled")
}
// Restart acl and relay (relay depends on acl)
s.stopProcess(s.relayProc, 5*time.Second)
s.stopProcess(s.aclProc, 5*time.Second)
time.Sleep(500 * time.Millisecond)
if err := s.startACL(); err != nil {
return restarted, fmt.Errorf("failed to restart acl: %w", err)
}
restarted = append(restarted, "orly-acl")
if err := s.waitForACLReady(s.cfg.ACLReadyTimeout); err != nil {
return restarted, fmt.Errorf("acl not ready: %w", err)
}
if err := s.startRelay(); err != nil {
return restarted, fmt.Errorf("failed to restart relay: %w", err)
}
restarted = append(restarted, "orly")
case "orly-sync-distributed", "distributed-sync":
if !s.cfg.DistributedSyncEnabled {
return restarted, fmt.Errorf("distributed sync is not enabled")
}
s.stopProcess(s.distributedSyncProc, 5*time.Second)
time.Sleep(500 * time.Millisecond)
if err := s.startDistributedSync(); err != nil {
return restarted, fmt.Errorf("failed to restart distributed sync: %w", err)
}
restarted = append(restarted, "orly-sync-distributed")
case "orly-sync-cluster", "cluster-sync":
if !s.cfg.ClusterSyncEnabled {
return restarted, fmt.Errorf("cluster sync is not enabled")
}
s.stopProcess(s.clusterSyncProc, 5*time.Second)
time.Sleep(500 * time.Millisecond)
if err := s.startClusterSync(); err != nil {
return restarted, fmt.Errorf("failed to restart cluster sync: %w", err)
}
restarted = append(restarted, "orly-sync-cluster")
case "orly-sync-relaygroup", "relaygroup":
if !s.cfg.RelayGroupEnabled {
return restarted, fmt.Errorf("relaygroup is not enabled")
}
s.stopProcess(s.relayGroupProc, 5*time.Second)
time.Sleep(500 * time.Millisecond)
if err := s.startRelayGroup(); err != nil {
return restarted, fmt.Errorf("failed to restart relaygroup: %w", err)
}
restarted = append(restarted, "orly-sync-relaygroup")
case "orly-sync-negentropy", "negentropy":
if !s.cfg.NegentropyEnabled {
return restarted, fmt.Errorf("negentropy is not enabled")
}
s.stopProcess(s.negentropyProc, 5*time.Second)
time.Sleep(500 * time.Millisecond)
if err := s.startNegentropy(); err != nil {
return restarted, fmt.Errorf("failed to restart negentropy: %w", err)
}
restarted = append(restarted, "orly-sync-negentropy")
case "orly-certs", "certs":
if !s.cfg.CertsEnabled {
return restarted, fmt.Errorf("certificate service is not enabled")
}
s.stopProcess(s.certsProc, 5*time.Second)
time.Sleep(500 * time.Millisecond)
if err := s.startCerts(); err != nil {
return restarted, fmt.Errorf("failed to restart certificate service: %w", err)
}
restarted = append(restarted, "orly-certs")
case "orly", "relay":
// Just restart relay
s.stopProcess(s.relayProc, 5*time.Second)
time.Sleep(500 * time.Millisecond)
if err := s.startRelay(); err != nil {
return restarted, fmt.Errorf("failed to restart relay: %w", err)
}
restarted = append(restarted, "orly")
default:
return restarted, fmt.Errorf("unknown service: %s", serviceName)
}
log.I.F("restarted services: %v", restarted)
return restarted, nil
}
// RestartAll stops all processes and starts them again. // RestartAll stops all processes and starts them again.
func (s *Supervisor) RestartAll() error { func (s *Supervisor) RestartAll() error {
log.I.F("restarting all processes...") log.I.F("restarting all processes...")

4
cmd/orly-launcher/web/dist/bundle.css vendored

File diff suppressed because one or more lines are too long

16
cmd/orly-launcher/web/dist/bundle.js vendored

File diff suppressed because one or more lines are too long

59
cmd/orly-launcher/web/src/api.js

@ -122,6 +122,17 @@ export async function fetchBinaries(signer, pubkey) {
return response.json(); return response.json();
} }
/**
* Fetch available releases from official repo (proxied to avoid CORS)
*/
export async function fetchReleases(signer, pubkey) {
const response = await authFetch('/api/releases', {}, signer, pubkey);
if (!response.ok) {
throw new Error(`Failed to fetch releases: ${response.statusText}`);
}
return response.json();
}
/** /**
* Update binaries from URLs * Update binaries from URLs
*/ */
@ -153,6 +164,24 @@ export async function restartServices(signer, pubkey) {
return response.json(); return response.json();
} }
/**
* Restart a specific service with dependency handling
* @param {string} service - The service name (e.g., 'orly-db-badger', 'orly-acl-follows', 'orly')
*/
export async function restartService(signer, pubkey, service) {
const response = await authFetch('/api/restart-service', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ service }),
}, signer, pubkey);
if (!response.ok) {
const data = await response.json().catch(() => ({}));
throw new Error(data.message || `Restart failed: ${response.statusText}`);
}
return response.json();
}
/** /**
* Rollback to previous version * Rollback to previous version
*/ */
@ -167,3 +196,33 @@ export async function rollbackVersion(signer, pubkey) {
} }
return response.json(); return response.json();
} }
/**
* Start all services
*/
export async function startServices(signer, pubkey) {
const response = await authFetch('/api/start-services', {
method: 'POST',
}, signer, pubkey);
if (!response.ok) {
const data = await response.json().catch(() => ({}));
throw new Error(data.message || `Start failed: ${response.statusText}`);
}
return response.json();
}
/**
* Stop all services
*/
export async function stopServices(signer, pubkey) {
const response = await authFetch('/api/stop-services', {
method: 'POST',
}, signer, pubkey);
if (!response.ok) {
const data = await response.json().catch(() => ({}));
throw new Error(data.message || `Stop failed: ${response.statusText}`);
}
return response.json();
}

91
cmd/orly-launcher/web/src/pages/Dashboard.svelte

@ -1,7 +1,7 @@
<script> <script>
import { onMount, onDestroy } from 'svelte'; import { onMount, onDestroy } from 'svelte';
import { userSigner, userPubkey, statusData, isLoading, error } from '../stores.js'; import { userSigner, userPubkey, statusData, isLoading, error } from '../stores.js';
import { fetchStatus, restartServices } from '../api.js'; import { fetchStatus, restartServices, startServices, stopServices } from '../api.js';
import ProcessCard from '../components/ProcessCard.svelte'; import ProcessCard from '../components/ProcessCard.svelte';
let refreshInterval; let refreshInterval;
@ -43,6 +43,36 @@
$isLoading = false; $isLoading = false;
} }
} }
async function handleStart() {
$isLoading = true;
try {
await startServices($userSigner, $userPubkey);
// Wait a moment then refresh
setTimeout(loadStatus, 2000);
} catch (e) {
$error = e.message;
} finally {
$isLoading = false;
}
}
async function handleStop() {
if (!confirm('Are you sure you want to stop all services?')) {
return;
}
$isLoading = true;
try {
await stopServices($userSigner, $userPubkey);
// Wait a moment then refresh
setTimeout(loadStatus, 2000);
} catch (e) {
$error = e.message;
} finally {
$isLoading = false;
}
}
</script> </script>
<div class="dashboard"> <div class="dashboard">
@ -52,9 +82,18 @@
<button class="refresh-btn" on:click={loadStatus} disabled={$isLoading}> <button class="refresh-btn" on:click={loadStatus} disabled={$isLoading}>
Refresh Refresh
</button> </button>
<button class="restart-btn" on:click={handleRestart} disabled={$isLoading}> {#if $statusData?.services_running}
Restart All <button class="stop-btn" on:click={handleStop} disabled={$isLoading}>
</button> Stop Services
</button>
<button class="restart-btn" on:click={handleRestart} disabled={$isLoading}>
Restart All
</button>
{:else}
<button class="start-btn" on:click={handleStart} disabled={$isLoading}>
Start Services
</button>
{/if}
</div> </div>
</div> </div>
@ -64,9 +103,15 @@
{#if $statusData} {#if $statusData}
<div class="status-summary"> <div class="status-summary">
<div class="summary-card">
<span class="label">Status</span>
<span class="value status-indicator" class:running={$statusData.services_running} class:stopped={!$statusData.services_running}>
{$statusData.services_running ? 'Running' : 'Stopped'}
</span>
</div>
<div class="summary-card"> <div class="summary-card">
<span class="label">Version</span> <span class="label">Version</span>
<span class="value">{$statusData.version}</span> <span class="value">{$statusData.version || 'unknown'}</span>
</div> </div>
<div class="summary-card"> <div class="summary-card">
<span class="label">Uptime</span> <span class="label">Uptime</span>
@ -112,7 +157,9 @@
} }
.refresh-btn, .refresh-btn,
.restart-btn { .restart-btn,
.start-btn,
.stop-btn {
padding: 8px 16px; padding: 8px 16px;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
@ -139,8 +186,30 @@
opacity: 0.9; opacity: 0.9;
} }
.start-btn {
background: var(--success, #4caf50);
border: none;
color: white;
}
.start-btn:hover:not(:disabled) {
opacity: 0.9;
}
.stop-btn {
background: var(--error, #f44336);
border: none;
color: white;
}
.stop-btn:hover:not(:disabled) {
opacity: 0.9;
}
.restart-btn:disabled, .restart-btn:disabled,
.refresh-btn:disabled { .refresh-btn:disabled,
.start-btn:disabled,
.stop-btn:disabled {
opacity: 0.5; opacity: 0.5;
cursor: not-allowed; cursor: not-allowed;
} }
@ -182,6 +251,14 @@
color: var(--text-color); color: var(--text-color);
} }
.status-indicator.running {
color: var(--success, #4caf50);
}
.status-indicator.stopped {
color: var(--error, #f44336);
}
h3 { h3 {
font-size: 1.1rem; font-size: 1.1rem;
color: var(--text-color); color: var(--text-color);

661
cmd/orly-launcher/web/src/pages/Update.svelte

@ -1,22 +1,108 @@
<script> <script>
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { userSigner, userPubkey, binariesData, isLoading, error } from '../stores.js'; import { userSigner, userPubkey, binariesData, isLoading, error } from '../stores.js';
import { fetchBinaries, updateBinaries, rollbackVersion } from '../api.js'; import { fetchBinaries, updateBinaries, rollbackVersion, restartServices, restartService, fetchReleases } from '../api.js';
let version = ''; let version = '';
let urls = { let releaseBaseUrl = '';
'orly': '', let architecture = 'amd64';
'orly-db-badger': '',
'orly-acl-follows': '',
'orly-launcher': '',
};
let updateResult = null; let updateResult = null;
let isUpdating = false; let isUpdating = false;
let launcherUpdated = false;
// Official releases - fetched via backend proxy to avoid CORS
const RELEASES_BASE = 'https://git.nostrdev.com/mleku/next.orly.dev/releases/download';
let availableReleases = [];
let selectedRelease = '';
let loadingReleases = false;
// Category definitions with available options
const categoryDefs = {
launcher: {
label: 'Launcher',
options: [
{ value: 'orly-launcher', label: 'orly-launcher' },
{ value: 'custom', label: 'Custom' }
],
required: true
},
relay: {
label: 'Relay',
options: [
{ value: 'orly', label: 'orly' },
{ value: 'custom', label: 'Custom' }
],
required: true
},
database: {
label: 'Database',
options: [
{ value: 'orly-db-badger', label: 'Badger' },
{ value: 'orly-db-neo4j', label: 'Neo4j' },
{ value: 'custom', label: 'Custom' }
],
required: true
},
acl: {
label: 'ACL',
options: [
{ value: 'none', label: 'None (disabled)' },
{ value: 'orly-acl-follows', label: 'Follows' },
{ value: 'orly-acl-managed', label: 'Managed' },
{ value: 'orly-acl-curation', label: 'Curation' },
{ value: 'custom', label: 'Custom' }
],
required: false
},
sync: {
label: 'Sync',
options: [
{ value: 'none', label: 'None (disabled)' },
{ value: 'orly-sync-negentropy', label: 'Negentropy' },
{ value: 'custom', label: 'Custom' }
],
required: false
}
};
// Current selections for each category
let categories = {
launcher: { selected: 'orly-launcher', customUrl: '', url: '', installing: false, installed: false },
relay: { selected: 'orly', customUrl: '', url: '', installing: false, installed: false },
database: { selected: 'orly-db-badger', customUrl: '', url: '', installing: false, installed: false },
acl: { selected: 'none', customUrl: '', url: '', installing: false, installed: false },
sync: { selected: 'none', customUrl: '', url: '', installing: false, installed: false }
};
onMount(async () => { onMount(async () => {
await loadBinaries(); await loadBinaries();
await loadAvailableReleases();
}); });
async function loadAvailableReleases() {
loadingReleases = true;
try {
const result = await fetchReleases($userSigner, $userPubkey);
if (result.releases) {
availableReleases = result.releases.map(r => ({
tag: r.tag,
message: r.message || ''
}));
}
} catch (e) {
console.error('Failed to fetch releases:', e);
} finally {
loadingReleases = false;
}
}
function handleReleaseSelect() {
if (!selectedRelease) return;
version = selectedRelease;
releaseBaseUrl = `${RELEASES_BASE}/${selectedRelease}`;
updateUrls();
}
async function loadBinaries() { async function loadBinaries() {
$isLoading = true; $isLoading = true;
try { try {
@ -29,12 +115,154 @@
} }
} }
async function handleUpdate() { function generateUrl(binaryName) {
// Filter out empty URLs if (!releaseBaseUrl || !version) return '';
const filteredUrls = {}; const verNum = version.replace(/^v/, '');
for (const [name, url] of Object.entries(urls)) { return `${releaseBaseUrl}/${binaryName}-${verNum}-linux-${architecture}`;
if (url.trim()) { }
filteredUrls[name] = url.trim();
function updateUrls() {
for (const key of Object.keys(categories)) {
const cat = categories[key];
if (cat.selected !== 'none' && cat.selected !== 'custom') {
cat.url = generateUrl(cat.selected);
} else if (cat.selected === 'custom') {
cat.url = cat.customUrl;
} else {
cat.url = '';
}
}
categories = categories;
}
function handleSelectionChange(categoryKey) {
updateUrls();
}
function setReleaseUrl() {
let inputUrl = prompt('Enter release URL (e.g., https://git.mleku.dev/mleku/next.orly.dev/releases/tag/v0.56.1):');
if (!inputUrl) return;
// Normalize the URL
let cleanBase = inputUrl.replace(/\/$/, '');
if (cleanBase.includes('/releases/tag/')) {
cleanBase = cleanBase.replace('/releases/tag/', '/releases/download/');
} else if (!cleanBase.includes('/releases/download/')) {
const ver = version.trim() || 'v0.56.1';
cleanBase = cleanBase + '/releases/download/' + ver;
}
// Extract version from URL
const urlParts = cleanBase.split('/');
const ver = urlParts[urlParts.length - 1];
releaseBaseUrl = cleanBase;
if (!version) {
version = ver;
}
updateUrls();
}
function getBinaryName(categoryKey) {
const cat = categories[categoryKey];
if (cat.selected === 'custom') {
// Try to extract binary name from URL
const urlParts = cat.customUrl.split('/');
const filename = urlParts[urlParts.length - 1];
// Remove version suffix like -0.56.1-linux-amd64
return filename.replace(/-[\d.]+-linux-(amd64|arm64)$/, '') || categoryKey;
}
return cat.selected;
}
function getEffectiveUrl(categoryKey) {
const cat = categories[categoryKey];
if (cat.selected === 'custom') {
return cat.customUrl;
}
return cat.url;
}
async function installCategory(categoryKey) {
const cat = categories[categoryKey];
const url = getEffectiveUrl(categoryKey);
if (!url.trim()) {
$error = `URL is required for ${categoryDefs[categoryKey].label}`;
return;
}
if (!version.trim()) {
$error = 'Version is required';
return;
}
cat.installing = true;
categories = categories;
$error = '';
try {
const binaryName = getBinaryName(categoryKey);
const urls = { [binaryName]: url.trim() };
const result = await updateBinaries($userSigner, $userPubkey, version.trim(), urls);
if (result.success) {
cat.installed = true;
if (categoryKey === 'launcher') {
launcherUpdated = true;
updateResult = {
success: true,
message: `Downloaded ${binaryName}. Click 'Restart Launcher' to apply.`,
downloaded_files: result.downloaded_files
};
} else {
updateResult = {
success: true,
message: `Downloaded ${binaryName}, restarting service...`,
downloaded_files: result.downloaded_files
};
try {
await restartService($userSigner, $userPubkey, binaryName);
updateResult = {
success: true,
message: `${binaryName} installed and restart initiated`,
downloaded_files: result.downloaded_files
};
} catch (restartErr) {
updateResult = {
success: true,
message: `Downloaded ${binaryName}, but restart failed: ${restartErr.message}`,
downloaded_files: result.downloaded_files
};
}
}
await loadBinaries();
}
} catch (e) {
$error = `Failed to install ${categoryDefs[categoryKey].label}: ${e.message}`;
} finally {
cat.installing = false;
categories = categories;
}
}
async function handleInstallAll() {
const urls = {};
let hasLauncher = false;
for (const key of Object.keys(categories)) {
const cat = categories[key];
if (cat.selected !== 'none') {
const url = getEffectiveUrl(key);
if (url.trim()) {
const binaryName = getBinaryName(key);
urls[binaryName] = url.trim();
if (key === 'launcher') hasLauncher = true;
}
} }
} }
@ -43,18 +271,23 @@
return; return;
} }
if (Object.keys(filteredUrls).length === 0) { if (Object.keys(urls).length === 0) {
$error = 'At least one binary URL is required'; $error = 'No binaries selected for installation';
return; return;
} }
isUpdating = true; isUpdating = true;
updateResult = null; updateResult = null;
launcherUpdated = false;
$error = ''; $error = '';
try { try {
updateResult = await updateBinaries($userSigner, $userPubkey, version.trim(), filteredUrls); updateResult = await updateBinaries($userSigner, $userPubkey, version.trim(), urls);
await loadBinaries(); await loadBinaries();
if (hasLauncher && updateResult.success) {
launcherUpdated = true;
}
} catch (e) { } catch (e) {
$error = e.message; $error = e.message;
} finally { } finally {
@ -84,38 +317,28 @@
} }
} }
function setReleaseUrls() { async function handleRestartLauncher() {
// Helper to fill in URLs from a release base if (!confirm('Restart the launcher? This will briefly disconnect you.')) {
let inputUrl = prompt('Enter release URL (e.g., https://git.mleku.dev/mleku/next.orly.dev/releases/tag/v0.56.0):'); return;
if (!inputUrl) return;
// Normalize the URL - convert /releases/tag/ to /releases/download/
let cleanBase = inputUrl.replace(/\/$/, '');
if (cleanBase.includes('/releases/tag/')) {
cleanBase = cleanBase.replace('/releases/tag/', '/releases/download/');
} else if (!cleanBase.includes('/releases/download/')) {
// If it's just a repo URL, construct the download path
const ver = version.trim() || 'v0.56.0';
cleanBase = cleanBase.replace(/\/$/, '') + '/releases/download/' + ver;
} }
try {
const arch = prompt('Enter architecture (amd64 or arm64):', 'amd64'); await restartServices($userSigner, $userPubkey);
if (!arch) return; updateResult = {
success: true,
// Extract version from URL message: 'Launcher restart initiated. The page will reconnect automatically...'
const urlParts = cleanBase.split('/'); };
const ver = urlParts[urlParts.length - 1]; setTimeout(() => {
const verNum = ver.replace('v', ''); window.location.reload();
}, 5000);
urls['orly'] = `${cleanBase}/orly-${verNum}-linux-${arch}`; } catch (e) {
urls['orly-db-badger'] = `${cleanBase}/orly-db-badger-${verNum}-linux-${arch}`; $error = e.message;
urls['orly-acl-follows'] = `${cleanBase}/orly-acl-follows-${verNum}-linux-${arch}`;
urls['orly-launcher'] = `${cleanBase}/orly-launcher-${verNum}-linux-${arch}`;
if (!version.trim()) {
version = ver;
} }
} }
// React to architecture or version changes
$: if (architecture || version) {
updateUrls();
}
</script> </script>
<div class="update-page"> <div class="update-page">
@ -133,6 +356,14 @@
{#if updateResult.downloaded_files?.length} {#if updateResult.downloaded_files?.length}
<br>Downloaded: {updateResult.downloaded_files.join(', ')} <br>Downloaded: {updateResult.downloaded_files.join(', ')}
{/if} {/if}
{#if launcherUpdated}
<div class="launcher-restart">
<strong>Launcher was updated!</strong>
<button class="restart-launcher-btn" on:click={handleRestartLauncher}>
Restart Launcher Now
</button>
</div>
{/if}
</div> </div>
{/if} {/if}
@ -153,44 +384,128 @@
<div class="update-form"> <div class="update-form">
<h3>Install New Version</h3> <h3>Install New Version</h3>
<div class="form-group"> <div class="release-settings">
<label for="version">Version</label> <div class="form-row">
<input <div class="form-group">
type="text" <label for="release-select">Official Release</label>
id="version" <select
bind:value={version} id="release-select"
placeholder="v0.55.11" bind:value={selectedRelease}
disabled={isUpdating} on:change={handleReleaseSelect}
/> disabled={isUpdating || loadingReleases}
</div> >
<option value="">
<div class="form-group"> {loadingReleases ? 'Loading...' : '-- Select release --'}
<div class="url-header"> </option>
<label>Binary URLs</label> {#each availableReleases as release}
<button class="helper-btn" on:click={setReleaseUrls} disabled={isUpdating}> <option value={release.tag}>
Fill from Release {release.tag}{release.message ? ` - ${release.message.slice(0, 40)}` : ''}
</button> </option>
{/each}
</select>
</div>
<div class="form-group">
<label for="arch">Architecture</label>
<select id="arch" bind:value={architecture} disabled={isUpdating}>
<option value="amd64">AMD64 (x86_64)</option>
<option value="arm64">ARM64 (aarch64)</option>
</select>
</div>
</div> </div>
{#each Object.keys(urls) as name} <div class="form-row custom-release-row">
<div class="url-input"> <div class="form-group">
<span class="binary-name">{name}</span> <label for="version">Or Custom Version</label>
<input <input
type="text" type="text"
bind:value={urls[name]} id="version"
placeholder="https://..." bind:value={version}
placeholder="v0.56.1"
disabled={isUpdating} disabled={isUpdating}
/> />
</div> </div>
<div class="form-group">
<label>&nbsp;</label>
<button class="helper-btn fill-btn" on:click={setReleaseUrl} disabled={isUpdating}>
Set Custom URL
</button>
</div>
</div>
{#if releaseBaseUrl}
<div class="release-url-display">
<span class="release-label">Release:</span>
<code>{releaseBaseUrl}</code>
</div>
{/if}
</div>
<div class="categories">
{#each Object.entries(categoryDefs) as [key, def]}
<div class="category-row">
<div class="category-header">
<span class="category-label">{def.label}</span>
{#if !def.required}
<span class="optional-badge">optional</span>
{/if}
</div>
<div class="category-controls">
<select
bind:value={categories[key].selected}
on:change={() => handleSelectionChange(key)}
disabled={isUpdating || categories[key].installing}
>
{#each def.options as opt}
<option value={opt.value}>{opt.label}</option>
{/each}
</select>
{#if categories[key].selected === 'custom'}
<input
type="text"
class="custom-url"
bind:value={categories[key].customUrl}
on:input={() => { categories[key].url = categories[key].customUrl; }}
placeholder="https://... (custom binary URL)"
disabled={isUpdating || categories[key].installing}
/>
{:else if categories[key].selected !== 'none'}
<input
type="text"
class="url-display"
value={categories[key].url}
readonly
placeholder="Set release URL above"
/>
{/if}
{#if categories[key].selected !== 'none'}
<button
class="install-btn"
on:click={() => installCategory(key)}
disabled={isUpdating || categories[key].installing || !getEffectiveUrl(key)}
title="Download and install this component"
>
{#if categories[key].installing}
...
{:else if categories[key].installed}
Done
{:else}
Install
{/if}
</button>
{/if}
</div>
</div>
{/each} {/each}
</div> </div>
<button <button
class="update-btn" class="update-btn"
on:click={handleUpdate} on:click={handleInstallAll}
disabled={isUpdating} disabled={isUpdating}
> >
{isUpdating ? 'Updating...' : 'Download & Install'} {isUpdating ? 'Installing...' : 'Install All Selected'}
</button> </button>
</div> </div>
@ -257,6 +572,29 @@
border: 1px solid #c8e6c9; border: 1px solid #c8e6c9;
} }
.launcher-restart {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #c8e6c9;
display: flex;
align-items: center;
gap: 12px;
}
.restart-launcher-btn {
padding: 8px 16px;
background: #1976d2;
border: none;
color: white;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
}
.restart-launcher-btn:hover {
background: #1565c0;
}
.current-version, .current-version,
.update-form, .update-form,
.versions-list { .versions-list {
@ -304,84 +642,185 @@
cursor: not-allowed; cursor: not-allowed;
} }
.release-settings {
margin-bottom: 24px;
padding-bottom: 20px;
border-bottom: 1px solid var(--border-color);
}
.form-row {
display: flex;
gap: 16px;
align-items: flex-end;
}
.form-group { .form-group {
margin-bottom: 20px; flex: 1;
} }
.form-group > label { .form-group label {
display: block; display: block;
font-size: 0.9rem; font-size: 0.85rem;
color: var(--text-color); color: var(--text-color);
margin-bottom: 8px; margin-bottom: 6px;
font-weight: 500; font-weight: 500;
} }
.form-group input[type="text"] { .form-group input[type="text"],
.form-group select {
width: 100%; width: 100%;
padding: 10px 12px; padding: 8px 12px;
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: 4px; border-radius: 4px;
font-size: 0.95rem; font-size: 0.9rem;
background: var(--bg-color); background: var(--bg-color);
color: var(--text-color); color: var(--text-color);
} }
.form-group input:focus { .form-group input:focus,
.form-group select:focus {
outline: none; outline: none;
border-color: var(--primary); border-color: var(--primary);
} }
.url-header { .helper-btn {
padding: 8px 16px;
font-size: 0.85rem;
background: var(--primary);
border: none;
border-radius: 4px;
color: white;
cursor: pointer;
white-space: nowrap;
}
.helper-btn:hover:not(:disabled) {
opacity: 0.9;
}
.helper-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.fill-btn {
width: 100%;
}
.custom-release-row {
margin-top: 12px;
padding-top: 12px;
border-top: 1px dashed var(--border-color);
}
.release-url-display {
margin-top: 12px;
padding: 8px 12px;
background: var(--bg-color);
border-radius: 4px;
font-size: 0.8rem;
display: flex; display: flex;
justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 12px; gap: 8px;
} }
.url-header label { .release-label {
font-size: 0.9rem; color: var(--muted-color);
}
.release-url-display code {
color: var(--text-color); color: var(--text-color);
font-weight: 500; word-break: break-all;
} }
.helper-btn { .categories {
padding: 4px 12px; display: flex;
font-size: 0.8rem; flex-direction: column;
background: var(--card-bg); gap: 12px;
margin-bottom: 20px;
}
.category-row {
padding: 12px;
background: var(--bg-color);
border-radius: 6px;
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: 4px; }
.category-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.category-label {
font-weight: 600;
color: var(--text-color); color: var(--text-color);
cursor: pointer; font-size: 0.95rem;
} }
.helper-btn:hover:not(:disabled) { .optional-badge {
font-size: 0.7rem;
color: var(--muted-color);
background: var(--border-color); background: var(--border-color);
padding: 2px 6px;
border-radius: 3px;
} }
.url-input { .category-controls {
display: flex; display: flex;
gap: 12px; gap: 8px;
align-items: center; align-items: center;
margin-bottom: 8px;
} }
.binary-name { .category-controls select {
width: 140px; min-width: 140px;
font-family: monospace; padding: 6px 10px;
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 0.85rem; font-size: 0.85rem;
color: var(--muted-color); background: var(--card-bg);
color: var(--text-color);
} }
.url-input input { .category-controls .custom-url,
.category-controls .url-display {
flex: 1; flex: 1;
padding: 8px 12px; padding: 6px 10px;
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: 4px; border-radius: 4px;
font-size: 0.85rem; font-size: 0.8rem;
background: var(--bg-color); font-family: monospace;
background: var(--card-bg);
color: var(--text-color); color: var(--text-color);
} }
.category-controls .url-display {
background: var(--bg-color);
color: var(--muted-color);
}
.install-btn {
padding: 6px 14px;
background: var(--primary);
border: none;
color: white;
border-radius: 4px;
cursor: pointer;
font-size: 0.8rem;
min-width: 70px;
}
.install-btn:hover:not(:disabled) {
opacity: 0.9;
}
.install-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.update-btn { .update-btn {
width: 100%; width: 100%;
padding: 12px; padding: 12px;
@ -439,4 +878,24 @@
border-radius: 4px; border-radius: 4px;
font-size: 0.75rem; font-size: 0.75rem;
} }
@media (max-width: 768px) {
.form-row {
flex-direction: column;
gap: 12px;
}
.category-controls {
flex-wrap: wrap;
}
.category-controls select {
min-width: 100%;
}
.category-controls .custom-url,
.category-controls .url-display {
min-width: 100%;
}
}
</style> </style>

190
go.mod

@ -8,6 +8,7 @@ require (
github.com/alexflint/go-arg v1.6.1 github.com/alexflint/go-arg v1.6.1
github.com/aperturerobotics/go-indexeddb v0.2.3 github.com/aperturerobotics/go-indexeddb v0.2.3
github.com/dgraph-io/badger/v4 v4.8.0 github.com/dgraph-io/badger/v4 v4.8.0
github.com/go-acme/lego/v4 v4.31.0
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3 github.com/gorilla/websocket v1.5.3
github.com/hack-pad/safejs v0.1.1 github.com/hack-pad/safejs v0.1.1
@ -28,66 +29,241 @@ require (
golang.org/x/term v0.38.0 golang.org/x/term v0.38.0
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
google.golang.org/grpc v1.78.0 google.golang.org/grpc v1.78.0
google.golang.org/protobuf v1.36.10 google.golang.org/protobuf v1.36.11
honnef.co/go/tools v0.6.1 honnef.co/go/tools v0.6.1
lol.mleku.dev v1.0.5 lol.mleku.dev v1.0.5
lukechampine.com/frand v1.5.1 lukechampine.com/frand v1.5.1
) )
require ( require (
github.com/BurntSushi/toml v1.5.0 // indirect cloud.google.com/go/auth v0.18.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 // indirect
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 // indirect
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
github.com/Azure/go-autorest/autorest v0.11.30 // indirect
github.com/Azure/go-autorest/autorest/adal v0.9.22 // indirect
github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 // indirect
github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 // indirect
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
github.com/Azure/go-autorest/autorest/to v0.4.1 // indirect
github.com/Azure/go-autorest/logger v0.2.1 // indirect
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
github.com/BurntSushi/toml v1.6.0 // indirect
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 // indirect github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 // indirect
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 // indirect
github.com/alexflint/go-scalar v1.2.0 // indirect github.com/alexflint/go-scalar v1.2.0 // indirect
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13 // indirect
github.com/alibabacloud-go/debug v1.0.1 // indirect
github.com/alibabacloud-go/tea v1.4.0 // indirect
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect
github.com/aliyun/credentials-go v1.4.7 // indirect
github.com/aws/aws-sdk-go-v2 v1.41.0 // indirect
github.com/aws/aws-sdk-go-v2/config v1.32.6 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.19.6 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 // indirect
github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.10 // indirect
github.com/aws/aws-sdk-go-v2/service/route53 v1.62.0 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 // indirect
github.com/aws/smithy-go v1.24.0 // indirect
github.com/aziontech/azionapi-go-sdk v0.144.0 // indirect
github.com/baidubce/bce-sdk-go v0.9.256 // indirect
github.com/benbjohnson/clock v1.3.5 // indirect
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect
github.com/bytedance/sonic v1.13.1 // indirect github.com/bytedance/sonic v1.13.1 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect github.com/cloudwego/base64x v0.1.5 // indirect
github.com/coder/websocket v1.8.12 // indirect github.com/coder/websocket v1.8.12 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
github.com/dgraph-io/ristretto/v2 v2.3.0 // indirect github.com/dgraph-io/ristretto/v2 v2.3.0 // indirect
github.com/dimchansky/utfbom v1.1.1 // indirect
github.com/dnsimple/dnsimple-go/v4 v4.0.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.9.1 // indirect github.com/ebitengine/purego v0.9.1 // indirect
github.com/exoscale/egoscale/v3 v3.1.33 // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/felixge/fgprof v0.9.5 // indirect github.com/felixge/fgprof v0.9.5 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-acme/alidns-20150109/v4 v4.7.0 // indirect
github.com/go-acme/esa-20240910/v2 v2.44.0 // indirect
github.com/go-acme/jdcloud-sdk-go v1.64.0 // indirect
github.com/go-acme/tencentclouddnspod v1.1.25 // indirect
github.com/go-acme/tencentedgdeone v1.1.48 // indirect
github.com/go-errors/errors v1.0.1 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.23.0 // indirect
github.com/go-resty/resty/v2 v2.17.1 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/goccy/go-yaml v1.9.8 // indirect
github.com/gofrs/flock v0.13.0 // indirect
github.com/gofrs/uuid v4.4.0+incompatible // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/btree v1.1.2 // indirect github.com/google/btree v1.1.2 // indirect
github.com/google/flatbuffers v25.9.23+incompatible // indirect github.com/google/flatbuffers v25.9.23+incompatible // indirect
github.com/google/go-querystring v1.2.0 // indirect
github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d // indirect github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
github.com/googleapis/gax-go/v2 v2.16.0 // indirect
github.com/gophercloud/gophercloud v1.14.1 // indirect
github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.182 // indirect
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect
github.com/infobloxopen/infoblox-go-client/v2 v2.10.0 // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/labbsr0x/bindman-dns-webhook v1.0.2 // indirect
github.com/labbsr0x/goh v1.0.1 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/linode/linodego v1.64.0 // indirect
github.com/liquidweb/liquidweb-cli v0.6.9 // indirect
github.com/liquidweb/liquidweb-go v1.6.4 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mailru/easyjson v0.9.0 // indirect github.com/mailru/easyjson v0.9.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.32 // indirect github.com/mattn/go-sqlite3 v1.14.32 // indirect
github.com/miekg/dns v1.1.69 // indirect
github.com/mimuret/golang-iij-dpf v0.9.1 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/namedotcom/go/v4 v4.0.2 // indirect
github.com/nrdcg/auroradns v1.1.0 // indirect
github.com/nrdcg/bunny-go v0.1.0 // indirect
github.com/nrdcg/desec v0.11.1 // indirect
github.com/nrdcg/dnspod-go v0.4.0 // indirect
github.com/nrdcg/freemyip v0.3.0 // indirect
github.com/nrdcg/goacmedns v0.2.0 // indirect
github.com/nrdcg/goinwx v0.12.0 // indirect
github.com/nrdcg/mailinabox v0.3.0 // indirect
github.com/nrdcg/namesilo v0.5.0 // indirect
github.com/nrdcg/nodion v0.1.0 // indirect
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.2 // indirect
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.2 // indirect
github.com/nrdcg/porkbun v0.4.0 // indirect
github.com/nrdcg/vegadns v0.3.0 // indirect
github.com/nzdjb/go-metaname v1.0.0 // indirect
github.com/ovh/go-ovh v1.9.0 // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/peterhellberg/link v1.2.0 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/pquerna/otp v1.5.0 // indirect
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
github.com/regfish/regfish-dnsapi-go v0.1.1 // indirect
github.com/sacloud/api-client-go v0.3.3 // indirect
github.com/sacloud/go-http v0.1.9 // indirect
github.com/sacloud/iaas-api-go v1.23.1 // indirect
github.com/sacloud/packages-go v0.0.12 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 // indirect
github.com/selectel/domains-go v1.1.0 // indirect
github.com/selectel/go-selvpcclient/v4 v4.1.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/softlayer/softlayer-go v1.2.1 // indirect
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect
github.com/sony/gobreaker v1.0.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.7.0 // indirect
github.com/spf13/pflag v1.0.7 // indirect
github.com/spf13/viper v1.18.2 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/templexxx/cpu v0.1.1 // indirect github.com/templexxx/cpu v0.1.1 // indirect
github.com/templexxx/xhex v0.0.0-20200614015412-aed53437177b // indirect github.com/templexxx/xhex v0.0.0-20200614015412-aed53437177b // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.28 // indirect
github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
github.com/transip/gotransip/v6 v6.26.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ultradns/ultradns-go-sdk v1.8.1-20250722213956-faef419 // indirect
github.com/vinyldns/go-vinyldns v0.9.17 // indirect
github.com/volcengine/volc-sdk-golang v1.0.233 // indirect
github.com/vultr/govultr/v3 v3.26.1 // indirect
github.com/yandex-cloud/go-genproto v0.43.0 // indirect
github.com/yandex-cloud/go-sdk/services/dns v0.0.25 // indirect
github.com/yandex-cloud/go-sdk/v2 v2.37.0 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.mongodb.org/mongo-driver v1.13.1 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/ratelimit v0.3.1 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/arch v0.15.0 // indirect golang.org/x/arch v0.15.0 // indirect
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 // indirect golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 // indirect
golang.org/x/exp/typeparams v0.0.0-20251023183803-a4bb9ffd2546 // indirect golang.org/x/exp/typeparams v0.0.0-20251023183803-a4bb9ffd2546 // indirect
golang.org/x/mod v0.31.0 // indirect golang.org/x/mod v0.31.0 // indirect
golang.org/x/net v0.48.0 // indirect golang.org/x/net v0.48.0 // indirect
golang.org/x/oauth2 v0.34.0 // indirect
golang.org/x/sync v0.19.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect golang.org/x/text v0.32.0 // indirect
golang.org/x/time v0.7.0 // indirect golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.40.0 // indirect golang.org/x/tools v0.40.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda // indirect google.golang.org/api v0.259.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/ns1/ns1-go.v2 v2.16.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c // indirect gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c // indirect
p256k1.mleku.dev v1.0.3 // indirect p256k1.mleku.dev v1.0.3 // indirect

1406
go.sum

File diff suppressed because it is too large Load Diff

BIN
libsecp256k1.so

Binary file not shown.

2
pkg/version/version

@ -1 +1 @@
v0.56.1 v0.56.2

Loading…
Cancel
Save