- Add Nostr OAuth2 server with NIP-98 authentication support - Implement OAuth2 authorization and token endpoints - Add .well-known/openid-configuration discovery endpoint - Include Dockerfile for containerized deployment - Add Claude Code release command for version management - Create example configuration file 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
147 lines
3.1 KiB
Go
147 lines
3.1 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
type Config struct {
|
|
Server ServerConfig `yaml:"server"`
|
|
OAuth2 OAuth2Config `yaml:"oauth2"`
|
|
Nostr NostrConfig `yaml:"nostr"`
|
|
}
|
|
|
|
type ServerConfig struct {
|
|
Port int `yaml:"port"`
|
|
Host string `yaml:"host"`
|
|
BaseURL string `yaml:"base_url"`
|
|
}
|
|
|
|
func (s ServerConfig) Address() string {
|
|
host := s.Host
|
|
if host == "" {
|
|
host = "0.0.0.0"
|
|
}
|
|
port := s.Port
|
|
if port == 0 {
|
|
port = 8080
|
|
}
|
|
return fmt.Sprintf("%s:%d", host, port)
|
|
}
|
|
|
|
type OAuth2Config struct {
|
|
Clients []ClientConfig `yaml:"clients"`
|
|
}
|
|
|
|
type ClientConfig struct {
|
|
ClientID string `yaml:"client_id"`
|
|
ClientSecret string `yaml:"client_secret"`
|
|
RedirectURIs []string `yaml:"redirect_uris"`
|
|
}
|
|
|
|
type NostrConfig struct {
|
|
ChallengeTTL time.Duration `yaml:"challenge_ttl"`
|
|
FallbackRelays []string `yaml:"fallback_relays"`
|
|
}
|
|
|
|
func Load(path string) (*Config, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var cfg Config
|
|
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
|
return nil, fmt.Errorf("failed to parse config: %w", err)
|
|
}
|
|
|
|
cfg.setDefaults()
|
|
return &cfg, nil
|
|
}
|
|
|
|
func FromEnv() *Config {
|
|
cfg := &Config{}
|
|
|
|
// Server config
|
|
if port := os.Getenv("PORT"); port != "" {
|
|
cfg.Server.Port, _ = strconv.Atoi(port)
|
|
}
|
|
cfg.Server.Host = os.Getenv("HOST")
|
|
cfg.Server.BaseURL = os.Getenv("BASE_URL")
|
|
|
|
// OAuth2 client config (single client from env)
|
|
clientID := os.Getenv("OAUTH2_CLIENT_ID")
|
|
clientSecret := os.Getenv("OAUTH2_CLIENT_SECRET")
|
|
redirectURIs := os.Getenv("OAUTH2_REDIRECT_URIS")
|
|
|
|
if clientID != "" {
|
|
cfg.OAuth2.Clients = []ClientConfig{{
|
|
ClientID: clientID,
|
|
ClientSecret: clientSecret,
|
|
RedirectURIs: strings.Split(redirectURIs, ","),
|
|
}}
|
|
}
|
|
|
|
// Nostr config
|
|
if ttl := os.Getenv("NOSTR_CHALLENGE_TTL"); ttl != "" {
|
|
cfg.Nostr.ChallengeTTL, _ = time.ParseDuration(ttl)
|
|
}
|
|
if relays := os.Getenv("NOSTR_FALLBACK_RELAYS"); relays != "" {
|
|
cfg.Nostr.FallbackRelays = strings.Split(relays, ",")
|
|
}
|
|
|
|
cfg.setDefaults()
|
|
return cfg
|
|
}
|
|
|
|
// DefaultFallbackRelays are well-known relays that aggregate profile data
|
|
var DefaultFallbackRelays = []string{
|
|
"wss://relay.nostr.band/",
|
|
"wss://nostr.wine/",
|
|
"wss://nos.lol/",
|
|
"wss://relay.primal.net/",
|
|
"wss://purplepag.es/",
|
|
}
|
|
|
|
func (c *Config) setDefaults() {
|
|
if c.Server.Port == 0 {
|
|
c.Server.Port = 8080
|
|
}
|
|
if c.Server.BaseURL == "" {
|
|
c.Server.BaseURL = fmt.Sprintf("http://localhost:%d", c.Server.Port)
|
|
}
|
|
if c.Nostr.ChallengeTTL == 0 {
|
|
c.Nostr.ChallengeTTL = 60 * time.Second
|
|
}
|
|
if len(c.Nostr.FallbackRelays) == 0 {
|
|
c.Nostr.FallbackRelays = DefaultFallbackRelays
|
|
}
|
|
}
|
|
|
|
func (c *Config) GetClient(clientID string) *ClientConfig {
|
|
for i := range c.OAuth2.Clients {
|
|
if c.OAuth2.Clients[i].ClientID == clientID {
|
|
return &c.OAuth2.Clients[i]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Config) ValidateRedirectURI(clientID, redirectURI string) bool {
|
|
client := c.GetClient(clientID)
|
|
if client == nil {
|
|
return false
|
|
}
|
|
for _, uri := range client.RedirectURIs {
|
|
if uri == redirectURI {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|