Skip to content

feat: REST API layer for web dashboard#70

Merged
BillChirico merged 72 commits intomainfrom
feat/rest-api
Feb 17, 2026
Merged

feat: REST API layer for web dashboard#70
BillChirico merged 72 commits intomainfrom
feat/rest-api

Conversation

@BillChirico
Copy link
Collaborator

Summary

Add Express HTTP server running alongside the Discord WebSocket client, providing a REST API for the upcoming web dashboard.

Closes #29

Endpoints

Method Path Description
GET /api/v1/health Uptime, memory, Discord status
GET /api/v1/guilds/:id Guild info via Discord client
GET /api/v1/guilds/:id/config Read config
PATCH /api/v1/guilds/:id/config Update config value
GET /api/v1/guilds/:id/stats AI stats, message counts from DB
GET /api/v1/guilds/:id/members Paginated member list with roles
GET /api/v1/guilds/:id/moderation Paginated mod cases from DB
POST /api/v1/guilds/:id/actions Bot actions (sendMessage)

Middleware

  • Auth: x-api-secret header check against BOT_API_SECRET
  • Rate limit: Per-IP in-memory limiter (100 req/15min)
  • CORS: Restricted to DASHBOARD_URL origin
  • JSON body parsing + global error handler

Structure

src/api/
├── server.js          # Express app, middleware, start/stop
├── index.js           # Router mount
├── middleware/
│   ├── auth.js        # x-api-secret auth
│   └── rateLimit.js   # Per-IP rate limiter
└── routes/
    ├── health.js      # Health endpoint
    └── guilds.js      # Guild endpoints (info, config, stats, members, moderation, actions)

Integration

  • Starts in src/index.js after Discord login
  • Graceful shutdown before DB close
  • ENV vars: BOT_API_PORT, BOT_API_SECRET, DASHBOARD_URL

Tests

  • 5 test files covering all endpoints, auth, rate limiting, and server lifecycle
  • All 52 test files pass (965 tests)
  • Coverage well above 80%: 93.9% stmts, 85.4% branches, 87.9% funcs, 94.5% lines
  • API-specific coverage: 95%+ statements, 94%+ branches, 90%+ functions

Dependencies

  • Added express (already in package.json + lockfile)

Add Express HTTP server running alongside the Discord WebSocket client,
providing a REST API for the upcoming web dashboard.

Endpoints:
- GET /api/v1/health — uptime, memory, discord status
- GET /api/v1/guilds/:id — guild info via Discord client
- GET /api/v1/guilds/:id/config — read config
- PATCH /api/v1/guilds/:id/config — update config value
- GET /api/v1/guilds/:id/stats — AI stats, message counts from DB
- GET /api/v1/guilds/:id/members — paginated member list with roles
- GET /api/v1/guilds/:id/moderation — paginated mod cases from DB
- POST /api/v1/guilds/:id/actions — bot actions (sendMessage)

Middleware:
- x-api-secret header authentication (BOT_API_SECRET)
- Per-IP rate limiting (in-memory)
- CORS restricted to DASHBOARD_URL origin
- JSON body parsing, global error handler

Structure:
- src/api/server.js — Express app, middleware, start/stop
- src/api/index.js — router mount
- src/api/middleware/auth.js — auth middleware
- src/api/middleware/rateLimit.js — rate limiter
- src/api/routes/health.js — health endpoint
- src/api/routes/guilds.js — guild endpoints

Integration:
- Starts in src/index.js after Discord login
- Graceful shutdown before DB close

Tests: 5 test files, all endpoints tested with mocked
Discord client and DB pool. 80%+ coverage on all metrics.

Closes #29
@claude
Copy link

claude bot commented Feb 17, 2026

Claude Code is working…

I'll analyze this and get back to you.

View job run

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 17, 2026

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added REST API server with guild management endpoints for retrieving guild information, configuration, statistics, members, moderation cases, and bot actions.
    • Implemented API health check endpoint.
    • Added API authentication and rate limiting protection.
    • Added guild ID tracking for conversations.
  • Chores

    • Updated configuration with new API port and URL environment variables.

Walkthrough

Adds an Express REST API (health and guild endpoints) with timing-safe x-api-secret auth, in-memory per-IP rate limiting, and lifecycle functions (createApp/startServer/stopServer). Threads optional guildId into AI history and conversations, updates example envs, DB schema, and adds comprehensive tests.

Changes

