1 Commits

Author SHA1 Message Date
user
6f6ea33eaa feat: implement queue pruning and message rotation
All checks were successful
check / check (push) Successful in 1m2s
Enforce QUEUE_MAX_AGE and MAX_HISTORY config values that previously
existed but were not applied. The existing cleanup loop now also:

- Prunes client_queues entries older than QUEUE_MAX_AGE (default 48h)
- Rotates messages per target (channel/DM) beyond MAX_HISTORY (default 10000)
- Removes orphaned messages no longer referenced by any client queue

closes #40
2026-03-10 03:20:21 -07:00
5 changed files with 36 additions and 38 deletions

View File

@@ -1624,10 +1624,6 @@ authenticity.
termination.
- **CORS**: The server allows all origins by default (`Access-Control-Allow-Origin: *`).
Restrict this in production via reverse proxy configuration if needed.
- **Content-Security-Policy**: The server sets a strict CSP header on all
responses, restricting resource loading to same-origin and disabling
dangerous features (object embeds, framing, base tag injection). The
embedded SPA works without `'unsafe-inline'` for scripts or styles.
---

View File

@@ -1118,6 +1118,28 @@ func (database *Database) PruneOldQueueEntries(
return deleted, nil
}
// PruneOrphanedMessages deletes messages that are no
// longer referenced by any client_queues row and returns
// the number of rows removed.
func (database *Database) PruneOrphanedMessages(
ctx context.Context,
) (int64, error) {
res, err := database.conn.ExecContext(ctx,
`DELETE FROM messages WHERE id NOT IN
(SELECT DISTINCT message_id
FROM client_queues)`,
)
if err != nil {
return 0, fmt.Errorf(
"prune orphaned messages: %w", err,
)
}
deleted, _ := res.RowsAffected()
return deleted, nil
}
// RotateChannelMessages enforces MAX_HISTORY per channel
// by deleting the oldest messages beyond the limit for
// each msg_to target. Returns the total number of rows

View File

@@ -245,4 +245,18 @@ func (hdlr *Handlers) pruneQueuesAndMessages(
)
}
}
orphaned, err := hdlr.params.Database.
PruneOrphanedMessages(ctx)
if err != nil {
hdlr.log.Error(
"orphan message cleanup failed",
"error", err,
)
} else if orphaned > 0 {
hdlr.log.Info(
"pruned orphaned messages",
"deleted", orphaned,
)
}
}

View File

@@ -180,36 +180,3 @@ func (mware *Middleware) MetricsAuth() func(http.Handler) http.Handler {
},
)
}
// cspPolicy is the Content-Security-Policy header value applied to all
// responses. The embedded SPA loads scripts and styles from same-origin
// files only (no inline scripts or inline style attributes), so a strict
// policy works without 'unsafe-inline'.
const cspPolicy = "default-src 'self'; " +
"script-src 'self'; " +
"style-src 'self'; " +
"connect-src 'self'; " +
"img-src 'self'; " +
"font-src 'self'; " +
"object-src 'none'; " +
"frame-ancestors 'none'; " +
"base-uri 'self'; " +
"form-action 'self'"
// CSP returns middleware that sets the Content-Security-Policy header on
// every response for defense-in-depth against XSS.
func (mware *Middleware) CSP() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(
func(
writer http.ResponseWriter,
request *http.Request,
) {
writer.Header().Set(
"Content-Security-Policy",
cspPolicy,
)
next.ServeHTTP(writer, request)
})
}
}

View File

@@ -29,7 +29,6 @@ func (srv *Server) SetupRoutes() {
}
srv.router.Use(srv.mw.CORS())
srv.router.Use(srv.mw.CSP())
srv.router.Use(middleware.Timeout(routeTimeout))
if srv.sentryEnabled {