docs: add implementation plans for project cleanup

This commit is contained in:
2026-05-06 23:27:43 +02:00
parent 56fd782cdc
commit 69f0f1c7ef
4 changed files with 894 additions and 0 deletions

View File

@@ -0,0 +1,167 @@
# AllAnime Client Decomposition Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Decompose the monolithic `api/playback/allanime_client.go` into focused, maintainable units.
**Architecture:**
1. Move uTLS networking logic to `pkg/net/utls`.
2. Move crypto and key discovery to `api/playback/allanime_crypto.go`.
3. Move data extraction and stream detection to `api/playback/allanime_extractor.go`.
4. Keep core API and GraphQL logic in `api/playback/allanime_client.go`.
**Tech Stack:** Go (uTLS, SHA256, AES-CTR)
---
### Task 1: Move uTLS RoundTripper to pkg/net/utls
**Files:**
- Create: `pkg/net/utls/utls.go`
- Modify: `api/playback/allanime_client.go`
- [ ] **Step 1: Create pkg/net/utls/utls.go**
```go
package utls
import (
"bufio"
"context"
"net"
"net/http"
"time"
utls "github.com/refraction-networking/utls"
"golang.org/x/net/http2"
)
type UtlsRoundTripper struct{}
func (rt *UtlsRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
port := req.URL.Port()
if port == "" {
if req.URL.Scheme == "https" {
port = "443"
} else {
port = "80"
}
}
addr := net.JoinHostPort(req.URL.Hostname(), port)
conn, err := net.DialTimeout("tcp", addr, 10*time.Second)
if err != nil {
return nil, err
}
config := &utls.Config{ServerName: req.URL.Hostname()}
uConn := utls.UClient(conn, config, utls.HelloFirefox_Auto)
if err := uConn.Handshake(); err != nil {
return nil, err
}
var httpRT http.RoundTripper
switch uConn.ConnectionState().NegotiatedProtocol {
case http2.NextProtoTLS:
t2 := &http2.Transport{
DialTLSContext: func(ctx context.Context, network, addr string, cfg *utls.Config) (net.Conn, error) {
return uConn, nil
},
}
httpRT = t2
default:
t1 := &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return uConn, nil
},
}
httpRT = t1
}
return httpRT.RoundTrip(req)
}
```
- [ ] **Step 2: Update allanime_client.go to use the new package**
Remove `utlsRoundTripper` from `api/playback/allanime_client.go` and update `newAllAnimeClient` to use `&utls.UtlsRoundTripper{}`.
- [ ] **Step 3: Verify build and commit**
Run: `go build ./api/playback/...`
Expected: PASS
```bash
git add pkg/net/utls/utls.go api/playback/allanime_client.go
git commit -m "refactor: move utls roundtripper to pkg/net/utls"
```
### Task 2: Extract Crypto and Key Discovery
**Files:**
- Create: `api/playback/allanime_crypto.go`
- Modify: `api/playback/allanime_client.go`
- [ ] **Step 1: Move crypto logic to allanime_crypto.go**
Extract constants (`allAnimeAESKey`, `aniCliRawSourceURL`, etc.) and functions (`decryptTobeparsed`, `tryDecryptCTR`, `getAESKey`, `fetchKeyFromForks`, `extractKey`, `getTestPayload`, `validateKeys`, `getAllKeys`) to `api/playback/allanime_crypto.go`.
- [ ] **Step 2: Update allanime_client.go**
Remove the extracted code from `api/playback/allanime_client.go`.
- [ ] **Step 3: Verify build and commit**
Run: `go build ./api/playback/...`
Expected: PASS
```bash
git add api/playback/allanime_crypto.go api/playback/allanime_client.go
git commit -m "refactor: extract allanime crypto and key discovery"
```
### Task 3: Extract Data Extraction and Stream Detection
**Files:**
- Create: `api/playback/allanime_extractor.go`
- Modify: `api/playback/allanime_client.go`
- [ ] **Step 1: Move extraction logic to allanime_extractor.go**
Extract functions (`extractSourceURLsFromData`, `buildStreamSource`, `sourceReference` struct, `buildSourceReferences`, `extractEpisodeData`, `decodeSourceURL`, `detectStreamType`, `detectEmbedType`, `getMapKeys`) to `api/playback/allanime_extractor.go`.
- [ ] **Step 2: Update allanime_client.go**
Remove the extracted code from `api/playback/allanime_client.go`. Ensure `allAnimeClient` methods still call these (now package-level) functions.
- [ ] **Step 3: Verify build and commit**
Run: `go build ./api/playback/...`
Expected: PASS
```bash
git add api/playback/allanime_extractor.go api/playback/allanime_client.go
git commit -m "refactor: extract allanime data extraction logic"
```
### Task 4: Final Cleanup and Verification
**Files:**
- Modify: `api/playback/allanime_client.go`
- [ ] **Step 1: Final review of allanime_client.go**
Ensure only `allAnimeClient` struct, `newAllAnimeClient`, `graphqlRequest`, `graphqlRequestWithHash`, `Search`, `GetEpisodes`, `GetAvailableEpisodes`, and `GetEpisodeMetadata` remain.
- [ ] **Step 2: Run all tests**
Run: `go test -v ./api/playback/...`
Expected: PASS
- [ ] **Step 3: Commit final cleanup**
```bash
git add api/playback/allanime_client.go
git commit -m "refactor: final cleanup of allanime_client.go"
```