Cohort / File(s) Summary
Configuration & Packages
\.env.example, package.json
Enable BOT-related envs (BOT_API_PORT, BOT_API_URL, DASHBOARD_URL) in example; add express dependency and supertest devDependency.
API Aggregator & Server
src/api/index.js, src/api/server.js
Add router aggregator and Express app lifecycle (createApp, startServer, stopServer); mount routes under /api/v1, conditional CORS, JSON parsing, rate limiting, centralized error handling, and graceful start/stop.
Authentication Middleware
src/api/middleware/auth.js
Add timing-safe isValidSecret(secret) and requireAuth() middleware enforcing x-api-secret, returning structured 401 responses and logging misconfiguration.
Rate-Limit Middleware
src/api/middleware/rateLimit.js
Add in-memory per-IP rate limiter with configurable windowMs/max, periodic cleanup (unref), 429 + Retry-After behavior, and destroy() to stop cleanup.
Routes: Health & Guilds
src/api/routes/health.js, src/api/routes/guilds.js
Add GET /health (augments response when secret valid) and authenticated guild routes (info, config GET/PATCH, stats, members pagination, moderation, actions like sendMessage) with validation and DB-backed operations.
Bot Integration & AI
src/index.js, src/modules/ai.js, src/modules/events.js
Start/stop API server during bot lifecycle; thread optional guildId through addToHistory and generateResponse; events now pass message.guild?.id into AI flows.
Database Schema & Backfill
src/db.js, tests/db.test.js
Add nullable guild_id column to conversations, guarded ALTER TABLE backfill and index creation (idx_conversations_guild_id); test asserts index presence.
Tests
tests/api/middleware/*.test.js, tests/api/routes/*.test.js, tests/api/server.test.js, tests/modules/*.test.js, tests/db.test.js
Add comprehensive tests covering auth, rate limiter, health, guild routes, server lifecycle, AI guildId persistence, and DB index; use mocks, fake timers, and supertest.
Documentation
AGENTS.md, README.md
Document new API layer and update local/dev notes and env examples to reflect the bot REST API and dashboard integration.

Suggested reviewers

  • claude
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Linked Issues check ❓ Inconclusive Implementation covers /api/v1 router and core endpoints, but diverges from issue requirements on OAuth-based Discord token verification, per-user rate limits, credential-supporting CORS, and OpenAPI documentation. Clarify whether OAuth-based auth (originally required) was intentionally replaced with BOT_API_SECRET header auth, and plan for OpenAPI/Swagger docs and per-user rate limiting in follow-ups.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: REST API layer for web dashboard' is concise (38 characters), descriptive, and clearly summarizes the main change introducing a REST API for the web dashboard.
Description check ✅ Passed The description is directly related to the changeset, providing comprehensive details about endpoints, middleware, structure, integration, tests, and dependencies that align with the code changes.
Out of Scope Changes check ✅ Passed All changes are scoped to REST API implementation, including new middleware, routes, server lifecycle, database schema updates for guild tracking, and comprehensive tests; no unrelated modifications detected.
Docstring Coverage ✅ Passed Docstring coverage is 93.33% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/rest-api

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

1 critical, 5 warnings, 2 nitpicks found across the REST API implementation.

🔴 Critical (must fix)

  • Mention sanitization bypass (guilds.js:222): channel.send(content) sends raw user input to Discord without using safeSend(). An API caller could trigger @everyone/@here pings, bypassing the defense-in-depth sanitization used everywhere else in the codebase.

🟡 Warnings (should fix)

  • Timing-safe secret comparison (auth.js:24): String comparison vulnerable to timing attacks. Use crypto.timingSafeEqual().
  • Config endpoint exposes full config (guilds.js:69): GET /:id/config returns the entire bot config, potentially including sensitive data (DB credentials, API keys, internal URLs).
  • Config PATCH is not guild-scoped (guilds.js:77-96): The guild ID parameter is cosmetic — setConfigValue() modifies global config. No allowlist restricts which paths can be modified via API.
  • Conversations query not guild-scoped (guilds.js:110): Stats endpoint counts all conversations globally, not filtered to the requested guild.
  • Members pagination is broken (guilds.js:133-137): guild.members.fetch({ limit }) doesn't support offset — page 2+ returns the same results as page 1.

🔵 Nitpicks

  • User input reflected in error response (guilds.js:230)
  • CORS Access-Control-Allow-Methods includes DELETE but no DELETE routes exist

See inline comments for details and suggested fixes.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6d0c0e2 and a1da2d3.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (14)
  • .env.example
  • package.json
  • src/api/index.js
  • src/api/middleware/auth.js
  • src/api/middleware/rateLimit.js
  • src/api/routes/guilds.js
  • src/api/routes/health.js
  • src/api/server.js
  • src/index.js
  • tests/api/middleware/auth.test.js
  • tests/api/middleware/rateLimit.test.js
  • tests/api/routes/guilds.test.js
  • tests/api/routes/health.test.js
  • tests/api/server.test.js
🧰 Additional context used
📓 Path-based instructions (3)
**/*.js

📄 CodeRabbit inference engine (AGENTS.md)

**/*.js: Use ESM modules with import/export syntax, never use require()
Always use node: protocol prefix for Node.js builtin imports (e.g., import { readFileSync } from 'node:fs')
Always use semicolons at the end of statements
Use single quotes for string literals, enforced by Biome
Use 2-space indentation throughout the codebase, enforced by Biome

Files:

  • src/api/middleware/auth.js
  • tests/api/server.test.js
  • tests/api/middleware/auth.test.js
  • src/api/routes/guilds.js
  • src/api/server.js
  • src/api/index.js
  • src/api/routes/health.js
  • tests/api/routes/health.test.js
  • src/api/middleware/rateLimit.js
  • tests/api/routes/guilds.test.js
  • tests/api/middleware/rateLimit.test.js
  • src/index.js
src/**/*.js

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.js: Never use console.log(), console.warn(), console.error(), or any other console.* method in src/ files. Always use Winston logger instead: import { info, warn, error } from '../logger.js' and log with structured metadata
Use custom error classes from src/utils/errors.js for error handling. Always log errors with context before re-throwing
Use splitMessage() utility to handle Discord's 2000-character message limit for outgoing messages
Use safeSend() wrappers from src/utils/safeSend.js for all outgoing messages, and use sanitizeMentions() from src/utils/sanitizeMentions.js to strip @everyone/@here via zero-width space insertion
Any new code must include tests. Run pnpm test before every commit. Maintain minimum 80% code coverage on statements, branches, functions, and lines using @vitest/coverage-v8. PRs that drop coverage below 80% will fail CI
Write JSDoc comments for documentation instead of TypeScript, as the project uses plain JavaScript without TypeScript

Files:

  • src/api/middleware/auth.js
  • src/api/routes/guilds.js
  • src/api/server.js
  • src/api/index.js
  • src/api/routes/health.js
  • src/api/middleware/rateLimit.js
  • src/index.js
src/index.js

📄 CodeRabbit inference engine (AGENTS.md)

src/index.js: Enforce Discord intents: the bot requires MessageContent, GuildMembers, and GuildVoiceStates intents to be enabled in src/index.js client setup
Tempban scheduler runs on a 60-second interval. Started in src/index.js startup and stopped in graceful shutdown. Catches up on missed unbans after restart
The PostgreSQL logging transport is a long-lived Winston transport that requires reactive onConfigChange wiring in src/index.js startup to add/remove/recreate the transport when logging.database.* settings change at runtime

Files:

  • src/index.js
🧠 Learnings (2)
📚 Learning: 2025-10-10T15:05:26.145Z
Learnt from: CR
Repo: BillChirico/LUA-Obfuscator PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-10-10T15:05:26.145Z
Learning: Applies to package.json : Only add new packages when absolutely necessary or explicitly requested

Applied to files:

  • package.json
📚 Learning: 2026-02-17T02:29:07.358Z
Learnt from: CR
Repo: BillChirico/bills-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-17T02:29:07.358Z
Learning: Applies to src/**/*.js : Any new code must include tests. Run `pnpm test` before every commit. Maintain minimum 80% code coverage on statements, branches, functions, and lines using vitest/coverage-v8. PRs that drop coverage below 80% will fail CI

