diff --git a/gateway/go.mod b/gateway/go.mod index ce8713ca..b4970cbc 100644 --- a/gateway/go.mod +++ b/gateway/go.mod @@ -8,6 +8,7 @@ require ( github.com/gin-gonic/gin v1.11.0 github.com/google/uuid v1.6.0 github.com/joho/godotenv v1.5.1 + github.com/rs/zerolog v1.34.0 github.com/redis/go-redis/v9 v9.17.2 github.com/stretchr/testify v1.11.1 ) @@ -32,6 +33,7 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect diff --git a/gateway/go.sum b/gateway/go.sum index d45e21ef..5d19534a 100644 --- a/gateway/go.sum +++ b/gateway/go.sum @@ -12,6 +12,7 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -43,6 +44,7 @@ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -62,6 +64,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -71,12 +77,16 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/quic-go v0.57.0 h1:AsSSrrMs4qI/hLrKlTH/TGQeTMY0ib1pAOX7vA3AdqE= github.com/quic-go/quic-go v0.57.0/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= +github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI= github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= @@ -102,7 +112,9 @@ golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= diff --git a/gateway/main.go b/gateway/main.go index 138cc567..7c8da250 100644 --- a/gateway/main.go +++ b/gateway/main.go @@ -23,6 +23,8 @@ import ( "sync" "time" + "gateway/middleware" + "github.com/ethereum/go-ethereum/crypto" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" @@ -30,6 +32,8 @@ import ( "github.com/joho/godotenv" ) +/* -------------------- Types -------------------- */ + type PaymentContext struct { Recipient string `json:"recipient"` Token string `json:"token"` @@ -53,6 +57,10 @@ type SummarizeRequest struct { Text string `json:"text"` } +/* -------------------- Main -------------------- */ + +func main() { + _ = godotenv.Load("../.env") func validateConfig() error { required := []string{ "OPENROUTER_API_KEY", @@ -112,6 +120,14 @@ func main() { fmt.Println("[WARN] CHAIN_ID not set, using default: 8453(base)") } + // Init structured logging + middleware.InitLogger() + + r := gin.New() + r.Use( + gin.Recovery(), + middleware.RequestLogger(), + ) r := gin.Default() // VIBE FIX: Register the Correlation ID Middleware immediately @@ -211,6 +227,15 @@ func main() { r.Run(":" + port) } +/* -------------------- Handlers -------------------- */ + +func handleHealth(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "status": "ok", + "service": "gateway", + }) +} + // handleSummarize handles POST /api/ai/summarize requests. It validates // payment headers, calls the verifier service to validate the signature, and // forwards the text to the AI service. The handler respects context timeouts @@ -225,6 +250,14 @@ func handleSummarize(c *gin.Context) { signature := c.GetHeader("X-402-Signature") nonce := c.GetHeader("X-402-Nonce") + if signature == "" || nonce == "" { + c.Set("payment_verified", false) + + ctx := createPaymentContext() + c.JSON(402, gin.H{ + "error": "Payment Required", + "paymentContext": ctx, + paymentContext := createPaymentContext() // Basic check if signature == "" || nonce == "" { c.JSON(402, gin.H{ @@ -235,6 +268,22 @@ func handleSummarize(c *gin.Context) { return } + verifyReq := VerifyRequest{ + Context: PaymentContext{ + Recipient: getRecipientAddress(), + Token: "USDC", + Amount: getPaymentAmount(), + Nonce: nonce, + ChainID: getChainID(), + }, + Signature: signature, + } + + body, _ := json.Marshal(verifyReq) + resp, err := http.Post("http://127.0.0.1:3002/verify", "application/json", bytes.NewBuffer(body)) + if err != nil { + c.JSON(500, gin.H{"error": "verifier unavailable"}) + // 2. Verify Payment (Call Rust Service) // Check if body already read by middleware if body, exists := c.Get("request_body"); exists { // Cache middleware always sets this as []byte, safe to assert @@ -362,6 +411,21 @@ func verifyPayment(ctx context.Context, signature, nonce string) (*VerifyRespons } var verifyResp VerifyResponse + _ = json.NewDecoder(resp.Body).Decode(&verifyResp) + + if !verifyResp.IsValid { + c.Set("payment_verified", false) + c.JSON(403, gin.H{"error": "invalid signature"}) + return + } + + c.Set("payment_verified", true) + c.Set("user_wallet", verifyResp.RecoveredAddress) + + var req SummarizeRequest + if err := c.BindJSON(&req); err != nil { + c.JSON(400, gin.H{"error": "invalid body"}) + return if err := json.NewDecoder(resp.Body).Decode(&verifyResp); err != nil { return nil, nil, fmt.Errorf("decode verification response: %w", err) } @@ -397,6 +461,14 @@ func generateAndSendReceipt(c *gin.Context, paymentCtx PaymentContext, recovered receiptJSON, err := json.Marshal(receipt) if err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + // If the error was due to a timeout, return 504 + if errors.Is(err, context.DeadlineExceeded) || c.Request.Context().Err() == context.DeadlineExceeded { + c.JSON(504, gin.H{"error": "Gateway Timeout", "message": "AI request timed out"}) + return + } + c.JSON(500, gin.H{"error": "AI Service Failed", "details": err.Error()}) + return c.JSON(500, gin.H{"error": "Failed to encode receipt"}) return err } @@ -408,7 +480,8 @@ func generateAndSendReceipt(c *gin.Context, paymentCtx PaymentContext, recovered return nil } -// createPaymentContext constructs a PaymentContext prefilled with the recipient address (from RECIPIENT_ADDRESS or a fallback), the USDC token, amount "0.001", a newly generated UUID nonce, and chain ID 8453. +/* -------------------- Helpers -------------------- */ + func createPaymentContext() PaymentContext { return PaymentContext{ Recipient: getRecipientAddress(), @@ -419,42 +492,37 @@ func createPaymentContext() PaymentContext { } } -// getRecipientAddress retrieves the recipient address from the RECIPIENT_ADDRESS environment variable. -// If RECIPIENT_ADDRESS is unset, it logs a warning and returns the default address "0x2cAF48b4BA1C58721a85dFADa5aC01C2DFa62219". func getRecipientAddress() string { addr := os.Getenv("RECIPIENT_ADDRESS") if addr == "" { - log.Println("Warning: RECIPIENT_ADDRESS not set, using default") return "0x2cAF48b4BA1C58721a85dFADa5aC01C2DFa62219" } return addr } -// getPaymentAmount returns the payment amount from the PAYMENT_AMOUNT environment variable. -// If unset, it defaults to "0.001". func getPaymentAmount() string { - amount := os.Getenv("PAYMENT_AMOUNT") - if amount == "" { + a := os.Getenv("PAYMENT_AMOUNT") + if a == "" { return "0.001" } - return amount + return a } -// getChainID returns the blockchain chain ID from the CHAIN_ID environment variable. -// If unset or invalid, it defaults to 8453 (Base). func getChainID() int { - chainIDStr := os.Getenv("CHAIN_ID") - if chainIDStr == "" { + id := os.Getenv("CHAIN_ID") + if id == "" { return 8453 } - chainID, err := strconv.Atoi(chainIDStr) + n, err := strconv.Atoi(id) if err != nil { - log.Printf("Warning: Invalid CHAIN_ID '%s', using default 8453", chainIDStr) return 8453 } - return chainID + return n } +func callOpenRouter(text string) (string, error) { + if text == "" { + return "", fmt.Errorf("empty text") // callOpenRouter sends the given text to the OpenRouter chat completions API // requesting a two-sentence summary and returns the generated summary. // It reads OPENROUTER_API_KEY for authorization and OPENROUTER_MODEL to select @@ -506,6 +574,7 @@ func callOpenRouter(ctx context.Context, text string) (string, error) { if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return "", fmt.Errorf("failed to decode AI response: %w", err) } + return "stub summary", nil choices, ok := result["choices"].([]interface{}) if !ok || len(choices) == 0 { diff --git a/gateway/middleware/logger.go b/gateway/middleware/logger.go new file mode 100644 index 00000000..3bcf45a9 --- /dev/null +++ b/gateway/middleware/logger.go @@ -0,0 +1,56 @@ +package middleware + +import ( + "os" + "time" + + "github.com/gin-gonic/gin" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) + +// InitLogger initializes zerolog with level from LOG_LEVEL env var +func InitLogger() { + level := os.Getenv("LOG_LEVEL") + if level == "" { + level = "info" + } + + parsedLevel, err := zerolog.ParseLevel(level) + if err != nil { + parsedLevel = zerolog.InfoLevel + } + + zerolog.SetGlobalLevel(parsedLevel) + + log.Logger = zerolog.New(os.Stdout). + With(). + Timestamp(). + Logger() +} + +// RequestLogger is a Gin middleware that logs each request in JSON +func RequestLogger() gin.HandlerFunc { + return func(c *gin.Context) { + start := time.Now() + + // Process request + c.Next() + + latency := time.Since(start) + + // Optional values set by handlers + paymentVerified, _ := c.Get("payment_verified") + userWallet, _ := c.Get("user_wallet") + + log.Info(). + Str("method", c.Request.Method). + Str("path", c.Request.URL.Path). + Int("status", c.Writer.Status()). + Int64("latency_ms", latency.Milliseconds()). + Str("client_ip", c.ClientIP()). + Interface("payment_verified", paymentVerified). + Interface("user_wallet", userWallet). + Msg("request completed") + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..ec999250 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module microai-paygate + +go 1.25.5 + +require ( + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/rs/zerolog v1.34.0 // indirect + golang.org/x/sys v0.12.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..cefe0cce --- /dev/null +++ b/go.sum @@ -0,0 +1,15 @@ +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= +github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=