View File

@@ -0,0 +1,300 @@
# Extract AllAnime Data Extraction Logic Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Extract data parsing and stream detection logic from `allanime_client.go` into a new file `allanime_extractor.go` to improve maintainability and separate concerns.
**Architecture:** Move non-network utility functions and helper structs related to parsing AllAnime responses and URL decoding/detection into a dedicated extractor file within the same package.
**Tech Stack:** Go (Standard Library)
---
### Task 1: Create allanime_extractor.go
**Files:**
- Create: `api/playback/allanime_extractor.go`
- [ ] **Step 1: Create the file with moved logic**
```go
package playback
import (
"context"
"encoding/json"
"fmt"
"log"
"net/url"
"strings"
)
type sourceReference struct {
URL string
Name string
}
func (c *allAnimeClient) extractSourceURLsFromData(ctx context.Context, data map[string]any) []StreamSource {
episodeData, ok := data["episode"].(map[string]any)
if !ok {
return nil
}
sourceURLs, ok := episodeData["sourceUrls"].([]any)
if !ok || len(sourceURLs) == 0 {
return nil
}
references := buildSourceReferences(sourceURLs)
if len(references) == 0 {
return nil
}
out := make([]StreamSource, 0, len(references))
for _, ref := range references {
target := strings.TrimSpace(ref.URL)
if target == "" {
continue
}
if strings.HasPrefix(target, "http://") || strings.HasPrefix(target, "https://") {
sourceType := detectStreamType(target)
if sourceType == "unknown" {
sourceType = detectEmbedType(target)
}
out = append(out, buildStreamSource(target, sourceType, ref.Name))
continue
}
decoded := decodeSourceURL(target)
if decoded == "" {
continue
}
if strings.HasPrefix(decoded, "http://") || strings.HasPrefix(decoded, "https://") {
sourceType := detectStreamType(decoded)
if sourceType == "unknown" {
sourceType = detectEmbedType(decoded)
}
out = append(out, buildStreamSource(decoded, sourceType, ref.Name))
continue
}
if !strings.HasPrefix(decoded, "/") {
decoded = "/" + decoded
}
extracted, err := c.extractor.ExtractVideoLinks(ctx, decoded)
if err != nil {
log.Printf("source extraction failed for %s: %v", decoded, err)
continue
}
out = append(out, extracted...)
}
return out
}
func buildStreamSource(url, sourceType, provider string) StreamSource {
return StreamSource{
URL: url,
Provider: provider,
Type: sourceType,
Referer: allAnimeReferer,
}
}
func buildSourceReferences(rawSourceURLs []any) []sourceReference {
priorityOrder := []string{"default", "yt-mp4", "s-mp4", "luf-mp4"}
prioritySet := map[string]struct{}{"default": {}, "yt-mp4": {}, "s-mp4": {}, "luf-mp4": {}}
prioritized := make(map[string]sourceReference)
fallback := make([]sourceReference, 0, len(rawSourceURLs))
seen := make(map[string]struct{})
for _, source := range rawSourceURLs {
item, ok := source.(map[string]any)
if !ok {
continue
}
sourceURL, _ := item["sourceUrl"].(string)
sourceName, _ := item["sourceName"].(string)
sourceURL = strings.TrimSpace(sourceURL)
sourceName = strings.TrimSpace(sourceName)
if sourceURL == "" {
continue
}
if _, exists := seen[sourceURL]; exists {
continue
}
seen[sourceURL] = struct{}{}
ref := sourceReference{URL: sourceURL, Name: sourceName}
normalized := strings.ToLower(sourceName)
if _, prioritizedProvider := prioritySet[normalized]; prioritizedProvider {
if _, exists := prioritized[normalized]; !exists {
prioritized[normalized] = ref
}
continue
}
fallback = append(fallback, ref)
}
ordered := make([]sourceReference, 0, len(prioritized)+len(fallback))
for _, provider := range priorityOrder {
if ref, ok := prioritized[provider]; ok {
ordered = append(ordered, ref)
}
}
ordered = append(ordered, fallback...)
return ordered
}
func extractEpisodeData(data map[string]any) (map[string]any, error) {
episodeData, ok := data["episode"].(map[string]any)
if ok && episodeData != nil {
return episodeData, nil
}
toBeParsed, ok := data["tobeparsed"].(string)
if !ok || strings.TrimSpace(toBeParsed) == "" {
return nil, fmt.Errorf("episode not found")
}
decoded, err := decryptTobeparsed(toBeParsed)
if err != nil {
return nil, fmt.Errorf("decode episode payload: %w", err)
}
var parsed map[string]any
if err := json.Unmarshal(decoded, &parsed); err != nil {
return nil, fmt.Errorf("parse decoded payload: %w", err)
}
episodeData, ok = parsed["episode"].(map[string]any)
if !ok || episodeData == nil {
return nil, fmt.Errorf("decoded payload missing episode")
}
return episodeData, nil
}
func decodeSourceURL(encoded string) string {
if encoded == "" {
return ""
}
encoded = strings.TrimPrefix(encoded, "--")
substitutions := map[string]string{
"79": "A", "7a": "B", "7b": "C", "7c": "D", "7d": "E",
"7e": "F", "7f": "G", "70": "H", "71": "I", "72": "J",
"73": "K", "74": "L", "75": "M", "76": "N", "77": "O",
"68": "P", "69": "Q", "6a": "R", "6b": "S", "6c": "T",
"6d": "U", "6e": "V", "6f": "W", "60": "X", "61": "Y",
"62": "Z",
"59": "a", "5a": "b", "5b": "c", "5c": "d", "5d": "e",
"5e": "f", "5f": "g", "50": "h", "51": "i", "52": "j",
"53": "k", "54": "l", "55": "m", "56": "n", "57": "o",
"48": "p", "49": "q", "4a": "r", "4b": "s", "4c": "t",
"4d": "u", "4e": "v", "4f": "w", "40": "x", "41": "y",
"42": "z",
"08": "0", "09": "1", "0a": "2", "0b": "3", "0c": "4",
"0d": "5", "0e": "6", "0f": "7", "00": "8", "01": "9",
"15": "-", "16": ".", "67": "_", "46": "~", "02": ":",
"17": "/", "07": "?", "1b": "#", "63": "[", "65": "]",
"78": "@", "19": "!", "1c": "$", "1e": "&", "10": "(",
"11": ")", "12": "*", "13": "+", "14": ",", "03": ";",
"05": "=", "1d": "%",
}
var result strings.Builder
for idx := 0; idx < len(encoded); {
if idx+2 <= len(encoded) {
pair := encoded[idx : idx+2]
if sub, ok := substitutions[pair]; ok {
result.WriteString(sub)
idx += 2
continue
}
}
result.WriteByte(encoded[idx])
idx++
}
decoded := result.String()
if strings.Contains(decoded, "/clock") && !strings.Contains(decoded, "/clock.json") {
decoded = strings.Replace(decoded, "/clock", "/clock.json", 1)
}
return decoded
}
func detectStreamType(sourceURL string) string {
lower := strings.ToLower(sourceURL)
if strings.Contains(lower, ".m3u8") || strings.Contains(lower, "master.m3u8") {
return "m3u8"
}
if strings.Contains(lower, ".mp4") {
return "mp4"
}
return "unknown"
}
func detectEmbedType(rawURL string) string {
lower := strings.ToLower(rawURL)
embedHosts := []string{"streamwish", "streamsb", "mp4upload", "ok.ru", "gogoplay", "streamlare"}
for _, host := range embedHosts {
if strings.Contains(lower, host) {
return "embed"
}
}
return "unknown"
}
func getMapKeys(m map[string]any) []string {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
return keys
}
```
- [ ] **Step 2: Commit**
```bash
git add api/playback/allanime_extractor.go
git commit -m "feat: add allanime_extractor.go with moved logic"
```
### Task 2: Update allanime_client.go
**Files:**
- Modify: `api/playback/allanime_client.go`
- [ ] **Step 1: Remove moved logic from allanime_client.go**
Remove: `extractSourceURLsFromData`, `buildStreamSource`, `sourceReference` struct, `buildSourceReferences`, `extractEpisodeData`, `decodeSourceURL`, `detectStreamType`, `detectEmbedType`, `getMapKeys`.
- [ ] **Step 2: Verify build**
Run: `go build ./api/playback/...`
Expected: PASS
- [ ] **Step 3: Commit**
```bash
git add api/playback/allanime_client.go
git commit -m "refactor: remove moved logic from allanime_client.go"
```