Applied to files:

  • package.json
  • tests/api/server.test.js
🧬 Code graph analysis (11)
src/api/middleware/auth.js (1)
src/logger.js (1)
  • warn (224-226)
tests/api/server.test.js (1)
src/api/server.js (6)
  • stopServer (89-107)
  • app (22-22)
  • app (68-68)
  • createApp (21-58)
  • server (12-12)
  • startServer (67-82)
tests/api/middleware/auth.test.js (1)
src/api/middleware/auth.js (1)
  • requireAuth (14-31)
src/api/routes/guilds.js (3)
src/index.js (2)
  • client (79-88)
  • config (57-57)
src/modules/config.js (3)
  • getConfig (133-135)
  • setConfigValue (222-311)
  • err (33-33)
src/logger.js (2)
  • info (217-219)
  • error (231-233)
src/api/index.js (3)
src/api/routes/guilds.js (1)
  • router (10-10)
src/api/routes/health.js (1)
  • router (8-8)
src/api/middleware/auth.js (1)
  • requireAuth (14-31)
src/api/routes/health.js (3)
src/api/index.js (1)
  • router (11-11)
src/api/routes/guilds.js (6)
  • router (10-10)
  • req (32-32)
  • req (78-78)
  • req (102-102)
  • req (164-164)
  • req (200-200)
src/index.js (1)
  • client (79-88)
tests/api/routes/health.test.js (2)
src/index.js (1)
  • client (79-88)
src/api/server.js (3)
  • createApp (21-58)
  • app (22-22)
  • app (68-68)
src/api/middleware/rateLimit.js (1)
tests/api/middleware/rateLimit.test.js (3)
  • req (6-6)
  • res (7-7)
  • next (8-8)
tests/api/routes/guilds.test.js (2)
src/api/server.js (3)
  • app (22-22)
  • app (68-68)
  • createApp (21-58)
src/modules/config.js (2)
  • getConfig (133-135)
  • setConfigValue (222-311)
tests/api/middleware/rateLimit.test.js (1)
src/api/middleware/rateLimit.js (1)
  • rateLimit (15-52)
src/index.js (1)
src/api/server.js (2)
  • stopServer (89-107)
  • startServer (67-82)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: claude-review
🔇 Additional comments (6)
.env.example (1)

47-59: LGTM!

Environment variable entries are well-documented with sensible defaults and align with the new API server configuration needs.

src/api/index.js (1)

1-19: LGTM!

Clean route aggregation with appropriate separation of public (/health) and authenticated (/guilds) endpoints.

tests/api/routes/health.test.js (1)

1-50: LGTM!

Good test structure — the mock client accurately mirrors the shape accessed by the health route, and the tests validate both response structure and the public (no-auth) access requirement.

package.json (1)

22-22: Express 5.2.1 is stable and the code is compatible.

Express 5.0.0 has been stable since September 2024, became the npm default in March 2025, and is now well-established (February 2026). The route handlers and middleware use no deprecated Express methods and are fully compatible with Express 5's semantics—req.query, req.params, req.body, and middleware patterns all work correctly.

tests/api/middleware/auth.test.js (1)

1-70: LGTM!

Good coverage of all auth middleware branches — unconfigured secret, missing header, mismatched header, and valid header. Mock setup and teardown are clean.

tests/api/routes/guilds.test.js (1)

1-387: LGTM!

