diff --git a/cmd/cli/main.go b/cmd/cli/main.go index 4440e49..adc09ec 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -5,7 +5,11 @@ import ( "encoding/base64" "encoding/json" "fmt" + "log" + "net" + "net/http" "os" + "os/exec" "strconv" "strings" "time" @@ -83,6 +87,8 @@ func main() { handlePeerID() case "help", "--help", "-h": showHelp() + case "auth": + handleAuth(args) default: fmt.Fprintf(os.Stderr, "Unknown command: %s\n", command) showHelp() @@ -358,6 +364,155 @@ func handlePubSub(args []string) { } } +// handleAuth launches a local webpage to perform wallet signature and obtain an API key. +// Usage: network-cli auth [--gateway ] [--namespace ] [--wallet ] [--plan ] +func handleAuth(args []string) { + // Defaults + gatewayURL := getenvDefault("GATEWAY_URL", "http://localhost:8080") + namespace := getenvDefault("GATEWAY_NAMESPACE", "default") + wallet := "" + plan := "free" + + // Parse simple flags + for i := 0; i < len(args); i++ { + switch args[i] { + case "--gateway": + if i+1 < len(args) { gatewayURL = strings.TrimSpace(args[i+1]); i++ } + case "--namespace": + if i+1 < len(args) { namespace = strings.TrimSpace(args[i+1]); i++ } + case "--wallet": + if i+1 < len(args) { wallet = strings.TrimSpace(args[i+1]); i++ } + case "--plan": + if i+1 < len(args) { plan = strings.TrimSpace(strings.ToLower(args[i+1])); i++ } + } + } + + // Spin up local HTTP server on random port + ln, err := net.Listen("tcp", "localhost:0") + if err != nil { fmt.Fprintf(os.Stderr, "Failed to listen: %v\n", err); os.Exit(1) } + defer ln.Close() + addr := ln.Addr().String() + // Normalize URL host to localhost for consistency with gateway default + parts := strings.Split(addr, ":") + listenURL := "http://localhost:" + parts[len(parts)-1] + "/" + + // Channel to receive API key + type result struct { APIKey string `json:"api_key"`; Namespace string `json:"namespace"` } + resCh := make(chan result, 1) + srv := &http.Server{} + + mux := http.NewServeMux() + // Root serves the HTML page with embedded gateway URL and defaults + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + fmt.Fprintf(w, ` + +DeBros Auth + + + +

Authenticate with Wallet to Get API Key

+

This will create or return an API key for namespace on gateway .

+
+
+
+
+ + +

+
+`, gatewayURL, namespace, wallet, plan)
+    })
+    // Callback to deliver API key back to CLI
+    mux.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) {
+        if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed); return }
+        var payload struct{ APIKey string `json:"api_key"`; Namespace string `json:"namespace"` }
+        if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { w.WriteHeader(http.StatusBadRequest); return }
+        if strings.TrimSpace(payload.APIKey) == "" { w.WriteHeader(http.StatusBadRequest); return }
+        select { case resCh <- result{APIKey: payload.APIKey, Namespace: payload.Namespace}: default: }
+        _, _ = w.Write([]byte("ok"))
+        go func(){ time.Sleep(500*time.Millisecond); _ = srv.Close() }()
+    })
+    srv.Handler = mux
+
+    // Open browser
+    url := listenURL
+    go func(){
+        // Try to open in default browser
+        _ = openBrowser(url)
+    }()
+
+    // Serve and wait for result or timeout
+    go func(){ _ = srv.Serve(ln) }()
+    fmt.Printf("🌐 Please complete authentication in your browser: %s\n", url)
+    select {
+    case r := <-resCh:
+        fmt.Printf("✅ API Key issued for namespace '%s'\n", r.Namespace)
+        fmt.Printf("%s\n", r.APIKey)
+    case <-time.After(5 * time.Minute):
+        fmt.Fprintf(os.Stderr, "Timed out waiting for wallet signature.\n")
+        _ = srv.Close()
+        os.Exit(1)
+    }
+}
+
+func openBrowser(target string) error {
+    cmds := [][]string{
+        {"xdg-open", target},
+        {"open", target},
+        {"cmd", "/c", "start", target},
+    }
+    for _, c := range cmds {
+        cmd := exec.Command(c[0], c[1:]...)
+        if err := cmd.Start(); err == nil { return nil }
+    }
+    log.Printf("Please open %s manually", target)
+    return nil
+}
+
+// getenvDefault returns env var or default if empty/undefined.
+func getenvDefault(key, def string) string {
+    if v := strings.TrimSpace(os.Getenv(key)); v != "" {
+        return v
+    }
+    return def
+}
+
 func handleConnect(peerAddr string) {
 	client, err := createClient()
 	if err != nil {
@@ -525,7 +680,8 @@ func showHelp() {
 	fmt.Printf("  pubsub publish   - Publish message\n")
 	fmt.Printf("  pubsub subscribe  [duration] - Subscribe to topic\n")
 	fmt.Printf("  pubsub topics             - List topics\n")
-	fmt.Printf("  connect     - Connect to peer\n")
+    fmt.Printf("  connect     - Connect to peer\n")
+    fmt.Printf("  auth [--gateway URL] [--namespace NS] [--wallet 0x..] [--plan free|premium] - Obtain API key via wallet signature\n")
 	fmt.Printf("  help                      - Show this help\n\n")
 	fmt.Printf("Global Flags:\n")
 	fmt.Printf("  -b, --bootstrap     - Bootstrap peer address (default: /ip4/127.0.0.1/tcp/4001)\n")
diff --git a/cmd/gateway/config.go b/cmd/gateway/config.go
index 82d9f3d..ecaf3a6 100644
--- a/cmd/gateway/config.go
+++ b/cmd/gateway/config.go
@@ -42,8 +42,7 @@ func parseGatewayConfig(logger *logging.ColoredLogger) *gateway.Config {
 	addr := flag.String("addr", getEnvDefault("GATEWAY_ADDR", ":8080"), "HTTP listen address (e.g., :8080)")
 	ns := flag.String("namespace", getEnvDefault("GATEWAY_NAMESPACE", "default"), "Client namespace for scoping resources")
 	peers := flag.String("bootstrap-peers", getEnvDefault("GATEWAY_BOOTSTRAP_PEERS", ""), "Comma-separated bootstrap peers for network client")
-	requireAuth := flag.Bool("require-auth", getEnvBoolDefault("GATEWAY_REQUIRE_AUTH", false), "Require API key authentication for requests")
-	apiKeysStr := flag.String("api-keys", getEnvDefault("GATEWAY_API_KEYS", ""), "Comma-separated API keys, optionally as key:namespace")
+    requireAuth := flag.Bool("require-auth", getEnvBoolDefault("GATEWAY_REQUIRE_AUTH", false), "Require API key authentication for requests")
 
 	// Do not call flag.Parse() elsewhere to avoid double-parsing
 	flag.Parse()
@@ -59,32 +58,11 @@ func parseGatewayConfig(logger *logging.ColoredLogger) *gateway.Config {
 		}
 	}
 
-	apiKeys := make(map[string]string)
-	if s := strings.TrimSpace(*apiKeysStr); s != "" {
-		tokens := strings.Split(s, ",")
-		for _, tok := range tokens {
-			tok = strings.TrimSpace(tok)
-			if tok == "" {
-				continue
-			}
-			key := tok
-			nsOverride := ""
-			if i := strings.Index(tok, ":"); i != -1 {
-				key = strings.TrimSpace(tok[:i])
-				nsOverride = strings.TrimSpace(tok[i+1:])
-			}
-			if key != "" {
-				apiKeys[key] = nsOverride
-			}
-		}
-	}
-
 	logger.ComponentInfo(logging.ComponentGeneral, "Loaded gateway configuration",
 		zap.String("addr", *addr),
 		zap.String("namespace", *ns),
 		zap.Int("bootstrap_peer_count", len(bootstrap)),
-		zap.Bool("require_auth", *requireAuth),
-		zap.Int("api_key_count", len(apiKeys)),
+        zap.Bool("require_auth", *requireAuth),
 	)
 
 	return &gateway.Config{
@@ -92,6 +70,5 @@ func parseGatewayConfig(logger *logging.ColoredLogger) *gateway.Config {
 		ClientNamespace: *ns,
 		BootstrapPeers:  bootstrap,
 		RequireAuth:     *requireAuth,
-		APIKeys:         apiKeys,
 	}
 }
diff --git a/cmd/gateway/main.go b/cmd/gateway/main.go
index 7e0d670..4d35c78 100644
--- a/cmd/gateway/main.go
+++ b/cmd/gateway/main.go
@@ -2,6 +2,7 @@ package main
 
 import (
 	"context"
+	"net"
 	"net/http"
 	"os"
 	"os/signal"
@@ -27,42 +28,75 @@ func main() {
 	// Load gateway config (flags/env)
 	cfg := parseGatewayConfig(logger)
 
+	logger.ComponentInfo(logging.ComponentGeneral, "Starting gateway initialization...")
+
 	// Initialize gateway (connect client, prepare routes)
-	g, err := gateway.New(logger, cfg)
+	gw, err := gateway.New(logger, cfg)
 	if err != nil {
 		logger.ComponentError(logging.ComponentGeneral, "failed to initialize gateway", zap.Error(err))
 		os.Exit(1)
 	}
-	defer g.Close()
+	defer gw.Close()
+
+	logger.ComponentInfo(logging.ComponentGeneral, "Gateway initialization completed successfully")
+
+	logger.ComponentInfo(logging.ComponentGeneral, "Creating HTTP server and routes...")
 
 	server := &http.Server{
 		Addr:    cfg.ListenAddr,
-		Handler: g.Routes(),
+		Handler: gw.Routes(),
 	}
 
-	// Start server
+	// Try to bind listener explicitly so binding failures are visible immediately.
+	logger.ComponentInfo(logging.ComponentGeneral, "Gateway HTTP server starting",
+		zap.String("addr", cfg.ListenAddr),
+		zap.String("namespace", cfg.ClientNamespace),
+		zap.Int("bootstrap_peer_count", len(cfg.BootstrapPeers)),
+	)
+
+	logger.ComponentInfo(logging.ComponentGeneral, "Attempting to bind HTTP listener...")
+
+	ln, err := net.Listen("tcp", cfg.ListenAddr)
+	if err != nil {
+		logger.ComponentError(logging.ComponentGeneral, "failed to bind HTTP listen address", zap.Error(err))
+		// exit because server cannot function without a listener
+		os.Exit(1)
+	}
+	logger.ComponentInfo(logging.ComponentGeneral, "HTTP listener bound", zap.String("listen_addr", ln.Addr().String()))
+
+	// Serve in a goroutine so we can handle graceful shutdown on signals.
+	serveErrCh := make(chan error, 1)
 	go func() {
-		logger.ComponentInfo(logging.ComponentGeneral, "Gateway HTTP server starting",
-			zap.String("addr", cfg.ListenAddr),
-			zap.String("namespace", cfg.ClientNamespace),
-			zap.Int("bootstrap_peer_count", len(cfg.BootstrapPeers)),
-		)
-		if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
-			logger.ComponentError(logging.ComponentGeneral, "HTTP server error", zap.Error(err))
-			os.Exit(1)
+		if err := server.Serve(ln); err != nil && err != http.ErrServerClosed {
+			serveErrCh <- err
+			return
 		}
+		serveErrCh <- nil
 	}()
 
-	// Graceful shutdown
+	// Wait for termination signal or server error
 	quit := make(chan os.Signal, 1)
 	signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
-	<-quit
+
+	select {
+	case sig := <-quit:
+		logger.ComponentInfo(logging.ComponentGeneral, "shutdown signal received", zap.String("signal", sig.String()))
+	case err := <-serveErrCh:
+		if err != nil {
+			logger.ComponentError(logging.ComponentGeneral, "HTTP server error", zap.Error(err))
+			// continue to shutdown path so we close resources cleanly
+		} else {
+			logger.ComponentInfo(logging.ComponentGeneral, "HTTP server exited normally")
+		}
+	}
+
 	logger.ComponentInfo(logging.ComponentGeneral, "Shutting down gateway HTTP server...")
 
 	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
 	defer cancel()
 	if err := server.Shutdown(ctx); err != nil {
 		logger.ComponentError(logging.ComponentGeneral, "HTTP server shutdown error", zap.Error(err))
+	} else {
+		logger.ComponentInfo(logging.ComponentGeneral, "Gateway shutdown complete")
 	}
-	logger.ComponentInfo(logging.ComponentGeneral, "Gateway shutdown complete")
 }
diff --git a/migrations/003_wallet_api_keys.sql b/migrations/003_wallet_api_keys.sql
new file mode 100644
index 0000000..6c9e725
--- /dev/null
+++ b/migrations/003_wallet_api_keys.sql
@@ -0,0 +1,21 @@
+-- DeBros Gateway - Wallet to API Key linkage (Phase 3)
+-- Ensures one API key per (namespace, wallet) and enables lookup
+
+BEGIN;
+
+CREATE TABLE IF NOT EXISTS wallet_api_keys (
+    id            INTEGER PRIMARY KEY AUTOINCREMENT,
+    namespace_id  INTEGER NOT NULL,
+    wallet        TEXT NOT NULL,
+    api_key_id    INTEGER NOT NULL,
+    created_at    TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    UNIQUE(namespace_id, wallet),
+    FOREIGN KEY(namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE,
+    FOREIGN KEY(api_key_id) REFERENCES api_keys(id) ON DELETE CASCADE
+);
+
+CREATE INDEX IF NOT EXISTS idx_wallet_api_keys_ns ON wallet_api_keys(namespace_id);
+
+INSERT OR IGNORE INTO schema_migrations(version) VALUES (3);
+
+COMMIT;
diff --git a/pkg/client/client.go b/pkg/client/client.go
index eacdbf7..b4d3671 100644
--- a/pkg/client/client.go
+++ b/pkg/client/client.go
@@ -124,11 +124,6 @@ func (c *Client) Connect() error {
 		return nil
 	}
 
-	// Enforce credentials are present
-	if c.config == nil || (strings.TrimSpace(c.config.APIKey) == "" && strings.TrimSpace(c.config.JWT) == "") {
-		return fmt.Errorf("access denied: API key or JWT required")
-	}
-
 	// Derive and set namespace from provided credentials
 	ns, err := c.deriveNamespace()
 	if err != nil {
@@ -171,6 +166,8 @@ func (c *Client) Connect() error {
 		zap.Strings("listen_addrs", addrStrs),
 	)
 
+	c.logger.Info("Creating GossipSub...")
+
 	// Create LibP2P GossipSub with PeerExchange enabled (gossip-based peer exchange).
 	// Peer exchange helps propagate peer addresses via pubsub gossip and is enabled
 	// globally so discovery works without Anchat-specific branches.
@@ -183,17 +180,39 @@ func (c *Client) Connect() error {
 		return fmt.Errorf("failed to create pubsub: %w", err)
 	}
 	c.libp2pPS = ps
+	c.logger.Info("GossipSub created successfully")
 
-	// Create pubsub bridge once and store it
-	adapter := pubsub.NewClientAdapter(c.libp2pPS, c.getAppNamespace())
+	c.logger.Info("Creating pubsub bridge...")
+
+	c.logger.Info("Getting app namespace for pubsub...")
+	// Access namespace directly to avoid deadlock (we already hold c.mu.Lock())
+	var namespace string
+	if c.resolvedNamespace != "" {
+		namespace = c.resolvedNamespace
+	} else {
+		namespace = c.config.AppName
+	}
+	c.logger.Info("App namespace retrieved", zap.String("namespace", namespace))
+
+	c.logger.Info("Calling pubsub.NewClientAdapter...")
+	adapter := pubsub.NewClientAdapter(c.libp2pPS, namespace)
+	c.logger.Info("pubsub.NewClientAdapter completed successfully")
+
+	c.logger.Info("Creating pubSubBridge...")
 	c.pubsub = &pubSubBridge{client: c, adapter: adapter}
+	c.logger.Info("Pubsub bridge created successfully")
 
-	// Create storage client with the host
-	storageClient := storage.NewClient(h, c.getAppNamespace(), c.logger)
+	c.logger.Info("Creating storage client...")
+
+	// Create storage client with the host (use namespace directly to avoid deadlock)
+	storageClient := storage.NewClient(h, namespace, c.logger)
 	c.storage = &StorageClientImpl{
 		client:        c,
 		storageClient: storageClient,
 	}
+	c.logger.Info("Storage client created successfully")
+
+	c.logger.Info("Starting bootstrap peer connections...")
 
 	// Connect to bootstrap peers FIRST
 	ctx, cancel := context.WithTimeout(context.Background(), c.config.ConnectTimeout)
@@ -201,6 +220,7 @@ func (c *Client) Connect() error {
 
 	bootstrapPeersConnected := 0
 	for _, bootstrapAddr := range c.config.BootstrapPeers {
+		c.logger.Info("Attempting to connect to bootstrap peer", zap.String("addr", bootstrapAddr))
 		if err := c.connectToBootstrap(ctx, bootstrapAddr); err != nil {
 			c.logger.Warn("Failed to connect to bootstrap peer",
 				zap.String("addr", bootstrapAddr),
@@ -208,12 +228,17 @@ func (c *Client) Connect() error {
 			continue
 		}
 		bootstrapPeersConnected++
+		c.logger.Info("Successfully connected to bootstrap peer", zap.String("addr", bootstrapAddr))
 	}
 
 	if bootstrapPeersConnected == 0 {
 		c.logger.Warn("No bootstrap peers connected, continuing anyway")
+	} else {
+		c.logger.Info("Bootstrap peer connections completed", zap.Int("connected_count", bootstrapPeersConnected))
 	}
 
+	c.logger.Info("Adding bootstrap peers to peerstore...")
+
 	// Add bootstrap peers to peerstore so we can connect to them later
 	for _, bootstrapAddr := range c.config.BootstrapPeers {
 		if ma, err := multiaddr.NewMultiaddr(bootstrapAddr); err == nil {
@@ -224,6 +249,9 @@ func (c *Client) Connect() error {
 			}
 		}
 	}
+	c.logger.Info("Bootstrap peers added to peerstore")
+
+	c.logger.Info("Starting connection monitoring...")
 
 	// Client is a lightweight P2P participant - no discovery needed
 	// We only connect to known bootstrap peers and let nodes handle discovery
@@ -231,10 +259,14 @@ func (c *Client) Connect() error {
 
 	// Start minimal connection monitoring
 	c.startConnectionMonitoring()
+	c.logger.Info("Connection monitoring started")
+
+	c.logger.Info("Setting connected state...")
 
 	c.connected = true
+	c.logger.Info("Connected state set to true")
 
-	c.logger.Info("Client connected", zap.String("namespace", c.getAppNamespace()))
+	c.logger.Info("Client connected", zap.String("namespace", namespace))
 
 	return nil
 }
diff --git a/pkg/client/interface.go b/pkg/client/interface.go
index 834334c..e09244b 100644
--- a/pkg/client/interface.go
+++ b/pkg/client/interface.go
@@ -129,8 +129,8 @@ type ClientConfig struct {
 	RetryAttempts     int           `json:"retry_attempts"`
 	RetryDelay        time.Duration `json:"retry_delay"`
 	QuietMode         bool          `json:"quiet_mode"` // Suppress debug/info logs
-	APIKey            string        `json:"api_key"`   // API key for gateway auth
-	JWT               string        `json:"jwt"`       // Optional JWT bearer token
+	APIKey            string        `json:"api_key"`    // API key for gateway auth
+	JWT               string        `json:"jwt"`        // Optional JWT bearer token
 }
 
 // DefaultClientConfig returns a default client configuration
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 7f6b4e4..4e8d822 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -110,7 +110,7 @@ func DefaultConfig() *Config {
 		},
 		Discovery: DiscoveryConfig{
 			BootstrapPeers: []string{
-				"/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWRjaa3STPr2PDVai1eqZ2KEc942sbJpxcd42qSAc1P9A2",
+				"/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWGqqR8bxgmYsYrGYMKnUWwZUCpioLmA3H37ggRDnAiFa7",
 			},
 			BootstrapPort:     4001,             // Default LibP2P port
 			DiscoveryInterval: time.Second * 15, // Back to 15 seconds for testing
diff --git a/pkg/gateway/auth_handlers.go b/pkg/gateway/auth_handlers.go
index b534bf3..bb6333c 100644
--- a/pkg/gateway/auth_handlers.go
+++ b/pkg/gateway/auth_handlers.go
@@ -10,7 +10,7 @@ import (
 	"strings"
 	"time"
 
-	"git.debros.io/DeBros/network/pkg/storage"
+    "git.debros.io/DeBros/network/pkg/storage"
 	ethcrypto "github.com/ethereum/go-ethereum/crypto"
 )
 
@@ -211,8 +211,8 @@ func (g *Gateway) verifyHandler(w http.ResponseWriter, r *http.Request) {
 		writeError(w, http.StatusServiceUnavailable, "signing key unavailable")
 		return
 	}
-	// Issue access token (15m) and a refresh token (30d)
-	token, expUnix, err := g.generateJWT(ns, req.Wallet, 15*time.Minute)
+    // Issue access token (15m) and a refresh token (30d)
+    token, expUnix, err := g.generateJWT(ns, req.Wallet, 15*time.Minute)
 	if err != nil {
 		writeError(w, http.StatusInternalServerError, err.Error())
 		return
@@ -240,6 +240,176 @@ func (g *Gateway) verifyHandler(w http.ResponseWriter, r *http.Request) {
 	})
 }
 
+// issueAPIKeyHandler creates or returns an API key for a verified wallet in a namespace.
+// Requires: POST { wallet, nonce, signature, namespace }
+// Behavior:
+//  - Validates nonce and signature like verifyHandler
+//  - Ensures namespace exists
+//  - If an API key already exists for (namespace, wallet), returns it; else creates one
+//  - Records namespace ownership mapping for the wallet and api_key
+func (g *Gateway) issueAPIKeyHandler(w http.ResponseWriter, r *http.Request) {
+    if g.client == nil {
+        writeError(w, http.StatusServiceUnavailable, "client not initialized")
+        return
+    }
+    if r.Method != http.MethodPost {
+        writeError(w, http.StatusMethodNotAllowed, "method not allowed")
+        return
+    }
+    var req struct {
+        Wallet    string `json:"wallet"`
+        Nonce     string `json:"nonce"`
+        Signature string `json:"signature"`
+        Namespace string `json:"namespace"`
+        Plan      string `json:"plan"`
+    }
+    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+        writeError(w, http.StatusBadRequest, "invalid json body")
+        return
+    }
+    if strings.TrimSpace(req.Wallet) == "" || strings.TrimSpace(req.Nonce) == "" || strings.TrimSpace(req.Signature) == "" {
+        writeError(w, http.StatusBadRequest, "wallet, nonce and signature are required")
+        return
+    }
+    ns := strings.TrimSpace(req.Namespace)
+    if ns == "" {
+        ns = strings.TrimSpace(g.cfg.ClientNamespace)
+        if ns == "" { ns = "default" }
+    }
+    ctx := r.Context()
+    db := g.client.Database()
+    // Resolve namespace id
+    nsID, err := g.resolveNamespaceID(ctx, ns)
+    if err != nil {
+        writeError(w, http.StatusInternalServerError, err.Error())
+        return
+    }
+    // Validate nonce exists and not used/expired
+    q := "SELECT id FROM nonces WHERE namespace_id = ? AND wallet = ? AND nonce = ? AND used_at IS NULL AND (expires_at IS NULL OR expires_at > datetime('now')) LIMIT 1"
+    nres, err := db.Query(ctx, q, nsID, req.Wallet, req.Nonce)
+    if err != nil || nres == nil || nres.Count == 0 {
+        writeError(w, http.StatusBadRequest, "invalid or expired nonce")
+        return
+    }
+    nonceID := nres.Rows[0][0]
+    // Verify signature like verifyHandler
+    msg := []byte(req.Nonce)
+    prefix := []byte("\x19Ethereum Signed Message:\n" + strconv.Itoa(len(msg)))
+    hash := ethcrypto.Keccak256(prefix, msg)
+    sigHex := strings.TrimSpace(req.Signature)
+    if strings.HasPrefix(sigHex, "0x") || strings.HasPrefix(sigHex, "0X") { sigHex = sigHex[2:] }
+    sig, err := hex.DecodeString(sigHex)
+    if err != nil || len(sig) != 65 {
+        writeError(w, http.StatusBadRequest, "invalid signature format")
+        return
+    }
+    if sig[64] >= 27 { sig[64] -= 27 }
+    pub, err := ethcrypto.SigToPub(hash, sig)
+    if err != nil {
+        writeError(w, http.StatusUnauthorized, "signature recovery failed")
+        return
+    }
+    addr := ethcrypto.PubkeyToAddress(*pub).Hex()
+    want := strings.ToLower(strings.TrimPrefix(strings.TrimPrefix(req.Wallet, "0x"), "0X"))
+    got := strings.ToLower(strings.TrimPrefix(strings.TrimPrefix(addr, "0x"), "0X"))
+    if got != want {
+        writeError(w, http.StatusUnauthorized, "signature does not match wallet")
+        return
+    }
+    // Mark nonce used
+    if _, err := db.Query(ctx, "UPDATE nonces SET used_at = datetime('now') WHERE id = ?", nonceID); err != nil {
+        writeError(w, http.StatusInternalServerError, err.Error())
+        return
+    }
+    // Check if api key exists for (namespace, wallet) via linkage table
+    var apiKey string
+    r1, err := db.Query(ctx, "SELECT api_keys.key FROM wallet_api_keys JOIN api_keys ON wallet_api_keys.api_key_id = api_keys.id WHERE wallet_api_keys.namespace_id = ? AND LOWER(wallet_api_keys.wallet) = LOWER(?) LIMIT 1", nsID, req.Wallet)
+    if err == nil && r1 != nil && r1.Count > 0 && len(r1.Rows) > 0 && len(r1.Rows[0]) > 0 {
+        if s, ok := r1.Rows[0][0].(string); ok { apiKey = s } else { b, _ := json.Marshal(r1.Rows[0][0]); _ = json.Unmarshal(b, &apiKey) }
+    }
+    if strings.TrimSpace(apiKey) == "" {
+        // Create new API key with format ak_:
+        buf := make([]byte, 18)
+        if _, err := rand.Read(buf); err != nil {
+            writeError(w, http.StatusInternalServerError, "failed to generate api key")
+            return
+        }
+        apiKey = "ak_" + base64.RawURLEncoding.EncodeToString(buf) + ":" + ns
+        if _, err := db.Query(ctx, "INSERT INTO api_keys(key, name, namespace_id) VALUES (?, ?, ?)", apiKey, "", nsID); err != nil {
+            writeError(w, http.StatusInternalServerError, err.Error())
+            return
+        }
+        // Create linkage
+        // Find api_key id
+        rid, err := db.Query(ctx, "SELECT id FROM api_keys WHERE key = ? LIMIT 1", apiKey)
+        if err == nil && rid != nil && rid.Count > 0 && len(rid.Rows) > 0 && len(rid.Rows[0]) > 0 {
+            apiKeyID := rid.Rows[0][0]
+            _, _ = db.Query(ctx, "INSERT OR IGNORE INTO wallet_api_keys(namespace_id, wallet, api_key_id) VALUES (?, ?, ?)", nsID, strings.ToLower(req.Wallet), apiKeyID)
+        }
+    }
+    // Record ownerships (best-effort)
+    _, _ = db.Query(ctx, "INSERT OR IGNORE INTO namespace_ownership(namespace_id, owner_type, owner_id) VALUES (?, 'api_key', ?)", nsID, apiKey)
+    _, _ = db.Query(ctx, "INSERT OR IGNORE INTO namespace_ownership(namespace_id, owner_type, owner_id) VALUES (?, 'wallet', ?)", nsID, req.Wallet)
+
+    writeJSON(w, http.StatusOK, map[string]any{
+        "api_key":   apiKey,
+        "namespace": ns,
+        "plan":      func() string { if strings.TrimSpace(req.Plan) == "" { return "free" } else { return req.Plan } }(),
+        "wallet":    strings.ToLower(strings.TrimPrefix(strings.TrimPrefix(req.Wallet, "0x"), "0X")),
+    })
+}
+
+// apiKeyToJWTHandler issues a short-lived JWT for use with the gateway from a valid API key.
+// Requires Authorization header with API key (Bearer or ApiKey or X-API-Key header).
+// Returns a JWT bound to the namespace derived from the API key record.
+func (g *Gateway) apiKeyToJWTHandler(w http.ResponseWriter, r *http.Request) {
+    if g.client == nil {
+        writeError(w, http.StatusServiceUnavailable, "client not initialized")
+        return
+    }
+    if r.Method != http.MethodPost {
+        writeError(w, http.StatusMethodNotAllowed, "method not allowed")
+        return
+    }
+    key := extractAPIKey(r)
+    if strings.TrimSpace(key) == "" {
+        writeError(w, http.StatusUnauthorized, "missing API key")
+        return
+    }
+    // Validate and get namespace
+    db := g.client.Database()
+    ctx := r.Context()
+    q := "SELECT namespaces.name FROM api_keys JOIN namespaces ON api_keys.namespace_id = namespaces.id WHERE api_keys.key = ? LIMIT 1"
+    res, err := db.Query(ctx, q, key)
+    if err != nil || res == nil || res.Count == 0 || len(res.Rows) == 0 || len(res.Rows[0]) == 0 {
+        writeError(w, http.StatusUnauthorized, "invalid API key")
+        return
+    }
+    var ns string
+    if s, ok := res.Rows[0][0].(string); ok { ns = s } else { b, _ := json.Marshal(res.Rows[0][0]); _ = json.Unmarshal(b, &ns) }
+    ns = strings.TrimSpace(ns)
+    if ns == "" {
+        writeError(w, http.StatusUnauthorized, "invalid API key")
+        return
+    }
+    if g.signingKey == nil {
+        writeError(w, http.StatusServiceUnavailable, "signing key unavailable")
+        return
+    }
+    // Subject is the API key string for now
+    token, expUnix, err := g.generateJWT(ns, key, 15*time.Minute)
+    if err != nil {
+        writeError(w, http.StatusInternalServerError, err.Error())
+        return
+    }
+    writeJSON(w, http.StatusOK, map[string]any{
+        "access_token": token,
+        "token_type":   "Bearer",
+        "expires_in":   int(expUnix - time.Now().Unix()),
+        "namespace":    ns,
+    })
+}
+
 func (g *Gateway) registerHandler(w http.ResponseWriter, r *http.Request) {
 	if g.client == nil {
 		writeError(w, http.StatusServiceUnavailable, "client not initialized")
diff --git a/pkg/gateway/db_helpers.go b/pkg/gateway/db_helpers.go
index 62d5d72..2c2852a 100644
--- a/pkg/gateway/db_helpers.go
+++ b/pkg/gateway/db_helpers.go
@@ -1,8 +1,7 @@
 package gateway
 
 import (
-	"context"
-	"strings"
+    "context"
 )
 
 func (g *Gateway) resolveNamespaceID(ctx context.Context, ns string) (interface{}, error) {
@@ -17,40 +16,4 @@ func (g *Gateway) resolveNamespaceID(ctx context.Context, ns string) (interface{
 	return res.Rows[0][0], nil
 }
 
-func (g *Gateway) seedConfiguredAPIKeys(ctx context.Context) error {
-	db := g.client.Database()
-	for key, nsOverride := range g.cfg.APIKeys {
-		ns := strings.TrimSpace(nsOverride)
-		if ns == "" {
-			ns = strings.TrimSpace(g.cfg.ClientNamespace)
-			if ns == "" {
-				ns = "default"
-			}
-		}
-
-		// Ensure namespace exists
-		if _, err := db.Query(ctx, "INSERT OR IGNORE INTO namespaces(name) VALUES (?)", ns); err != nil {
-			return err
-		}
-		// Lookup namespace id
-		nres, err := db.Query(ctx, "SELECT id FROM namespaces WHERE name = ? LIMIT 1", ns)
-		if err != nil {
-			return err
-		}
-		var nsID interface{}
-		if nres != nil && nres.Count > 0 && len(nres.Rows) > 0 && len(nres.Rows[0]) > 0 {
-			nsID = nres.Rows[0][0]
-		} else {
-			// Should not happen, but guard
-			continue
-		}
-
-		// Upsert API key
-		if _, err := db.Query(ctx, "INSERT OR IGNORE INTO api_keys(key, name, namespace_id) VALUES (?, ?, ?)", key, "", nsID); err != nil {
-			return err
-		}
-		// Record namespace ownership for API key (best-effort)
-		_, _ = db.Query(ctx, "INSERT OR IGNORE INTO namespace_ownership(namespace_id, owner_type, owner_id) VALUES (?, 'api_key', ?)", nsID, key)
-	}
-	return nil
-}
+// Deprecated: seeding API keys from config is removed.
diff --git a/pkg/gateway/gateway.go b/pkg/gateway/gateway.go
index b37d9c1..e9fdf68 100644
--- a/pkg/gateway/gateway.go
+++ b/pkg/gateway/gateway.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"crypto/rand"
 	"crypto/rsa"
+	"net/http"
 	"strconv"
 	"time"
 
@@ -18,32 +19,35 @@ type Config struct {
 	ClientNamespace string
 	BootstrapPeers  []string
 	RequireAuth     bool
-	APIKeys         map[string]string // key -> optional namespace override
 }
 
 type Gateway struct {
-	logger    *logging.ColoredLogger
-	cfg       *Config
-	client    client.NetworkClient
-	startedAt time.Time
+	logger     *logging.ColoredLogger
+	cfg        *Config
+	client     client.NetworkClient
+	startedAt  time.Time
 	signingKey *rsa.PrivateKey
 	keyID      string
 }
 
 // New creates and initializes a new Gateway instance
 func New(logger *logging.ColoredLogger, cfg *Config) (*Gateway, error) {
+	logger.ComponentInfo(logging.ComponentGeneral, "Building client config...")
+
 	// Build client config from gateway cfg
 	cliCfg := client.DefaultClientConfig(cfg.ClientNamespace)
 	if len(cfg.BootstrapPeers) > 0 {
 		cliCfg.BootstrapPeers = cfg.BootstrapPeers
 	}
 
+	logger.ComponentInfo(logging.ComponentGeneral, "Creating network client...")
 	c, err := client.NewClient(cliCfg)
 	if err != nil {
 		logger.ComponentError(logging.ComponentClient, "failed to create network client", zap.Error(err))
 		return nil, err
 	}
 
+	logger.ComponentInfo(logging.ComponentGeneral, "Connecting network client...")
 	if err := c.Connect(); err != nil {
 		logger.ComponentError(logging.ComponentClient, "failed to connect network client", zap.Error(err))
 		return nil, err
@@ -54,6 +58,7 @@ func New(logger *logging.ColoredLogger, cfg *Config) (*Gateway, error) {
 		zap.Int("bootstrap_peer_count", len(cliCfg.BootstrapPeers)),
 	)
 
+	logger.ComponentInfo(logging.ComponentGeneral, "Creating gateway instance...")
 	gw := &Gateway{
 		logger:    logger,
 		cfg:       cfg,
@@ -61,20 +66,67 @@ func New(logger *logging.ColoredLogger, cfg *Config) (*Gateway, error) {
 		startedAt: time.Now(),
 	}
 
+	logger.ComponentInfo(logging.ComponentGeneral, "Generating RSA signing key...")
 	// Generate local RSA signing key for JWKS/JWT (ephemeral for now)
 	if key, err := rsa.GenerateKey(rand.Reader, 2048); err == nil {
 		gw.signingKey = key
 		gw.keyID = "gw-" + strconv.FormatInt(time.Now().Unix(), 10)
+		logger.ComponentInfo(logging.ComponentGeneral, "RSA key generated successfully")
 	} else {
 		logger.ComponentWarn(logging.ComponentGeneral, "failed to generate RSA key; jwks will be empty", zap.Error(err))
 	}
 
-	// Seed configured API keys into DB (best-effort)
-	_ = gw.seedConfiguredAPIKeys(context.Background())
+	logger.ComponentInfo(logging.ComponentGeneral, "Starting database migrations goroutine...")
+	// Non-blocking DB migrations: probe RQLite; if reachable, apply migrations asynchronously
+	go func() {
+		if gw.probeRQLiteReachable(3 * time.Second) {
+			if err := gw.applyMigrations(context.Background()); err != nil {
+				if err == errNoMigrationsFound {
+					if err2 := gw.applyAutoMigrations(context.Background()); err2 != nil {
+						logger.ComponentWarn(logging.ComponentDatabase, "auto migrations failed", zap.Error(err2))
+					} else {
+						logger.ComponentInfo(logging.ComponentDatabase, "auto migrations applied")
+					}
+				} else {
+					logger.ComponentWarn(logging.ComponentDatabase, "migrations failed", zap.Error(err))
+				}
+			} else {
+				logger.ComponentInfo(logging.ComponentDatabase, "migrations applied")
+			}
+		} else {
+			logger.ComponentWarn(logging.ComponentDatabase, "RQLite not reachable; skipping migrations for now")
+		}
+	}()
 
+	logger.ComponentInfo(logging.ComponentGeneral, "Gateway creation completed, returning...")
 	return gw, nil
 }
 
+// probeRQLiteReachable performs a quick GET /status against candidate endpoints with a short timeout.
+func (g *Gateway) probeRQLiteReachable(timeout time.Duration) bool {
+	endpoints := client.DefaultDatabaseEndpoints()
+	httpClient := &http.Client{Timeout: timeout}
+	for _, ep := range endpoints {
+		url := ep
+		if url == "" {
+			continue
+		}
+		if url[len(url)-1] == '/' {
+			url = url[:len(url)-1]
+		}
+		reqURL := url + "/status"
+		resp, err := httpClient.Get(reqURL)
+		if err != nil {
+			continue
+		}
+		resp.Body.Close()
+		if resp.StatusCode == http.StatusOK {
+			return true
+		}
+	}
+	return false
+}
+
 // Close disconnects the gateway client
 func (g *Gateway) Close() {
 	if g.client != nil {
diff --git a/pkg/gateway/middleware.go b/pkg/gateway/middleware.go
index 53d356f..1172920 100644
--- a/pkg/gateway/middleware.go
+++ b/pkg/gateway/middleware.go
@@ -2,6 +2,7 @@ package gateway
 
 import (
 	"context"
+    "encoding/json"
 	"net"
 	"net/http"
 	"strconv"
@@ -92,28 +93,44 @@ func (g *Gateway) authMiddleware(next http.Handler) http.Handler {
 			}
 		}
 
-		// 2) Fallback to API key
-		key := extractAPIKey(r)
-		if key == "" {
-			w.Header().Set("WWW-Authenticate", "Bearer realm=\"gateway\", charset=\"UTF-8\"")
-			writeError(w, http.StatusUnauthorized, "missing API key")
-			return
-		}
+        // 2) Fallback to API key (validate against DB)
+        key := extractAPIKey(r)
+        if key == "" {
+            w.Header().Set("WWW-Authenticate", "Bearer realm=\"gateway\", charset=\"UTF-8\"")
+            writeError(w, http.StatusUnauthorized, "missing API key")
+            return
+        }
 
-		// Validate key
-		nsOverride, ok := g.cfg.APIKeys[key]
-		if !ok {
-			w.Header().Set("WWW-Authenticate", "Bearer error=\"invalid_token\"")
-			writeError(w, http.StatusUnauthorized, "invalid API key")
-			return
-		}
+        // Look up API key in DB and derive namespace
+        db := g.client.Database()
+        ctx := r.Context()
+        // Join to namespaces to resolve name in one query
+        q := "SELECT namespaces.name FROM api_keys JOIN namespaces ON api_keys.namespace_id = namespaces.id WHERE api_keys.key = ? LIMIT 1"
+        res, err := db.Query(ctx, q, key)
+        if err != nil || res == nil || res.Count == 0 || len(res.Rows) == 0 || len(res.Rows[0]) == 0 {
+            w.Header().Set("WWW-Authenticate", "Bearer error=\"invalid_token\"")
+            writeError(w, http.StatusUnauthorized, "invalid API key")
+            return
+        }
+        // Extract namespace name
+        var ns string
+        if s, ok := res.Rows[0][0].(string); ok {
+            ns = strings.TrimSpace(s)
+        } else {
+            b, _ := json.Marshal(res.Rows[0][0])
+            _ = json.Unmarshal(b, &ns)
+            ns = strings.TrimSpace(ns)
+        }
+        if ns == "" {
+            w.Header().Set("WWW-Authenticate", "Bearer error=\"invalid_token\"")
+            writeError(w, http.StatusUnauthorized, "invalid API key")
+            return
+        }
 
-		// Attach auth metadata to context for downstream use
-		ctx := context.WithValue(r.Context(), ctxKeyAPIKey, key)
-		if ns := strings.TrimSpace(nsOverride); ns != "" {
-			ctx = storage.WithNamespace(ctx, ns)
-		}
-		next.ServeHTTP(w, r.WithContext(ctx))
+        // Attach auth metadata to context for downstream use
+        ctx = context.WithValue(ctx, ctxKeyAPIKey, key)
+        ctx = storage.WithNamespace(ctx, ns)
+        next.ServeHTTP(w, r.WithContext(ctx))
 	})
 }
 
@@ -145,7 +162,7 @@ func extractAPIKey(r *http.Request) string {
 // isPublicPath returns true for routes that should be accessible without API key auth
 func isPublicPath(p string) bool {
 	switch p {
-	case "/health", "/v1/health", "/status", "/v1/status", "/v1/auth/jwks", "/.well-known/jwks.json", "/v1/version", "/v1/auth/challenge", "/v1/auth/verify", "/v1/auth/register", "/v1/auth/refresh", "/v1/auth/logout":
+    case "/health", "/v1/health", "/status", "/v1/status", "/v1/auth/jwks", "/.well-known/jwks.json", "/v1/version", "/v1/auth/challenge", "/v1/auth/verify", "/v1/auth/register", "/v1/auth/refresh", "/v1/auth/logout", "/v1/auth/api-key":
 		return true
 	default:
 		return false
diff --git a/pkg/gateway/migrate.go b/pkg/gateway/migrate.go
index a511c38..dbabad1 100644
--- a/pkg/gateway/migrate.go
+++ b/pkg/gateway/migrate.go
@@ -54,8 +54,8 @@ func (g *Gateway) applyMigrations(ctx context.Context) error {
 		return err
 	}
 
-	// Locate migrations directory relative to CWD
-	migDir := "migrations"
+    // Locate migrations directory relative to CWD
+    migDir := "migrations"
 	if fi, err := os.Stat(migDir); err != nil || !fi.IsDir() {
 		return errNoMigrationsFound
 	}
@@ -79,7 +79,7 @@ func (g *Gateway) applyMigrations(ctx context.Context) error {
 	}
 	sort.Slice(migrations, func(i, j int) bool { return migrations[i].ver < migrations[j].ver })
 
-	// Helper to check if version applied
+    // Helper to check if version applied
 	isApplied := func(ctx context.Context, v int) (bool, error) {
 		res, err := db.Query(ctx, "SELECT 1 FROM schema_migrations WHERE version = ? LIMIT 1", v)
 		if err != nil { return false, err }
diff --git a/pkg/gateway/routes.go b/pkg/gateway/routes.go
index 12227b0..8edea5e 100644
--- a/pkg/gateway/routes.go
+++ b/pkg/gateway/routes.go
@@ -16,8 +16,11 @@ func (g *Gateway) Routes() http.Handler {
 	// auth endpoints
 	mux.HandleFunc("/v1/auth/jwks", g.jwksHandler)
 	mux.HandleFunc("/.well-known/jwks.json", g.jwksHandler)
-	mux.HandleFunc("/v1/auth/challenge", g.challengeHandler)
-	mux.HandleFunc("/v1/auth/verify", g.verifyHandler)
+    mux.HandleFunc("/v1/auth/challenge", g.challengeHandler)
+    mux.HandleFunc("/v1/auth/verify", g.verifyHandler)
+    // New: issue JWT from API key; new: create or return API key for a wallet after verification
+    mux.HandleFunc("/v1/auth/token", g.apiKeyToJWTHandler)
+    mux.HandleFunc("/v1/auth/api-key", g.issueAPIKeyHandler)
 	mux.HandleFunc("/v1/auth/register", g.registerHandler)
 	mux.HandleFunc("/v1/auth/refresh", g.refreshHandler)
 	mux.HandleFunc("/v1/auth/logout", g.logoutHandler)