View File

@@ -0,0 +1,150 @@
# Project Structure Cleanup Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Standardize the Go project structure, consolidate database packages, and remove redundant files to improve maintainability and idiomatic code patterns.
**Architecture:**
1. Consolidate all database-related code into `internal/db` with the package name `db`.
2. Remove the `internal/db/sqlite` sub-package and move its contents to `internal/db`.
3. Update `sqlc.yaml` and regenerate code.
4. Clean up the `tmp/scripts/` directory.
5. Update all imports throughout the project.
**Tech Stack:** Go, sqlc, Bun
---
### Task 1: Clean up tmp/scripts
**Files:**
- Delete: `tmp/scripts/*.go`
- [ ] **Step 1: Delete redundant scripts**
Run: `rm tmp/scripts/*.go`
- [ ] **Step 2: Commit cleanup**
```bash
git add tmp/scripts/
git commit -m "chore: remove redundant scripts"
```
### Task 2: Standardize internal/db Package and Flatten Structure
**Files:**
- Modify: `sqlc.yaml`
- Move: `internal/db/sqlite/sqlite.go` -> `internal/db/sqlite.go`
- Modify: `internal/db/*.go` (generated and manual)
- [ ] **Step 1: Update sqlc configuration**
Modify `sqlc.yaml`:
```yaml
version: "2"
sql:
- engine: "sqlite"
queries: "internal/db/queries.sql"
schema: "migrations/"
gen:
go:
package: "db"
out: "internal/db"
emit_json_tags: true
emit_prepared_queries: false
emit_interface: true
emit_exact_table_names: false
```
- [ ] **Step 2: Regenerate sqlc code**
Run: `sqlc generate`
- [ ] **Step 3: Move and update sqlite.go**
Run: `mv internal/db/sqlite/sqlite.go internal/db/sqlite.go && rm -rf internal/db/sqlite`
Modify `internal/db/sqlite.go`:
```go
package db
import (
"database/sql"
"fmt"
"os"
"path/filepath"
_ "github.com/mattn/go-sqlite3"
)
// ... existing functions, update return types to use local db package if needed
```
- [ ] **Step 4: Update package names in manual files**
Modify `internal/db/helpers.go` and `internal/db/migrate.go` to use `package db`.
- [ ] **Step 5: Commit database structural changes**
```bash
git add internal/db/ sqlc.yaml
git commit -m "refactor: consolidate db package and flatten structure"
```
### Task 3: Update Global Imports
**Files:**
- Modify: All files importing `mal/internal/db` or `mal/internal/db/sqlite`
- [ ] **Step 1: Replace imports and qualifiers**
Use `sed` or `edit` to update all imports:
- `database "mal/internal/db"` -> `"mal/internal/db"`
- `"mal/internal/db"` -> `"mal/internal/db"` (ensure it's used as `db.`)
- `"mal/internal/db/sqlite"` -> `"mal/internal/db"`
- [ ] **Step 2: Update code references**
Replace `database.` and `sqlite.` with `db.` globally where applicable.
- [ ] **Step 3: Verify build**
Run: `go build ./...`
Expected: PASS
- [ ] **Step 4: Commit import updates**
```bash
git add .
git commit -m "refactor: update imports to use new db package"
```
### Task 4: Clean dist/ and Verify Build
**Files:**
- Modify: `package.json` (if needed)
- Clean: `dist/`
- [ ] **Step 1: Clean dist directory**
Run: `rm -rf dist/*`
- [ ] **Step 2: Run frontend build**
Run: `bun run build:assets`
- [ ] **Step 3: Verify dist structure**
Check if `dist/static/static` still exists. If so, investigate `bun build` command in `package.json`.
- [ ] **Step 4: Final verification**
Run: `go build ./... && bun run typecheck`
Expected: PASS
- [ ] **Step 5: Commit build fixes**
```bash
git add .
git commit -m "build: clean dist and verify assets"
```

View File

@@ -0,0 +1,277 @@
# Rate Limiter Refactor Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Refactor the global-state rate limiter into a struct-based implementation that supports multiple instances with different configurations.
**Architecture:**
1. Introduce a `Limiter` struct that encapsulates state and configuration.
2. Remove global variables and `init()` in `pkg/middleware/ratelimit.go`.
3. Provide generic and auth-specific middleware methods.
4. Integrate the new `Limiter` into the server startup and router.
**Tech Stack:** Go (Standard Library)
---
### Task 1: Refactor Limiter Logic and State
**Files:**
- Modify: `pkg/middleware/ratelimit.go`
- [ ] **Step 1: Define new structs and remove global state**
Replace content of `pkg/middleware/ratelimit.go`:
```go
package middleware
import (
"fmt"
"net/http"
"strings"
"sync"
"time"
)
type visitor struct {
attempts int
lastSeen time.Time
}
type Config struct {
MaxAttempts int
Window time.Duration
}
type Limiter struct {
mu sync.Mutex
visitors map[string]*visitor
config Config
}
func NewLimiter(cfg Config) *Limiter {
return &Limiter{
visitors: make(map[string]*visitor),
config: cfg,
}
}
func (l *Limiter) Cleanup(now time.Time) {
l.mu.Lock()
defer l.mu.Unlock()
for ip, v := range l.visitors {
if now.Sub(v.lastSeen) > l.config.Window*3 {
delete(l.visitors, ip)
}
}
}
func getIP(r *http.Request) string {
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
ips := strings.Split(xff, ",")
return strings.TrimSpace(ips[0])
}
if realIP := r.Header.Get("X-Real-IP"); realIP != "" {
return realIP
}
ip := r.RemoteAddr
if colonIdx := strings.LastIndex(ip, ":"); colonIdx != -1 {
ip = ip[:colonIdx]
}
return ip
}
```
- [ ] **Step 2: Implement Middleware methods**
Add to `pkg/middleware/ratelimit.go`:
```go
func (l *Limiter) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !l.allow(getIP(r)) {
http.Error(w, "Too many requests. Please try again later.", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
func (l *Limiter) AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !l.allow(getIP(r)) {
if strings.HasPrefix(r.URL.Path, "/") {
http.Redirect(w, r, fmt.Sprintf("%s?error=rate_limited", r.URL.Path), http.StatusFound)
return
}
http.Error(w, "Too many requests. Please try again later.", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
func (l *Limiter) allow(ip string) bool {
l.mu.Lock()
defer l.mu.Unlock()
v, exists := l.visitors[ip]
if !exists {
l.visitors[ip] = &visitor{1, time.Now()}
return true
}
if time.Since(v.lastSeen) > l.config.Window {
v.attempts = 1
v.lastSeen = time.Now()
return true
}
v.attempts++
v.lastSeen = time.Now()
return v.attempts <= l.config.MaxAttempts
}
```
- [ ] **Step 3: Commit refactor**
```bash
git add pkg/middleware/ratelimit.go
git commit -m "refactor: convert rate limiter to struct-based implementation"
```
### Task 2: Add Unit Tests for Limiter
**Files:**
- Create: `pkg/middleware/ratelimit_test.go`
- [ ] **Step 1: Write tests for Limiter logic**
Create `pkg/middleware/ratelimit_test.go`:
```go
package middleware
import (
"net/http"
"net/http/httptest"
"testing"
"time"
)
func testHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
}
func TestLimiter(t *testing.T) {
cfg := Config{MaxAttempts: 2, Window: 100 * time.Millisecond}
l := NewLimiter(cfg)
handler := l.Middleware(testHandler())
// First attempt
req := httptest.NewRequest("GET", "/", nil)
req.RemoteAddr = "1.2.3.4:1234"
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("expected 200, got %d", rr.Code)
}
// Second attempt
rr = httptest.NewRecorder()
handler.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("expected 200, got %d", rr.Code)
}
// Third attempt (should fail)
rr = httptest.NewRecorder()
handler.ServeHTTP(rr, req)
if rr.Code != http.StatusTooManyRequests {
t.Errorf("expected 429, got %d", rr.Code)
}
// Wait for window to expire
time.Sleep(150 * time.Millisecond)
// Fourth attempt (should pass again)
rr = httptest.NewRecorder()
handler.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("expected 200, got %d", rr.Code)
}
}
```
- [ ] **Step 2: Run tests**
Run: `go test -v pkg/middleware/ratelimit.go pkg/middleware/ratelimit_test.go`
Expected: PASS
- [ ] **Step 3: Commit tests**
```bash
git add pkg/middleware/ratelimit_test.go
git commit -m "test: add unit tests for rate limiter"
```
### Task 3: Integrate Limiter into Server
**Files:**
- Modify: `internal/server/routes.go`
- Modify: `cmd/server/main.go`
- [ ] **Step 1: Update Config and NewRouter**
Modify `internal/server/routes.go`:
```go
type Config struct {
DB *db.Queries
SQLDB *sql.DB
JikanClient *jikan.Client
AuthService *auth.Service
PlaybackProxySecret string
AuthLimiter *pkgmiddleware.Limiter // Add this
}
// ... in NewRouter function, replace:
// pkgmiddleware.RateLimitAuth(...)
// with:
// cfg.AuthLimiter.AuthMiddleware(...)
```
- [ ] **Step 2: Instantiate Limiter in main.go**
Modify `cmd/server/main.go`:
```go
// ... in main function
authLimiter := pkgmiddleware.NewLimiter(pkgmiddleware.Config{
MaxAttempts: 5,
Window: time.Minute,
})
// Start cleanup goroutine
go func() {
for {
time.Sleep(time.Minute)
authLimiter.Cleanup(time.Now())
}
}()
router := server.NewRouter(server.Config{
// ...
AuthLimiter: authLimiter,
})
```
- [ ] **Step 3: Verify build and manual test**
Run: `go build ./...`
Expected: PASS
- [ ] **Step 4: Commit integration**
```bash
git add internal/server/routes.go cmd/server/main.go
git commit -m "feat: integrate new rate limiter into server"
```