Thorough test coverage across all guild endpoints — happy paths, validation errors, DB unavailability, and Discord API failures. Good verification of parameterized queries for SQL injection prevention.

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/api/middleware/auth.js`:
- Around line 24-27: Replace the plain equality check with a timing-safe
comparison using node:crypto: import crypto and convert secret and expected to
Buffers (e.g. secretBuf = Buffer.from(secret || '') and expectedBuf =
Buffer.from(expected)); if their lengths differ, call crypto.timingSafeEqual
against expectedBuf and a dummy Buffer.alloc(expectedBuf.length) to keep timing
consistent, then return 401; if lengths match, use
crypto.timingSafeEqual(secretBuf, expectedBuf) and if it returns false, log the
warn('Unauthorized API request', { ip: req.ip, path: req.path }) and return
res.status(401).json({ error: 'Unauthorized' }). Ensure you update the
conditional in the middleware (the block containing secret and expected) and
keep the existing log and response behavior.

In `@src/api/middleware/rateLimit.js`:
- Around line 15-51: The rateLimit factory currently creates a cleanup interval
that is never cleared; update rateLimit to expose a way to stop that interval by
returning the middleware function augmented with a cleanup method (e.g.,
middleware.close or middleware.destroy) that calls clearInterval(cleanup) (keep
cleanup.unref()); specifically, in rateLimit() keep the const cleanup =
setInterval(...) and then before returning create const middleware =
(req,res,next)=>{...}; add middleware.destroy = () => clearInterval(cleanup);
and return middleware instead of the raw function so callers (tests or app
shutdown) can call middleware.destroy() to avoid timer leaks.

In `@src/api/routes/guilds.js`:
- Around line 133-137: The members endpoint currently calls
req.guild.members.fetch({ limit }) and ignores offset-based page semantics from
parsePagination; update router.get('/:id/members') to support cursor-based
pagination: accept an 'after' (snowflake) query param instead of (or in addition
to) page, pass { limit, after } into req.guild.members.fetch, return the fetched
members plus the next cursor (the last member's id) so clients can page forward;
also adjust parsePagination usage and response shape to include the next_after
cursor (or remove/ignore page entirely and document the change) to avoid
misleading page-based pagination behavior.
- Around line 229-231: The response currently reflects unsanitized user input
(action) back to the client in res.status(400).json({ error: `Unknown action:
${action}` });; update the error to avoid echoing raw input by returning a
generic message such as { error: "Unknown action" } or, if you must include the
value, include a sanitized/truncated form (e.g., escape and limit length) before
adding it to the JSON. Locate the handler that sets this response and replace
the template interpolation of action with the generic text or sanitized
variable.
- Around line 221-224: The code sends raw user-supplied content via
channel.send, which can ping everyone and exceed Discord limits; replace the
direct channel.send with the safeSend wrapper and sanitize the content first by
calling sanitizeMentions(req.body.content) (or sanitizeMentions(content)) to
strip `@everyone/`@here, then call safeSend(channel, sanitizedContent) instead of
channel.send; update the response to use the returned message from safeSend
(message.id) and ensure sanitizeMentions and safeSend are imported/required in
this module; remove any direct channel.send usage in this try block.
- Around line 109-114: The conversations count query is currently unscoped and
returns a global total; update the dbPool.query that produces conversationResult
to restrict conversations to the requested guild by joining conversations to the
channels table on conversations.channel_id = channels.id (or the correct PK) and
adding WHERE channels.guild_id = $1 (use req.params.id as the parameter), e.g.
replace the SELECT COUNT(*)... FROM conversations with a SELECT COUNT(*)::int AS
count FROM conversations JOIN channels ON conversations.channel_id = channels.id
WHERE channels.guild_id = $1; keep the same Promise.all structure and parameter
ordering so conversationResult and caseResult remain unchanged.

In `@src/api/routes/health.js`:
- Around line 14-27: The health route handler (router.get) currently returns
full process.memoryUsage() and Discord internals (client.ws.status,
client.ws.ping, client.guilds.cache.size) to unauthenticated callers; remove or
sanitize detailed memory metrics and/or gate the detailed response behind
authentication. Update the handler so the public response only includes minimal
info (e.g., status: 'ok' and uptime) and move detailed fields (memory object,
client.ws.*, guild counts) behind an auth check or an isAdmin middleware; modify
the router.get callback to check req.user/auth and conditionally attach the
detailed diagnostics instead of always returning process.memoryUsage().

In `@src/api/server.js`:
- Around line 67-82: The startServer function overwrites the module-scoped
server variable and leaks an existing HTTP server; update startServer to detect
an existing server (the module-level server variable), gracefully close it (call
server.close and await its callback or wrap in a Promise), handle/propagate any
close errors, clear the old reference, then create and assign the new server
from createApp(...). Ensure the server 'error' and 'listening' handlers remain
attached to the new instance and that server is set to null after a successful
close or on failure to avoid orphaned sockets.
- Around line 33-43: The middleware registered with app.use (the anonymous
function that references dashboardUrl) currently returns res.status(204) for all
OPTIONS requests even when dashboardUrl is falsy; change the logic so the
preflight short-circuit only runs when dashboardUrl is set: set the CORS headers
when dashboardUrl is truthy and only return 204 for OPTIONS if dashboardUrl is
truthy (otherwise call next()), updating the anonymous middleware in
src/api/server.js that uses dashboardUrl to guard the OPTIONS response.

In `@src/index.js`:
- Around line 456-458: The startup sequence currently awaits startServer(client,
dbPool) without handling rejection, which can leave client logged in, schedulers
running, and dbPool open if the server fails; wrap the startServer call in a
try/catch inside startup() and on error either (a) log a warning and continue
(so the bot remains functional without the REST API) or (b) call
gracefulShutdown() to clean up client and dbPool then rethrow/exit; reference
the startServer(...) call, startup(), gracefulShutdown(), client, and dbPool
when making the change.

In `@tests/api/middleware/rateLimit.test.js`:
- Around line 67-87: The test uses vi.useFakeTimers() but calls
vi.useRealTimers() only at the end of the test, which can leak fake timers if an
assertion fails; change the test to always restore timers by either adding a
global afterEach(() => vi.useRealTimers()) for this test suite or wrap the
fake-timer section in a try/finally so vi.useRealTimers() is guaranteed to run;
refer to the rateLimit.test.js test case (the it block creating middleware via
rateLimit({ windowMs: 1000, max: 1 })) and ensure vi.useFakeTimers() is paired
with a guaranteed cleanup (vi.useRealTimers()) to avoid affecting other tests.

In `@tests/api/server.test.js`:
- Around line 42-59: Remove the redundant vi.unstubAllEnvs() calls from the
individual tests (e.g., inside the "should parse JSON request bodies" test and
the other tests that duplicate cleanup); the global afterEach cleanup already
calls vi.unstubAllEnvs(), so simply delete the per-test vi.unstubAllEnvs()
invocations and leave the tests to rely on the existing afterEach block to
restore env stubs.

BillChirico and others added 11 commits February 16, 2026 23:56
Replace channel.send() with safeSend() in the actions endpoint to ensure
mention sanitization and safe allowedMentions. Add content length
validation to reject messages exceeding Discord's 2000-char limit.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Add SAFE_CONFIG_KEYS allowlist to only expose safe config sections
(ai, welcome, spam, moderation, logging) via GET. Restrict PATCH to
only allow modifying keys under safe prefixes, returning 403 otherwise.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Move the OPTIONS 204 response inside the dashboardUrl check so that
when CORS is unconfigured, OPTIONS requests fall through to normal
routing instead of returning an empty 204.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
If startServer is called while a server is already running, close the
existing server first to prevent orphaned listeners leaking resources.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Discord API does not support offset-based pagination for members.
Switch from guild.members.fetch() to guild.members.list() which uses
cursor-based pagination with an "after" parameter (user ID).

The endpoint now accepts ?limit=25&after=<userId> instead of
?page=1&limit=25 and returns nextAfter cursor for the next page.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Remove process.memoryUsage() from the default health response to avoid
exposing server internals without authentication. Memory info is now
only included when a valid x-api-secret header is provided.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Wrap startServer in try/catch so the Discord bot continues running
even if the HTTP API server fails to bind its port.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
The conversations table has no guild_id column, so we scope
the stats count to the requesting guild by filtering on
channel_id using the guild's channel cache (channel_id = ANY).

Resolves PR #70 review threads:
- PRRT_kwDORICdSM5u9e-S (conversations query not guild-scoped)
- PRRT_kwDORICdSM5u9fwJ (same issue, coderabbitai)
- PRRT_kwDORICdSM5u9fG8 (same issue, claude)
Move vi.useRealTimers() to afterEach so timers are always restored
even if a test fails before the inline cleanup runs.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Remove redundant vi.unstubAllEnvs() calls from individual tests
and add it to the shared afterEach block for consistent cleanup.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Copy link

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 warnings found on the current code. CI passes and previous review iterations have addressed the major issues well.

🟡 Warnings

  1. Unguarded CREATE INDEX in src/db.js:122-127: The CREATE INDEX IF NOT EXISTS idx_conversations_guild_id is outside the try/catch that wraps the ALTER TABLE. If the ALTER TABLE fails (PG < 9.6) on an existing database where the column doesn't exist, this throws an unhandled error that crashes initDb() and prevents bot startup. Wrap it in its own try/catch.

  2. Shutdown timeout doesn't resolve promise on Node < 18.2 (src/api/server.js:134-139): When closeAllConnections is unavailable, the timeout fires, logs a warning, but the stopServer() promise never resolves — hanging graceful shutdown indefinitely. Add a fallback resolve() in the else branch.

See inline comments for suggested fixes.

Copy link

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 warnings, 2 nitpicks found. CI passes. The implementation has been iterated extensively (70 commits) and the vast majority of issues from prior review rounds have been well-addressed.

🟡 Warnings

  1. CREATE INDEX on guild_id will crash initDb if ALTER TABLE failed (src/db.js:122-127): The index creation is outside the try/catch that guards the ALTER TABLE ADD COLUMN IF NOT EXISTS. On PG < 9.6 where the ALTER fails, this will throw an unhandled error and prevent bot startup. Wrap in its own try/catch.
  2. Health endpoint doesn't guard against client.ws being unavailable (src/api/routes/health.js:18): If the health check is hit before Discord's WebSocket is fully initialized, accessing client.ws.status could throw.

🔵 Nitpicks

  1. Error handler logs full stack trace for 4xx client errors (src/api/server.js:63): Adds noise to logs for expected errors like malformed JSON.
  2. Missing explicit sanitizeMentions() call before safeSend (src/api/routes/guilds.js:307): Not a security issue (safeSend sanitizes internally), but deviates from the project convention stated in AGENTS.md.

See inline comments for details.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

- Add explicit sanitizeMentions() call before safeSend in guilds actions endpoint
- Wrap CREATE INDEX in try/catch for PG < 9.6 compatibility
- Add client?.ws defensive guard in health endpoint
- Rename _req to req in config GET handler
- Add resolve() to shutdown timeout so promise completes
- Only log stack traces for 5xx errors, not 4xx
- Validate config path has no empty segments
claude[bot]
claude bot previously approved these changes Feb 17, 2026
Copy link

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — this PR has been through extensive review iteration (71 commits, 10+ review rounds) and all critical and warning-level issues have been addressed. The 2 remaining nitpicks (double sanitization, channel type enum documentation) are non-blocking.

Key items verified as resolved: timing-safe auth with shared isValidSecret helper, safeSend for mention sanitization + message splitting, config key allowlist (SAFE_CONFIG_KEYS/READABLE_CONFIG_KEYS), cursor-based member pagination, input reflection prevention, CORS OPTIONS guarding, startServer double-start protection, non-fatal API startup, conversations guild scoping, health endpoint auth gating, NaN port handling with range validation, rateLimit destroy() lifecycle, shutdown timeout with closeAllConnections(), error handler respecting err.status for 4xx codes, req.body guard for Express 5, string type check for content, explicit column list in moderation query, documentation updates in AGENTS.md and README.md.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fafb83d and f41bf18.

📒 Files selected for processing (6)
  • README.md
  • src/api/routes/guilds.js
  • src/api/routes/health.js
  • src/api/server.js
  • src/db.js
  • tests/api/server.test.js
🧰 Additional context used
📓 Path-based instructions (2)
**/*.js

📄 CodeRabbit inference engine (AGENTS.md)

**/*.js: Use ESM modules with import/export syntax, never use require()
Always use node: protocol prefix for Node.js builtin imports (e.g., import { readFileSync } from 'node:fs')
Always use semicolons at the end of statements
Use single quotes for string literals, enforced by Biome
Use 2-space indentation throughout the codebase, enforced by Biome

Files:

  • src/api/server.js
  • src/api/routes/health.js
  • tests/api/server.test.js
  • src/api/routes/guilds.js
  • src/db.js
src/**/*.js

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.js: Never use console.log(), console.warn(), console.error(), or any other console.* method in src/ files. Always use Winston logger instead: import { info, warn, error } from '../logger.js' and log with structured metadata
Use custom error classes from src/utils/errors.js for error handling. Always log errors with context before re-throwing
Use splitMessage() utility to handle Discord's 2000-character message limit for outgoing messages
Use safeSend() wrappers from src/utils/safeSend.js for all outgoing messages, and use sanitizeMentions() from src/utils/sanitizeMentions.js to strip @everyone/@here via zero-width space insertion
Any new code must include tests. Run pnpm test before every commit. Maintain minimum 80% code coverage on statements, branches, functions, and lines using @vitest/coverage-v8. PRs that drop coverage below 80% will fail CI
Write JSDoc comments for documentation instead of TypeScript, as the project uses plain JavaScript without TypeScript

Files:

  • src/api/server.js
  • src/api/routes/health.js
  • src/api/routes/guilds.js
  • src/db.js
🧠 Learnings (11)
📚 Learning: 2026-02-17T02:29:07.358Z
Learnt from: CR
Repo: BillChirico/bills-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-17T02:29:07.358Z
Learning: Keep documentation files up to date after every code change: update README.md for setup/config changes, update AGENTS.md for architecture/key files changes, update CONTRIBUTING.md for workflow changes, update .env.example for environment variables, update config.json for new config sections

Applied to files:

  • src/api/server.js
📚 Learning: 2026-02-11T17:18:14.598Z
Learnt from: CR
Repo: BillChirico/bills-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-11T17:18:14.598Z
Learning: See AGENTS.md for full project context, architecture, and coding guidelines

Applied to files:

  • src/api/server.js
📚 Learning: 2026-02-17T02:29:07.358Z
Learnt from: CR
Repo: BillChirico/bills-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-17T02:29:07.358Z
Learning: Applies to src/**/*.js : Use custom error classes from `src/utils/errors.js` for error handling. Always log errors with context before re-throwing

Applied to files:

  • src/api/server.js
📚 Learning: 2026-02-17T02:29:07.358Z
Learnt from: CR
Repo: BillChirico/bills-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-17T02:29:07.358Z
Learning: Applies to src/**/*.js : Any new code must include tests. Run `pnpm test` before every commit. Maintain minimum 80% code coverage on statements, branches, functions, and lines using vitest/coverage-v8. PRs that drop coverage below 80% will fail CI

Applied to files:

  • tests/api/server.test.js
📚 Learning: 2026-02-17T02:29:07.358Z
Learnt from: CR
Repo: BillChirico/bills-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-17T02:29:07.358Z
Learning: Applies to src/**/*.js : Use `safeSend()` wrappers from `src/utils/safeSend.js` for all outgoing messages, and use `sanitizeMentions()` from `src/utils/sanitizeMentions.js` to strip everyone/here via zero-width space insertion

Applied to files:

  • src/api/routes/guilds.js
📚 Learning: 2026-02-17T02:29:07.358Z
Learnt from: CR
Repo: BillChirico/bills-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-17T02:29:07.358Z
Learning: Applies to src/**/*.js : Use `splitMessage()` utility to handle Discord's 2000-character message limit for outgoing messages

Applied to files:

  • src/api/routes/guilds.js
📚 Learning: 2026-02-17T02:29:07.358Z
Learnt from: CR
Repo: BillChirico/bills-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-17T02:29:07.358Z
Learning: Applies to src/modules/moderation.js : Case numbering must be per-guild sequential and assigned atomically using `COALESCE(MAX(case_number), 0) + 1` in a single INSERT statement inside `createCase()`

Applied to files:

  • src/api/routes/guilds.js
  • src/db.js
📚 Learning: 2026-02-17T02:29:07.358Z
Learnt from: CR
Repo: BillChirico/bills-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-17T02:29:07.358Z
Learning: Applies to src/index.js : Enforce Discord intents: the bot requires MessageContent, GuildMembers, and GuildVoiceStates intents to be enabled in `src/index.js` client setup

Applied to files:

  • src/api/routes/guilds.js
📚 Learning: 2026-02-17T02:29:07.358Z
Learnt from: CR
Repo: BillChirico/bills-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-17T02:29:07.358Z
Learning: Applies to src/index.js : The PostgreSQL logging transport is a long-lived Winston transport that requires reactive `onConfigChange` wiring in `src/index.js` startup to add/remove/recreate the transport when `logging.database.*` settings change at runtime

Applied to files:

  • src/api/routes/guilds.js
  • src/db.js
📚 Learning: 2026-02-17T02:29:07.358Z
Learnt from: CR
Repo: BillChirico/bills-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-17T02:29:07.358Z
Learning: Applies to src/commands/**/*.js : Discord timeouts have a maximum duration of 28 days; Discord slowmode has a maximum duration of 6 hours (21600 seconds). Enforce these caps in command logic

Applied to files:

  • src/api/routes/guilds.js
📚 Learning: 2026-02-17T02:29:07.358Z
Learnt from: CR
Repo: BillChirico/bills-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-17T02:29:07.358Z
Learning: Applies to src/**/*.js : Never use `console.log()`, `console.warn()`, `console.error()`, or any other `console.*` method in src/ files. Always use Winston logger instead: `import { info, warn, error } from '../logger.js'` and log with structured metadata

Applied to files:

  • src/db.js
🧬 Code graph analysis (4)
src/api/server.js (5)
src/index.js (2)
  • client (79-88)
  • dbPool (308-308)
src/api/routes/guilds.js (5)
  • req (57-57)
  • req (126-126)
  • req (166-166)
  • req (241-241)
  • req (281-281)
src/api/routes/health.js (1)
  • req (18-18)
src/api/middleware/rateLimit.js (1)
  • rateLimit (15-56)
src/logger.js (3)
  • error (231-233)
  • warn (224-226)
  • info (217-219)
src/api/routes/health.js (3)
src/api/index.js (1)
  • router (11-11)
src/index.js (1)
  • client (79-88)
src/api/middleware/auth.js (1)
  • isValidSecret (15-24)
src/api/routes/guilds.js (4)
src/modules/config.js (3)
  • getConfig (133-135)
  • setConfigValue (222-311)
  • err (33-33)
src/logger.js (2)
  • info (217-219)
  • error (231-233)
src/utils/sanitizeMentions.js (1)
  • sanitizeMentions (37-43)
src/utils/safeSend.js (1)
  • safeSend (116-123)
src/db.js (1)
src/logger.js (1)
  • warn (224-226)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Cursor Bugbot
  • GitHub Check: claude-review
🔇 Additional comments (10)
src/db.js (2)

7-7: LGTM!

Import of warn from the local logger is consistent with existing info and logError imports and follows the project's logging guidelines.


104-104: LGTM!

Adding guild_id TEXT (nullable) to the CREATE TABLE is appropriate — new rows can populate it while existing databases get backfilled separately.

README.md (1)

374-380: LGTM — documentation updates accurately reflect the new REST API surface.

The README correctly documents the bot's REST API server, its default port, and service details.

Also applies to: 447-447

src/api/routes/health.js (1)

1-46: LGTM — well-structured health endpoint with appropriate auth gating.

Previous review concerns (duplicated timing-safe logic, unauthenticated memory exposure) have been properly addressed. The defensive guard for pre-login state and conditional detail exposure are solid.

src/api/server.js (3)

36-45: CORS middleware correctly guards preflight behind DASHBOARD_URL.

Past review concern about OPTIONS responding 204 without CORS headers has been addressed. The early return on falsy dashboardUrl is clean.


87-117: Server lifecycle is well-structured with proper double-start guard and port validation.

Previous review concerns (double-start leak, NaN port, port range, startup error listener) have all been addressed. The once('error', ...) pattern for the startup listener is correct.


50-56: Rate limiter cleanup on re-creation is properly handled.

Previous concern about leaked rate limiter intervals has been addressed by destroying the old limiter before creating a new one, and cleaning up in stopServer.

tests/api/server.test.js (1)

1-114: LGTM — clean test suite with good lifecycle coverage.

Previous concerns (redundant vi.unstubAllEnvs(), delete process.env bypassing stub tracking) have been addressed. Using port 0 for dynamic assignment is a good practice for avoiding port conflicts in tests.

src/api/routes/guilds.js (2)

1-11: Good use of sanitizeMentions and safeSend per coding guidelines.

Previous critical concern about unsanitized content being sent to Discord has been properly addressed with both sanitizeMentions() and safeSend().


93-112: Config scope is clearly documented with issue reference.

The global-config-not-per-guild concern from a previous review is now explicitly documented in both the JSDoc and the API response with a reference to Issue #71.

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/api/routes/guilds.js`:
- Around line 254-257: The long single-line SQL in the dbPool.query call reduces
readability; change the SQL argument passed to dbPool.query (the SELECT from
mod_cases) into a multi-line template literal (backticks) with one column per
line and proper indentation, preserving the explicit column list and the
parameter placeholders ($1, $2, $3) and leaving the parameter array
([req.params.id, limit, offset]) unchanged so the call signature and
functionality of dbPool.query remain the same.
- Around line 312-317: The handler currently calls sanitizeMentions and safeSend
(which may return an array of Message objects when content >2k) and then returns
sent.content from the first chunk; update the endpoint's JSDoc for this
route/handler to explicitly state that safeSend can split messages and the API's
content field will contain only the first chunk when splitting occurs (mention
safeSend and the response shape { id, channelId, content }), so consumers know
the returned content may be truncated and should fetch the full message(s) if
needed.

