diff --git a/README.md b/README.md
index de40657..6ec8d7c 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,312 @@
-# appcore
+# AppCore
-go app core packages
\ No newline at end of file
+A comprehensive, production-ready Go utility library providing core functionality for building modern web applications.
+
+[](https://go.dev/)
+[](https://opensource.org/licenses/MIT)
+
+## Overview
+
+AppCore is a modular Go library that provides essential building blocks for web applications, with a focus on security, developer experience, and production reliability. The library offers reusable packages for common tasks including cryptography, email handling, JWT authentication, validation, and HTTP utilities.
+
+## Features
+
+- **Security-First Design**: Modern cryptography (ED25519, AES-GCM, Argon2id) following OWASP recommendations
+- **Type Safety**: Extensive use of Go generics and strong typing throughout
+- **Production Ready**: Comprehensive error handling and proper resource cleanup
+- **Developer Experience**: Convenient helper functions with consistent naming conventions
+- **Cross-Platform Support**: Works seamlessly across macOS, Linux, and Windows
+- **Modular Architecture**: Import only the packages you need
+
+## Installation
+
+```bash
+go get code.patial.tech/go/appcore
+```
+
+## Packages
+
+### Security & Cryptography (`crypto`)
+
+Password hashing, encryption, digital signatures, and secure random generation.
+
+```go
+import "code.patial.tech/go/appcore/crypto"
+
+// Password hashing (Argon2id - OWASP compliant)
+hash, err := crypto.PasswordHash("mypassword")
+valid := crypto.ComparePasswordHash("mypassword", hash)
+
+// AES-GCM encryption
+key := crypto.NewEncryptionKey(32)
+encrypted, err := crypto.Encrypt([]byte("data"), key)
+decrypted, err := crypto.Decrypt(encrypted, key)
+
+// ED25519 key management
+privateKey, publicKey, err := crypto.NewPersistedED25519("./keys")
+
+// Secure random generation
+randomBytes, err := crypto.RandomBytes(32)
+randomHex := crypto.RandomBytesHex(16)
+```
+
+**Key Functions**: `MD5()`, `SHA256()`, `PasswordHash()`, `ComparePasswordHash()`, `NewEncryptionKey()`, `Encrypt()`, `Decrypt()`, `NewPersistedED25519()`, `ParseEdPrivateKey()`, `ParseEdPublicKey()`, `RandomBytes()`, `RandomBytesHex()`
+
+### JWT Authentication (`jwt`)
+
+JWT token signing and parsing with support for EdDSA and HS256 algorithms.
+
+```go
+import "code.patial.tech/go/appcore/jwt"
+
+// EdDSA (recommended)
+token, err := jwt.SignEdDSA(privateKey, "issuer", "subject", time.Hour, extraClaims)
+claims, err := jwt.ParseEdDSA(token, publicKey, "issuer")
+
+// HS256
+token, err := jwt.SignHS256(secret, "issuer", "subject", time.Hour, extraClaims)
+claims, err := jwt.ParseHS256(token, secret, "issuer")
+```
+
+**Key Functions**: `Sign()`, `Parse()`, `SignEdDSA()`, `ParseEdDSA()`, `SignHS256()`, `ParseHS256()`
+
+### Email Handling (`email`)
+
+Email construction and delivery with SMTP support and development mode.
+
+```go
+import "code.patial.tech/go/appcore/email"
+
+msg := &email.Message{
+ From: "sender@example.com",
+ To: []string{"recipient@example.com"},
+ Subject: "Hello",
+ Body: "
Hello World
",
+}
+
+// SMTP transport
+transport := email.NewSMTPTransport("smtp.example.com", 587, "user", "pass")
+err := msg.Send(transport)
+
+// Development mode (opens in browser)
+transport := email.NewDumpToTempTransport()
+err := msg.Send(transport)
+```
+
+**Key Types**: `Message`, `Transport`, `SMTPTransport`, `DumpToTempTransport`
+
+### Data Validation (`validate`)
+
+Struct and map validation using go-playground/validator.
+
+```go
+import "code.patial.tech/go/appcore/validate"
+
+type User struct {
+ Email string `json:"email" validate:"required,email"`
+ Age int `json:"age" validate:"gte=0,lte=130"`
+}
+
+user := &User{Email: "test@example.com", Age: 25}
+err := validate.Struct(user) // Returns validation errors with JSON field names
+```
+
+**Key Functions**: `Struct()`, `Map()`, `RegisterAlias()`, `RegisterValidation()`
+
+### Environment Configuration (`dotenv`)
+
+Environment file parsing with variable expansion and struct assignment.
+
+```go
+import "code.patial.tech/go/appcore/dotenv"
+
+// Read .env file
+env, err := dotenv.Read(".env")
+
+// Assign to struct
+type Config struct {
+ Port int `env:"PORT"`
+ Host string `env:"HOST,HOSTNAME"` // Supports fallback tags
+}
+cfg := &Config{}
+err := dotenv.Assign(env, cfg)
+
+// Write .env file
+err := dotenv.Write(env, ".env")
+```
+
+**Key Functions**: `Read()`, `LoadFile()`, `Write()`, `Assign()`
+
+### HTTP Request Utilities (`request`)
+
+Request parsing, validation, and pagination helpers.
+
+```go
+import "code.patial.tech/go/appcore/request"
+
+// Parse and validate JSON payload
+type CreateUserRequest struct {
+ Email string `json:"email" validate:"required,email"`
+}
+var req CreateUserRequest
+err := request.PayloadWithValidate(r, &req)
+
+// Get pagination parameters
+pager := request.GetPager(r.URL.Query(), 50) // default page size 50
+```
+
+**Key Functions**: `Payload()`, `PayloadWithValidate()`, `PayloadMap()`, `FormField()`, `GetPager()`
+
+### HTTP Response Utilities (`response`)
+
+Standardized JSON response formatting.
+
+```go
+import "code.patial.tech/go/appcore/response"
+
+// Success responses
+response.Ok(w, data)
+response.Paged(w, data, pager, totalRecords)
+
+// Error responses
+response.BadRequest(w, "Invalid input")
+response.InternalServerError(w, err)
+response.NotAuthorized(w, "Invalid credentials")
+response.Forbidden(w, "Access denied")
+response.SessionExpired(w)
+```
+
+**Key Functions**: `Ok()`, `Paged()`, `BadRequest()`, `InternalServerError()`, `SessionExpired()`, `NotAuthorized()`, `Forbidden()`
+
+### Pointer Utilities (`ptr`)
+
+Safe reference and dereference operations with generic support.
+
+```go
+import "code.patial.tech/go/appcore/ptr"
+
+// Generic reference/dereference
+strPtr := ptr.Ref("hello")
+str := ptr.Deref(strPtr, "default")
+
+// Type-specific helpers
+boolPtr := ptr.Bool(true)
+numPtr := ptr.Number(42)
+strPtr := ptr.Str("test")
+
+// Safe getters with defaults
+value := ptr.GetBool(boolPtr, false)
+num := ptr.GetNumber(numPtr, 0)
+str := ptr.GetStr(strPtr, "")
+```
+
+**Key Functions**: `Ref()`, `Deref()`, `Bool()`, `GetBool()`, `Str()`, `GetStr()`, `StrTrim()`, `Number()`, `GetNumber()`
+
+### Unique ID Generation (`uid`)
+
+UUID v7 and Sqids-based ID generation.
+
+```go
+import "code.patial.tech/go/appcore/uid"
+
+// UUID v7 (time-based)
+id := uid.NewUUID()
+
+// Sqids (URL-friendly encoding)
+encoded := uid.Encode(123, 456)
+numbers, err := uid.Decode(encoded)
+```
+
+**Key Functions**: `NewUUID()`, `Encode()`, `Decode()`
+
+### Date/Time Utilities (`date`)
+
+Date parsing, arithmetic, and boundary calculations.
+
+```go
+import "code.patial.tech/go/appcore/date"
+
+// Parse ISO date
+t, err := date.ParseISODate("2024-01-15")
+
+// Calculate working days
+days := date.CountWorkingDays(startDate, endDate)
+
+// Date boundaries
+startOfDay := date.StartOfDay(now)
+endOfMonth := date.EndOfMonth(now)
+startOfWeek := date.StartOfWeek(now)
+
+// Date arithmetic
+lastWeek := date.LastWeek()
+lastMonth := date.LastMonth()
+threeMonthsAgo := date.SubtractMonths(now, 3)
+```
+
+**Key Functions**: `ParseISODate()`, `CountWorkingDays()`, `StartOfDay()`, `EndOfDay()`, `StartOfMonth()`, `EndOfMonth()`, `StartOfWeek()`, `EndOfWeek()`, `StartOfYear()`, `EndOfYear()`, `LastWeek()`, `LastMonth()`, `SubtractMonths()`, `SubtractDays()`, `SubtractAYear()`
+
+### Compression (`gz`)
+
+Gzip compression and decompression.
+
+```go
+import "code.patial.tech/go/appcore/gz"
+
+// Compress data
+compressed, err := gz.Zip([]byte("data"))
+
+// Decompress data
+decompressed, err := gz.UnZip(compressed)
+```
+
+**Key Functions**: `Zip()`, `UnZip()`
+
+### Additional Utilities
+
+- **`open`**: Cross-platform file/URI opening (`WithDefaultApp()`, `App()`)
+- **`structs`**: Reflection utilities (`Map()` - converts structs to maps)
+- **`mime`**: MIME type lookups from file extensions
+
+## Dependencies
+
+- `github.com/go-playground/validator/v10` - Data validation
+- `github.com/golang-jwt/jwt/v5` - JWT handling
+- `github.com/google/uuid` - UUID generation
+- `github.com/sqids/sqids-go` - URL-friendly ID encoding
+- `golang.org/x/crypto` - Cryptographic primitives
+
+## Use Cases
+
+AppCore is particularly well-suited for:
+
+- Building secure web applications and REST APIs
+- Standardizing authentication/authorization patterns
+- Email delivery systems
+- Configuration management
+- API response formatting
+- Data validation and sanitization
+
+## Security
+
+- **Password Hashing**: Uses Argon2id with OWASP-recommended parameters (m=19456, t=2, p=1)
+- **Encryption**: AES-GCM authenticated encryption
+- **JWT**: Supports EdDSA (recommended) and HS256 algorithms
+- **Random Generation**: Cryptographically secure random bytes
+
+## License
+
+MIT License - see [LICENSE](LICENSE) file for details.
+
+Copyright 2024/2025 Patial Tech (Ankit Patial).
+
+## Contributing
+
+Contributions are welcome! Please ensure:
+- Code follows Go best practices and project conventions
+- All tests pass (`go test ./...`)
+- New features include appropriate documentation
+- Security-sensitive code is reviewed carefully
+
+## Support
+
+For issues, questions, or feature requests, please open an issue on the project repository.
diff --git a/crypto/encryption.go b/crypto/encryption.go
index a98cf45..5ba34f7 100644
--- a/crypto/encryption.go
+++ b/crypto/encryption.go
@@ -15,10 +15,14 @@ import (
"io"
)
-// NewEncryptionKey will generate new key as base64 encoded string
-//
-// should be of 16, 24 or 32 bytes in length
+// NewEncryptionKey will generate new key as base64 encoded string.
+// Size must be 16, 24, or 32 bytes for AES-128, AES-192, or AES-256 respectively.
func NewEncryptionKey(size uint8) (string, error) {
+ // Validate AES key sizes
+ if size != 16 && size != 24 && size != 32 {
+ return "", fmt.Errorf("invalid key size %d: must be 16, 24, or 32 bytes for AES", size)
+ }
+
b, err := RandomBytes(size)
if err != nil {
return "", err
@@ -70,8 +74,14 @@ func Decrypt(key, text string) (string, error) {
nonceSize := gcm.NonceSize()
enc, err := hex.DecodeString(text)
if err != nil {
- return "", err
+ return "", fmt.Errorf("invalid hex encoding: %w", err)
}
+
+ // Validate ciphertext length to prevent panic
+ if len(enc) < nonceSize {
+ return "", fmt.Errorf("ciphertext too short: got %d bytes, need at least %d", len(enc), nonceSize)
+ }
+
nonce, ciphertext := enc[:nonceSize], enc[nonceSize:]
// decrypt
diff --git a/email/render.go b/email/render.go
index 64d3e6e..a2f4887 100644
--- a/email/render.go
+++ b/email/render.go
@@ -11,6 +11,21 @@ import (
txt "text/template"
)
+// RenderHTMLTemplate renders HTML email templates with automatic XSS protection.
+//
+// SECURITY WARNING:
+// - DO NOT pass untrusted user input as layout or content parameters
+// - Only use trusted, predefined template strings
+// - User data should ONLY be passed via the data map parameter
+// - The html/template package auto-escapes data to prevent XSS
+// - Template injection attacks are possible if template strings come from user input
+//
+// Example of SECURE usage:
+//
+// const layoutTemplate = "{{template \"content\" .}}"
+// const contentTemplate = "Hello {{.Name}}
"
+// data := map[string]any{"Name": userInput} // Safe - will be escaped
+// html, err := RenderHTMLTemplate(layoutTemplate, contentTemplate, data)
func RenderHTMLTemplate(layout, content string, data map[string]any) (string, error) {
// layout
tpl, err := template.New("layout").Parse(layout)
@@ -34,6 +49,21 @@ func RenderHTMLTemplate(layout, content string, data map[string]any) (string, er
return buf.String(), nil
}
+// RenderTxtTemplate renders plain text email templates WITHOUT escaping.
+//
+// SECURITY WARNING:
+// - DO NOT use this function for HTML content (use RenderHTMLTemplate instead)
+// - DO NOT pass untrusted user input as the content parameter
+// - Only use trusted, predefined template strings
+// - User data should ONLY be passed via the data map parameter
+// - text/template does NOT provide XSS protection - no auto-escaping
+// - Template injection attacks are possible if content comes from user input
+//
+// Example of SECURE usage:
+//
+// const textTemplate = "Hello {{.Name}}, your order #{{.OrderID}} is confirmed."
+// data := map[string]any{"Name": userName, "OrderID": orderID}
+// text, err := RenderTxtTemplate(textTemplate, data)
func RenderTxtTemplate(content string, data map[string]any) (string, error) {
tpl, err := txt.New("content").Parse(content)
if err != nil {
diff --git a/gz/gz.go b/gz/gz.go
index d762a4d..bd40cd6 100644
--- a/gz/gz.go
+++ b/gz/gz.go
@@ -34,6 +34,7 @@ func UnZip(data []byte) ([]byte, error) {
if err != nil {
return nil, err
}
+ defer r.Close() // Ensure reader is closed to prevent resource leak
var resB bytes.Buffer
if _, err := resB.ReadFrom(r); err != nil {
diff --git a/jwt/jwt.go b/jwt/jwt.go
index 2e2c2e9..2c37615 100644
--- a/jwt/jwt.go
+++ b/jwt/jwt.go
@@ -9,7 +9,6 @@ import (
"crypto/ed25519"
"errors"
"fmt"
- "log"
"maps"
"time"
@@ -51,14 +50,13 @@ func ParseEdDSA(key ed25519.PrivateKey, tokenString string, issuer string) (jwt.
jwt.WithExpirationRequired(),
)
if err != nil {
- log.Fatal(err)
+ return nil, fmt.Errorf("failed to parse JWT token: %w", err)
}
if claims, ok := token.Claims.(jwt.MapClaims); ok {
return claims, nil
- } else {
- return nil, errors.New("no claims found")
}
+ return nil, errors.New("no claims found in token")
}
func SignHS256(secret []byte, claims map[string]any, issuer string, d time.Duration) (string, error) {
@@ -88,12 +86,11 @@ func ParseHS256(secret []byte, tokenString string, issuer string) (jwt.MapClaims
jwt.WithExpirationRequired(),
)
if err != nil {
- log.Fatal(err)
+ return nil, fmt.Errorf("failed to parse JWT token: %w", err)
}
if claims, ok := token.Claims.(jwt.MapClaims); ok {
return claims, nil
- } else {
- return nil, errors.New("no claims found")
}
+ return nil, errors.New("no claims found in token")
}
diff --git a/open/open.go b/open/open.go
index 8f5545a..eb8ea84 100644
--- a/open/open.go
+++ b/open/open.go
@@ -6,12 +6,23 @@
package open
// WithDefaultApp open a file, directory, or URI using the OS's default application for that object type.
+// The input is validated to prevent path traversal and command injection attacks.
func WithDefaultApp(input string) error {
+ if err := validateInput(input); err != nil {
+ return err
+ }
cmd := open(input)
return cmd.Run()
}
-// WithApp will open a file directory, or URI using the specified application.
+// App will open a file directory, or URI using the specified application.
+// Both input and appName are validated to prevent command injection attacks.
func App(input string, appName string) error {
+ if err := validateInput(input); err != nil {
+ return err
+ }
+ if err := validateAppName(appName); err != nil {
+ return err
+ }
return openWith(input, appName).Run()
}
diff --git a/open/validate.go b/open/validate.go
new file mode 100644
index 0000000..546263e
--- /dev/null
+++ b/open/validate.go
@@ -0,0 +1,56 @@
+// Copyright 2025 Patial Tech (Ankit Patial).
+//
+// This file is part of code.patial.tech/go/appcore, which is MIT licensed.
+// See http://opensource.org/licenses/MIT
+
+package open
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+// validateInput checks for common security issues in file paths
+func validateInput(input string) error {
+ if input == "" {
+ return errors.New("input path cannot be empty")
+ }
+
+ // Clean the path to resolve . and .. references
+ clean := filepath.Clean(input)
+
+ // Check for suspicious characters that could be used for injection
+ // Allow URIs (http://, https://, etc.) but validate file paths
+ if !strings.Contains(clean, "://") {
+ // For file paths, check for path traversal attempts
+ if strings.Contains(clean, "..") {
+ return errors.New("path traversal detected in input")
+ }
+
+ // Check if path exists (for file paths)
+ if _, err := os.Stat(clean); err != nil {
+ if os.IsNotExist(err) {
+ return fmt.Errorf("path does not exist: %s", clean)
+ }
+ return fmt.Errorf("cannot access path: %w", err)
+ }
+ } else {
+ // For URIs, validate scheme
+ allowedSchemes := []string{"http://", "https://", "file://", "ftp://", "mailto:"}
+ valid := false
+ for _, scheme := range allowedSchemes {
+ if strings.HasPrefix(strings.ToLower(clean), scheme) {
+ valid = true
+ break
+ }
+ }
+ if !valid {
+ return fmt.Errorf("URI scheme not allowed: %s", clean)
+ }
+ }
+
+ return nil
+}
diff --git a/open/validate_darwin.go b/open/validate_darwin.go
new file mode 100644
index 0000000..ddfdc05
--- /dev/null
+++ b/open/validate_darwin.go
@@ -0,0 +1,28 @@
+// Copyright 2025 Patial Tech (Ankit Patial).
+//
+// This file is part of code.patial.tech/go/appcore, which is MIT licensed.
+// See http://opensource.org/licenses/MIT
+
+package open
+
+import (
+ "errors"
+ "strings"
+)
+
+// validateAppName validates application names on macOS
+func validateAppName(appName string) error {
+ if appName == "" {
+ return errors.New("application name cannot be empty")
+ }
+
+ // Check for suspicious characters that could be used for command injection
+ dangerous := []string{";", "|", "&", "$", "`", "\n", "\r", "$(", "&&", "||"}
+ for _, char := range dangerous {
+ if strings.Contains(appName, char) {
+ return errors.New("application name contains invalid characters")
+ }
+ }
+
+ return nil
+}
diff --git a/open/validate_linux.go b/open/validate_linux.go
new file mode 100644
index 0000000..65d5d85
--- /dev/null
+++ b/open/validate_linux.go
@@ -0,0 +1,34 @@
+// Copyright 2025 Patial Tech (Ankit Patial).
+//
+// This file is part of code.patial.tech/go/appcore, which is MIT licensed.
+// See http://opensource.org/licenses/MIT
+
+package open
+
+import (
+ "errors"
+ "os/exec"
+ "strings"
+)
+
+// validateAppName validates application names on Linux with strict security checks
+func validateAppName(appName string) error {
+ if appName == "" {
+ return errors.New("application name cannot be empty")
+ }
+
+ // Check for dangerous characters that could be used for command injection
+ dangerous := []string{";", "|", "&", "$", "`", "\n", "\r", "$(", "&&", "||", ">", "<", "*"}
+ for _, char := range dangerous {
+ if strings.Contains(appName, char) {
+ return errors.New("application name contains invalid characters")
+ }
+ }
+
+ // Verify the application exists in PATH (additional security check)
+ if _, err := exec.LookPath(appName); err != nil {
+ return errors.New("application not found in system PATH")
+ }
+
+ return nil
+}
diff --git a/open/validate_windows.go b/open/validate_windows.go
new file mode 100644
index 0000000..9e0b557
--- /dev/null
+++ b/open/validate_windows.go
@@ -0,0 +1,28 @@
+// Copyright 2025 Patial Tech (Ankit Patial).
+//
+// This file is part of code.patial.tech/go/appcore, which is MIT licensed.
+// See http://opensource.org/licenses/MIT
+
+package open
+
+import (
+ "errors"
+ "strings"
+)
+
+// validateAppName validates application names on Windows
+func validateAppName(appName string) error {
+ if appName == "" {
+ return errors.New("application name cannot be empty")
+ }
+
+ // Check for dangerous characters in Windows cmd context
+ dangerous := []string{";", "|", "&", "$", "`", "\n", "\r", "&&", "||", ">", "<", "^"}
+ for _, char := range dangerous {
+ if strings.Contains(appName, char) {
+ return errors.New("application name contains invalid characters")
+ }
+ }
+
+ return nil
+}
diff --git a/request/payload.go b/request/payload.go
index ca17e7c..7b07aed 100644
--- a/request/payload.go
+++ b/request/payload.go
@@ -7,12 +7,19 @@ package request
import (
"encoding/json"
+ "errors"
"fmt"
+ "io"
"net/http"
"code.patial.tech/go/appcore/validate"
)
+// MaxRequestBodySize is the maximum allowed size for request bodies (1MB default).
+// This prevents resource exhaustion attacks from large payloads.
+// Override this value if you need to accept larger requests.
+var MaxRequestBodySize int64 = 1 << 20 // 1MB
+
func FormField(r *http.Request, key string) (string, error) {
f, err := Payload[map[string]any](r)
if err != nil {
@@ -41,8 +48,45 @@ func PayloadWithValidate[T any](r *http.Request) (T, error) {
func Payload[T any](r *http.Request) (T, error) {
var p T
- if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
+
+ // Limit request body size to prevent resource exhaustion
+ limited := io.LimitReader(r.Body, MaxRequestBodySize)
+
+ decoder := json.NewDecoder(limited)
+ if err := decoder.Decode(&p); err != nil {
+ // Check if we hit the size limit
+ if err == io.EOF || err == io.ErrUnexpectedEOF {
+ // Try to read one more byte to see if there's more data
+ var buf [1]byte
+ if n, _ := limited.Read(buf[:]); n == 0 {
+ // We hit the limit
+ return p, errors.New("request body too large")
+ }
+ }
return p, err
}
return p, nil
}
+
+// DecodeJSON decodes JSON from the request body into the provided pointer.
+// This is useful when you want to decode into an existing variable.
+// The request body size is limited by MaxRequestBodySize to prevent DoS attacks.
+func DecodeJSON(r *http.Request, v any) error {
+ // Limit request body size to prevent resource exhaustion
+ limited := io.LimitReader(r.Body, MaxRequestBodySize)
+
+ decoder := json.NewDecoder(limited)
+ if err := decoder.Decode(v); err != nil {
+ // Check if we hit the size limit
+ if err == io.EOF || err == io.ErrUnexpectedEOF {
+ // Try to read one more byte to see if there's more data
+ var buf [1]byte
+ if n, _ := limited.Read(buf[:]); n == 0 {
+ // We hit the limit
+ return errors.New("request body too large")
+ }
+ }
+ return err
+ }
+ return nil
+}