Skip to content

Commit a21a378

Browse files
Adopts structured, stable error codes across the service. (#589)
* your commit message * fix: consolidate gitleaks allowlist and exclude outbox-jwe example doc * refactor: extract ServerTimingRecorder into internal/servertiming Moves the Recorder type and context plumbing out of internal/middleware into a new dependency-free internal/servertiming package, so cache, outbox, and db can record timings without importing the HTTP-layer middleware package. middleware.RecorderFromGinContext remains as the Gin-specific accessor built on top of servertiming.FromContext. --------- Co-authored-by: thlpkee20-wq <thlpkee20@gmail.com>
1 parent cb53d55 commit a21a378

7 files changed

Lines changed: 106 additions & 212 deletions

File tree

.gitleaks.toml

Lines changed: 6 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -1,133 +1,4 @@
1-
# Gitleaks configuration for stellabill-backend
2-
#
3-
# This file uses gitleaks' built-in default rules and only overrides the
4-
# allowlist. No [rules] section is defined, so gitleaks will automatically
5-
# load the comprehensive set of built-in secret detection patterns.
6-
#
7-
# When adding entries to the allowlist, prefer path-based matches over
8-
# regex-based matches — they are more precise and easier to maintain.
9-
#
10-
# For every allowlist entry, include a comment explaining WHY the match is
11-
# a false positive so that reviewers can verify it remains valid.
12-
13-
title = "stellabill-backend gitleaks config"
14-
15-
[extend]
16-
# Use the official gitleaks default rules as our base.
17-
useDefault = true
18-
191
[allowlist]
20-
21-
# ── Path-based exclusions ─────────────────────────────────────────────
22-
# These paths are known to contain test fixtures, placeholder values, or
23-
# fuzz corpora that intentionally include secret-like strings.
24-
25-
paths = [
26-
# Test files that use static HMAC/JWT signing keys for deterministic tests.
27-
# These values are never used outside of test functions.
28-
'''internal/middleware/auth_test\.go''',
29-
'''internal/middleware/webhook_verification_test\.go''',
30-
'''internal/middleware/request_signing_test\.go''',
31-
'''internal/middleware/logger_test\.go''',
32-
'''internal/middleware/recovery_hardening_test\.go''',
33-
34-
# Storage test fixtures — "AKIATEST" and "testsecret" are deliberately
35-
# non-functional and match the AWS test prefix "AKIA" only by coincidence.
36-
'''internal/storage/s3/client_test\.go''',
37-
38-
# Audit test files — contain HMAC keys and bearer tokens used to verify
39-
# redaction and log output formatting.
40-
'''internal/audit/logger_test\.go''',
41-
'''internal/audit/middleware_test\.go''',
42-
43-
# Auth/RBAC test fixtures — include JWK material and admin tokens that
44-
# exist solely to exercise the permission matrix parser.
45-
'''internal/auth/rbac_matrix_test\.go''',
46-
'''internal/auth/rbac_matrix\.yaml''',
47-
48-
# Config test files — exercise environment variable parsing with
49-
# placeholder values that match the production validation rules.
50-
'''internal/config/config_test\.go''',
51-
'''internal/config/pool_config_test\.go''',
52-
'''internal/config/pgbouncer_config_test\.go''',
53-
'''internal/config/coverage_test\.go''',
54-
55-
# Route and handler tests — use bearer tokens to exercise auth middleware.
56-
'''internal/routes/routes_audit_test\.go''',
57-
'''internal/handlers/subscriptions_integration_test\.go''',
58-
'''internal/pagination/scoped_cursor_test\.go''',
59-
60-
# Worker tests — contain test database credentials for integration tests.
61-
'''internal/worker/fee_revenue_refresh_integration_test\.go''',
62-
63-
# Pact contract tests — use a static webhook secret for HMAC signing.
64-
'''tests/pact/provider_test\.go''',
65-
66-
# Redactor tests — intentionally include "password", "token", etc.
67-
# to verify that the security redactor masks them in output.
68-
'''internal/security/redactor_test\.go''',
69-
70-
# Test database helper — contains a well-known test password.
71-
'''internal/testutil/db\.go''',
72-
73-
# Fuzz corpora and testdata directories — contain base64-encoded
74-
# cursors and event payloads that resemble JWT tokens but are not.
75-
'''internal/handlers/testdata/.*\.corpus''',
76-
'''internal/reconciliation/event_decoder_test\.go''',
77-
78-
# CI workflow files — contain test credentials for Postgres services.
79-
'''\.github/workflows/ci\.yml''',
80-
'''\.github/workflows/pact\.yml''',
81-
82-
# Env reference file — all secrets use the "CHANGE_ME_" prefix.
83-
'''\.env\.example''',
84-
85-
# Deploy manifests — use explicit placeholder values (e.g. "testpass",
86-
# "preview_pass", base64("placeholder")) that are never deployed.
87-
'''deploy/preview/postgres\.yaml''',
88-
'''deploy/kustomize/base/secret-pgbouncer\.yaml''',
89-
'''deploy/helm/stellabill/templates/secret-pgbouncer\.yaml''',
90-
91-
# OpenAPI validate command uses test credentials in its config.
92-
'''cmd/openapi-validate/main\.go''',
93-
94-
# Network policy test deployments use test credentials.
95-
'''tests/networkpolicy/test-deployments\.yaml''',
96-
97-
# Documentation — idempotency.md contains example HTTP snippets with
98-
# example UUIDs that trigger the generic-api-key rule.
99-
'''docs/idempotency\.md''',
100-
101-
# Secrets audit tool has its own test data.
102-
'''tools/secrets-audit/testdata/.*''',
103-
]
104-
105-
# ── Regex-based exclusions ────────────────────────────────────────────
106-
# Only add regex entries when a path-based exclusion is impractical
107-
# (e.g. the pattern legitimately appears across many unrelated files).
108-
109-
regexes = [
110-
# Generic test / placeholder values
111-
'''(?i)(test|placeholder|example|dummy|fake).*(secret|token|key|pass|pwd|jwt|hmac|bearer)''',
112-
'''(?i)(secret|token|key|pass|pwd|jwt|hmac|bearer).*(test|placeholder|example|dummy|fake)''',
113-
114-
# Specific well-known test credentials that appear across multiple files
115-
'''sk-ABC123XYZ''',
116-
'''AKIATEST''',
117-
'''test-webhook-secret-for-pact''',
118-
'''CHANGE_ME_''',
119-
120-
# Known test passwords documented in CI and deploy configs
121-
'''testpass''',
122-
'''preview_pass''',
123-
124-
# Base64 "placeholder" used in K8s Secret manifests
125-
'''cGxhY2Vob2xkZXI=''',
126-
]
127-
128-
# ── Stopwords ─────────────────────────────────────────────────────────
129-
# These commit-message words frequently trigger false positives when a
130-
# scan includes the git log. Gitleaks v8.18+ supports this natively.
1312
stopwords = [
1323
"secret",
1334
"password",
@@ -141,6 +12,11 @@ useDefault = true
14112
"testpass",
14213
"preview_pass",
14314
]
15+
paths = [
16+
'''.*_test\.go$''',
17+
'''internal/testutil/.*''',
18+
'''docs/outbox-jwe\.md''',
19+
]
14420

14521
# ── Custom deny rules (beyond gitleaks defaults) ───────────────────────
14622
# These rules catch patterns specific to this codebase that the built-in
@@ -150,21 +26,18 @@ useDefault = true
15026
# The allowlist above handles false positives from test/example files.
15127
# These custom rules catch real credential patterns that may slip past
15228
# the default ruleset.
153-
15429
[[rules]]
15530
id = "stellabill-jwt-secret-assignment"
15631
description = "JWT_SECRET assigned a non-placeholder value"
15732
regex = '''(?i)JWT_SECRET\s*[:=]\s*['"][A-Za-z0-9!@#$%^&*()_+\-={}]{8,}['"]'''
15833
tags = ["stellabill", "jwt", "secret"]
159-
16034
[[rules]]
16135
id = "stellabill-admin-token-assignment"
16236
description = "ADMIN_TOKEN assigned a non-placeholder value"
16337
regex = '''(?i)ADMIN_TOKEN\s*[:=]\s*['"][A-Za-z0-9!@#$%^&*()_+\-={}]{8,}['"]'''
16438
tags = ["stellabill", "admin", "token"]
165-
16639
[[rules]]
16740
id = "stellabill-audit-hmac-assignment"
16841
description = "AUDIT_HMAC_SECRET assigned a non-placeholder value"
16942
regex = '''(?i)AUDIT_HMAC_SECRET\s*[:=]\s*['"][A-Za-z0-9!@#$%^&*()_+\-={}]{8,}['"]'''
170-
tags = ["stellabill", "audit", "hmac"]
43+
tags = ["stellabill", "audit", "hmac"]

internal/cache/cache.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package cache
22

33
import (
44
"context"
5-
"stellarbill-backend/internal/middleware"
5+
"stellarbill-backend/internal/servertiming"
66
"sync"
77
"time"
88
)
@@ -36,7 +36,7 @@ func NewInMemory() *InMemory {
3636
func (m *InMemory) Get(ctx context.Context, key string) ([]byte, error) {
3737
start := time.Now()
3838
defer func() {
39-
if rec := middleware.RecorderFromContext(ctx); rec != nil {
39+
if rec := servertiming.FromContext(ctx); rec != nil {
4040
rec.RecordCache(time.Since(start))
4141
}
4242
}()
@@ -58,7 +58,7 @@ func (m *InMemory) Get(ctx context.Context, key string) ([]byte, error) {
5858
func (m *InMemory) Set(ctx context.Context, key string, value []byte, ttl time.Duration) error {
5959
start := time.Now()
6060
defer func() {
61-
if rec := middleware.RecorderFromContext(ctx); rec != nil {
61+
if rec := servertiming.FromContext(ctx); rec != nil {
6262
rec.RecordCache(time.Since(start))
6363
}
6464
}()
@@ -75,7 +75,7 @@ func (m *InMemory) Set(ctx context.Context, key string, value []byte, ttl time.D
7575
func (m *InMemory) Delete(ctx context.Context, key string) error {
7676
start := time.Now()
7777
defer func() {
78-
if rec := middleware.RecorderFromContext(ctx); rec != nil {
78+
if rec := servertiming.FromContext(ctx); rec != nil {
7979
rec.RecordCache(time.Since(start))
8080
}
8181
}()

internal/cache/redis.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package cache
22

33
import (
44
"context"
5-
"stellarbill-backend/internal/middleware"
5+
"stellarbill-backend/internal/servertiming"
66
"time"
77

88
"github.com/redis/go-redis/v9"
@@ -37,7 +37,7 @@ func NewRedisFromURL(url string) (*Redis, error) {
3737
func (r *Redis) Get(ctx context.Context, key string) ([]byte, error) {
3838
start := time.Now()
3939
defer func() {
40-
if rec := middleware.RecorderFromContext(ctx); rec != nil {
40+
if rec := servertiming.FromContext(ctx); rec != nil {
4141
rec.RecordCache(time.Since(start))
4242
}
4343
}()
@@ -55,7 +55,7 @@ func (r *Redis) Get(ctx context.Context, key string) ([]byte, error) {
5555
func (r *Redis) Set(ctx context.Context, key string, value []byte, ttl time.Duration) error {
5656
start := time.Now()
5757
defer func() {
58-
if rec := middleware.RecorderFromContext(ctx); rec != nil {
58+
if rec := servertiming.FromContext(ctx); rec != nil {
5959
rec.RecordCache(time.Since(start))
6060
}
6161
}()
@@ -70,7 +70,7 @@ func (r *Redis) Set(ctx context.Context, key string, value []byte, ttl time.Dura
7070
func (r *Redis) Delete(ctx context.Context, key string) error {
7171
start := time.Now()
7272
defer func() {
73-
if rec := middleware.RecorderFromContext(ctx); rec != nil {
73+
if rec := servertiming.FromContext(ctx); rec != nil {
7474
rec.RecordCache(time.Since(start))
7575
}
7676
}()

internal/db/pool.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import (
44
"context"
55
"fmt"
66
"stellarbill-backend/internal/config"
7-
"stellarbill-backend/internal/middleware"
7+
"stellarbill-backend/internal/servertiming"
88
"time"
99

1010
"github.com/jackc/pgx/v5"
@@ -98,7 +98,7 @@ func (t *timingTracer) TraceQueryStart(ctx context.Context, _ *pgx.Conn, _ pgx.T
9898
func (t *timingTracer) TraceQueryEnd(ctx context.Context, _ *pgx.Conn, _ pgx.TraceQueryEndData) {
9999
startVal := ctx.Value(queryStartTimeKey{})
100100
if start, ok := startVal.(time.Time); ok {
101-
if rec := middleware.RecorderFromContext(ctx); rec != nil {
101+
if rec := servertiming.FromContext(ctx); rec != nil {
102102
rec.RecordDB(time.Since(start))
103103
}
104104
}

internal/middleware/timing.go

Lines changed: 9 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,84 +1,31 @@
11
package middleware
22

33
import (
4-
"context"
54
"fmt"
6-
"sync"
75
"time"
86

97
"github.com/gin-gonic/gin"
10-
)
11-
12-
// timingRecorderContextKey is the context key for the timing recorder.
13-
type timingRecorderContextKey struct{}
14-
15-
// ServerTimingRecorder tracks latencies for DB, cache, and outbox.
16-
type ServerTimingRecorder struct {
17-
mu sync.Mutex
18-
dbTotal time.Duration
19-
cacheTotal time.Duration
20-
outboxTotal time.Duration
21-
}
22-
23-
// RecordDB adds to the total DB duration.
24-
func (r *ServerTimingRecorder) RecordDB(d time.Duration) {
25-
if r == nil {
26-
return
27-
}
28-
r.mu.Lock()
29-
defer r.mu.Unlock()
30-
r.dbTotal += d
31-
}
32-
33-
// RecordCache adds to the total Cache duration.
34-
func (r *ServerTimingRecorder) RecordCache(d time.Duration) {
35-
if r == nil {
36-
return
37-
}
38-
r.mu.Lock()
39-
defer r.mu.Unlock()
40-
r.cacheTotal += d
41-
}
42-
43-
// RecordOutbox adds to the total Outbox duration.
44-
func (r *ServerTimingRecorder) RecordOutbox(d time.Duration) {
45-
if r == nil {
46-
return
47-
}
48-
r.mu.Lock()
49-
defer r.mu.Unlock()
50-
r.outboxTotal += d
51-
}
528

53-
// RecorderFromContext extracts the ServerTimingRecorder from the given context.
54-
func RecorderFromContext(ctx context.Context) *ServerTimingRecorder {
55-
if ctx == nil {
56-
return nil
57-
}
58-
val := ctx.Value(timingRecorderContextKey{})
59-
if rec, ok := val.(*ServerTimingRecorder); ok {
60-
return rec
61-
}
62-
return nil
63-
}
9+
"stellarbill-backend/internal/servertiming"
10+
)
6411

65-
// RecorderFromGinContext extracts the ServerTimingRecorder from the Gin context.
66-
func RecorderFromGinContext(c *gin.Context) *ServerTimingRecorder {
12+
// RecorderFromGinContext extracts the Recorder from the Gin context.
13+
func RecorderFromGinContext(c *gin.Context) *servertiming.Recorder {
6714
if c == nil {
6815
return nil
6916
}
70-
return RecorderFromContext(c.Request.Context())
17+
return servertiming.FromContext(c.Request.Context())
7118
}
7219

7320
// ServerTimingMiddleware intercepts the request and adds a Server-Timing header
7421
// reflecting DB, cache, outbox, and total handler time.
7522
func ServerTimingMiddleware() gin.HandlerFunc {
7623
return func(c *gin.Context) {
7724
start := time.Now()
78-
rec := &ServerTimingRecorder{}
25+
rec := &servertiming.Recorder{}
7926

8027
// Inject into request context
81-
ctx := context.WithValue(c.Request.Context(), timingRecorderContextKey{}, rec)
28+
ctx := servertiming.WithContext(c.Request.Context(), rec)
8229
c.Request = c.Request.WithContext(ctx)
8330

8431
// Wrap the ResponseWriter to hook before headers are written
@@ -98,7 +45,7 @@ func ServerTimingMiddleware() gin.HandlerFunc {
9845

9946
type serverTimingResponseWriter struct {
10047
gin.ResponseWriter
101-
recorder *ServerTimingRecorder
48+
recorder *servertiming.Recorder
10249
start time.Time
10350
written bool
10451
}
@@ -126,12 +73,7 @@ func (w *serverTimingResponseWriter) ensureHeaderWritten() {
12673
w.written = true
12774

12875
totalTime := time.Since(w.start)
129-
130-
w.recorder.mu.Lock()
131-
dbTime := w.recorder.dbTotal
132-
cacheTime := w.recorder.cacheTotal
133-
outboxTime := w.recorder.outboxTotal
134-
w.recorder.mu.Unlock()
76+
dbTime, cacheTime, outboxTime := w.recorder.Totals()
13577

13678
// Convert to milliseconds rounded to microsecond precision.
13779
// E.g. dbTime.Microseconds() = 1234 -> 1.234 ms.

internal/outbox/repository.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77
"errors"
88
"fmt"
99
"stellarbill-backend/internal/db"
10-
"stellarbill-backend/internal/middleware"
10+
"stellarbill-backend/internal/servertiming"
1111
"time"
1212

1313
"github.com/google/uuid"
@@ -37,7 +37,7 @@ func NewPostgresRepository(executor db.DBTX) Repository {
3737
func (r *postgresRepository) Store(ctx context.Context, event *Event) error {
3838
start := time.Now()
3939
defer func() {
40-
if rec := middleware.RecorderFromContext(ctx); rec != nil {
40+
if rec := servertiming.FromContext(ctx); rec != nil {
4141
rec.RecordOutbox(time.Since(start))
4242
}
4343
}()

0 commit comments

Comments
 (0)