In `@src/api/server.js`:
- Around line 138-158: The timeout branch of the shutdown Promise leaves the
module-level server reference intact, causing a stale server to remain; update
the timeout handler (the block that uses SHUTDOWN_TIMEOUT_MS and calls
closing.closeAllConnections()) to set server = null before resolving the Promise
so subsequent startServer calls don't see a non-null server and trigger another
stop; keep the call to closing.closeAllConnections() and then set server = null
and resolve() in that order to ensure the server reference is cleared even on
forced timeout.

---

Duplicate comments:
In `@src/api/routes/guilds.js`:
- Around line 178-185: The conversations count was updated to correctly filter
by guild_id; ensure both dbPool.query calls use the same parameter
(req.params.id) as shown and keep the variables conversationResult and
caseResult intact; no additional code changes needed beyond confirming the
SELECT COUNT(*)::int AS count FROM conversations WHERE guild_id = $1 and the
SELECT COUNT(*)::int AS count FROM mod_cases WHERE guild_id = $1 queries use
req.params.id in their respective dbPool.query calls so counts remain scoped to
the guild.
- Around line 205-234: The cursor-based pagination is correctly implemented in
the router.get('/:id/members') handler by using req.guild.members.list({ limit,
after }) and mapping the returned members to memberList; no code changes are
required—leave the current logic (limit clamping, after handling, mapping to
id/username/displayName/roles/joinedAt, and nextAfter calculation) as-is.

In `@src/db.js`:
- Around line 112-130: The ALTER TABLE and CREATE INDEX operations are now each
wrapped in their own try/catch (the pool.query call that adds guild_id and the
pool.query that creates idx_conversations_guild_id), so no further code changes
are required; keep the separate try/catch blocks and the warn(...) calls as-is
and mark the change approved (the previous duplicate comment can be
ignored/removed).

- Remove redundant sanitizeMentions call (safeSend handles it internally)
- Document ChannelType enum values in guild info response
- Break long SQL query into multi-line template literal
- Add comment noting safeSend split behavior for API consumers
- Null server reference in shutdown timeout path to prevent stale ref
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f41bf18 and 5255c94.

📒 Files selected for processing (2)
  • src/api/routes/guilds.js
  • src/api/server.js
🧰 Additional context used
📓 Path-based instructions (2)
**/*.js

📄 CodeRabbit inference engine (AGENTS.md)

**/*.js: Use ESM modules with import/export syntax, never use require()
Always use node: protocol prefix for Node.js builtin imports (e.g., import { readFileSync } from 'node:fs')
Always use semicolons at the end of statements
Use single quotes for string literals, enforced by Biome
Use 2-space indentation throughout the codebase, enforced by Biome

Files:

  • src/api/server.js
  • src/api/routes/guilds.js
src/**/*.js

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.js: Never use console.log(), console.warn(), console.error(), or any other console.* method in src/ files. Always use Winston logger instead: import { info, warn, error } from '../logger.js' and log with structured metadata
Use custom error classes from src/utils/errors.js for error handling. Always log errors with context before re-throwing
Use splitMessage() utility to handle Discord's 2000-character message limit for outgoing messages
Use safeSend() wrappers from src/utils/safeSend.js for all outgoing messages, and use sanitizeMentions() from src/utils/sanitizeMentions.js to strip @everyone/@here via zero-width space insertion
Any new code must include tests. Run pnpm test before every commit. Maintain minimum 80% code coverage on statements, branches, functions, and lines using @vitest/coverage-v8. PRs that drop coverage below 80% will fail CI
Write JSDoc comments for documentation instead of TypeScript, as the project uses plain JavaScript without TypeScript

Files:

  • src/api/server.js
  • src/api/routes/guilds.js
🧠 Learnings (10)
📚 Learning: 2026-02-17T02:29:07.358Z
Learnt from: CR
Repo: BillChirico/bills-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-17T02:29:07.358Z
Learning: Keep documentation files up to date after every code change: update README.md for setup/config changes, update AGENTS.md for architecture/key files changes, update CONTRIBUTING.md for workflow changes, update .env.example for environment variables, update config.json for new config sections

Applied to files:

  • src/api/server.js
📚 Learning: 2026-02-11T17:18:14.598Z
Learnt from: CR
Repo: BillChirico/bills-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-11T17:18:14.598Z
Learning: See AGENTS.md for full project context, architecture, and coding guidelines

Applied to files:

  • src/api/server.js
📚 Learning: 2026-02-17T02:29:07.358Z
Learnt from: CR
Repo: BillChirico/bills-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-17T02:29:07.358Z
Learning: Applies to src/**/*.js : Use custom error classes from `src/utils/errors.js` for error handling. Always log errors with context before re-throwing

Applied to files:

  • src/api/server.js
📚 Learning: 2026-02-17T02:29:07.358Z
Learnt from: CR
Repo: BillChirico/bills-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-17T02:29:07.358Z
Learning: Applies to src/**/*.js : Use `safeSend()` wrappers from `src/utils/safeSend.js` for all outgoing messages, and use `sanitizeMentions()` from `src/utils/sanitizeMentions.js` to strip everyone/here via zero-width space insertion

Applied to files:

  • src/api/routes/guilds.js
📚 Learning: 2026-02-17T02:29:07.358Z
Learnt from: CR
Repo: BillChirico/bills-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-17T02:29:07.358Z
Learning: Applies to src/**/*.js : Use `splitMessage()` utility to handle Discord's 2000-character message limit for outgoing messages

Applied to files:

  • src/api/routes/guilds.js
📚 Learning: 2026-02-17T02:29:07.358Z
Learnt from: CR
Repo: BillChirico/bills-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-17T02:29:07.358Z
Learning: Applies to src/modules/moderation.js : Case numbering must be per-guild sequential and assigned atomically using `COALESCE(MAX(case_number), 0) + 1` in a single INSERT statement inside `createCase()`

Applied to files:

  • src/api/routes/guilds.js
📚 Learning: 2026-02-17T02:29:07.358Z
Learnt from: CR
Repo: BillChirico/bills-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-17T02:29:07.358Z
Learning: Applies to src/index.js : Enforce Discord intents: the bot requires MessageContent, GuildMembers, and GuildVoiceStates intents to be enabled in `src/index.js` client setup

Applied to files:

  • src/api/routes/guilds.js
📚 Learning: 2026-02-17T02:29:07.358Z
Learnt from: CR
Repo: BillChirico/bills-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-17T02:29:07.358Z
Learning: Applies to src/index.js : The PostgreSQL logging transport is a long-lived Winston transport that requires reactive `onConfigChange` wiring in `src/index.js` startup to add/remove/recreate the transport when `logging.database.*` settings change at runtime

Applied to files:

  • src/api/routes/guilds.js
📚 Learning: 2026-02-17T02:29:07.358Z
Learnt from: CR
Repo: BillChirico/bills-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-17T02:29:07.358Z
Learning: Applies to src/commands/**/*.js : Discord timeouts have a maximum duration of 28 days; Discord slowmode has a maximum duration of 6 hours (21600 seconds). Enforce these caps in command logic

Applied to files:

  • src/api/routes/guilds.js
📚 Learning: 2026-02-17T02:29:07.358Z
Learnt from: CR
Repo: BillChirico/bills-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-17T02:29:07.358Z
Learning: Applies to src/commands/**/*.js : Moderation commands must follow the pattern: deferReply({ ephemeral: true }), validate inputs, sendDmNotification(), execute Discord action, createCase(), sendModLogEmbed(), checkEscalation(). Use parseDuration() from `src/utils/duration.js` for duration-based commands (timeout, tempban, slowmode)

Applied to files:

  • src/api/routes/guilds.js
🔇 Additional comments (7)
src/api/routes/guilds.js (3)

73-92: LGTM!

Guild info endpoint is clean: caps channel list at 500, documents ChannelType enum values, and returns a focused set of metadata.


206-235: LGTM!

Cursor-based pagination using guild.members.list({ limit, after }) is the correct approach for Discord's API. The nextAfter cursor and limit clamping are well implemented.


319-321: The original review comment is accurate. safeSend() does internally sanitize mentions via the prepareOptions() → sanitizeMessageOptions() chain, which in turn calls sanitizeMentions() on all text fields (content, embeds, components). The comment correctly documents this behavior. No changes needed.

src/api/server.js (4)

11-15: LGTM!

Module-scoped server and rateLimiter references with proper JSDoc type annotations. The rateLimiter tracking addresses the prior resource leak concern.


24-78: LGTM!

createApp is well-structured: conditional CORS, rate limiter lifecycle management, clean error handler with appropriate 4xx/5xx differentiation. All prior review feedback has been addressed.


87-117: LGTM!

Startup is robust: guards against double-start, validates port (including range), uses server.once('error') to avoid stale listener issues. All prior feedback incorporated.


124-160: LGTM!

Graceful shutdown with timeout fallback, rate limiter cleanup, and proper server = null in all paths. The close/timeout race is handled correctly since the promise settles idempotently.

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/api/routes/guilds.js`:
- Around line 148-151: The code redundantly calls path.split('.') twice; reuse
the previously computed segments instead of splitting again. Update the code
that currently does const segments = path.split('.') to reuse the existing
segments array (the one used to extract topLevelKey) — e.g., reference the same
variable (segments) when checking for empty segments — so there is only a single
split operation on path and the check for empty segments uses that existing
segments array.

---

Duplicate comments:
In `@src/api/routes/guilds.js`:
- Around line 173-186: The conversation and mod_cases COUNT queries in the
Promise.all are now correctly scoped by guild via dbPool.query('SELECT
COUNT(*)::int AS count FROM conversations WHERE guild_id = $1', ...) and the
corresponding mod_cases query; no code change is required—keep the current
dbPool.query calls (and the explanatory JSDoc about legacy NULL guild_id rows)
as-is to preserve correct scoping and developer context around
conversationResult and caseResult.
- Around line 294-297: The handler branch for action === 'sendMessage' currently
only checks presence of channelId and content but not the type; update the
validation in that branch (the sendMessage check) to ensure channelId is a
string (e.g., typeof channelId === 'string' and non-empty) and return a 400 with
a clear error if it is not, before calling cache.get() or other Discord lookups;
this prevents numeric snowflakes from causing misleading 404s and keeps the
check near the existing channelId/content validation.

Comment on lines +148 to +151
const segments = path.split('.');
if (segments.some((s) => s === '')) {
return res.status(400).json({ error: 'Config path contains empty segments' });
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Double path.split('.') call is redundant.

path.split('.') is called on line 137 (to extract topLevelKey) and again on line 148. Consider reusing the result.

♻️ Proposed fix
-  const topLevelKey = path.split('.')[0];
+  const segments = path.split('.');
+  const topLevelKey = segments[0];
   if (!SAFE_CONFIG_KEYS.includes(topLevelKey)) {
     return res.status(403).json({ error: 'Modifying this config key is not allowed' });
   }
 
-  if (!path.includes('.')) {
+  if (segments.length < 2) {
     return res
       .status(400)
       .json({ error: 'Config path must include at least one dot separator (e.g., "ai.model")' });
   }
 
-  const segments = path.split('.');
   if (segments.some((s) => s === '')) {
     return res.status(400).json({ error: 'Config path contains empty segments' });
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/api/routes/guilds.js` around lines 148 - 151, The code redundantly calls
path.split('.') twice; reuse the previously computed segments instead of
splitting again. Update the code that currently does const segments =
path.split('.') to reuse the existing segments array (the one used to extract
topLevelKey) — e.g., reference the same variable (segments) when checking for
empty segments — so there is only a single split operation on path and the check
for empty segments uses that existing segments array.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Web dashboard: REST API layer for bot data and actions

1 participant