Conversation
… file - Import REST and Routes from discord.js - Import commandData from commands.js - Add deployCommands() function to register slash commands with Discord API - Support both guild-specific (dev) and global command registration - Call deployCommands() in ready event handler - Log success message when commands are registered Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
- Added startTime variable to track bot startup - Implemented /status command with uptime, server count, memory usage, API health check, and latency - Uses EmbedBuilder following existing patterns - Displays operational status with color-coded health indicators
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📒 Files selected for processing (6)
✨ Finishing touches🧪 Generate unit tests (beta)
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. Comment |
| "created_at": "2026-02-03T19:51:09.135836", | ||
| "project_hash": "51a4f617fc8ece9b63e20f8a9950e73b", | ||
| "inherited_from": "/Users/billchirico/Developer/bill-bot" | ||
| } No newline at end of file |
There was a problem hiding this comment.
Local development files accidentally committed to repository
Medium Severity
Several local development/tooling files were accidentally committed. These include .auto-claude-security.json, .auto-claude-status, and .claude_settings.json, which contain machine-specific paths (e.g., /Users/billchirico/Developer/bill-bot), session timestamps, and developer-specific permissions. These files are transient local state for development tools and don't belong in the repository.
Additional Locations (2)
| } | ||
| } else { | ||
| await interaction.editReply(response); | ||
| } |
There was a problem hiding this comment.
Slash command /ask ignores AI disabled config setting
Medium Severity
The /ask slash command handler doesn't check config.ai?.enabled before calling generateResponse. The message-based AI handler respects this configuration by checking if (config.ai?.enabled) before processing mentions/replies, but the slash command bypasses this check entirely. This means /ask will attempt API calls even when AI functionality is deliberately disabled.
| } | ||
| } else { | ||
| await interaction.editReply(response); | ||
| } |
There was a problem hiding this comment.
Slash command /ask ignores channel restriction configuration
Medium Severity
The /ask slash command doesn't check config.ai?.channels before processing. The message-based AI handler validates that the channel is in the allowed list via allowedChannels.includes(message.channel.id), but the slash command handler has no such check. This means /ask will work in any channel even when administrators have explicitly configured AI to be restricted to specific channels.
| .setFooter({ text: `${client.user.tag}` }) | ||
| .setTimestamp(); | ||
|
|
||
| await interaction.reply({ embeds: [embed] }); |
There was a problem hiding this comment.
Status command may timeout during API health check
High Severity
The /status command makes an API request to check health (lines 407-419) before responding, but doesn't call deferReply() first. Discord requires interaction responses within 3 seconds, otherwise the interaction fails and users see "This interaction failed." AI API calls commonly take several seconds due to network latency and server processing, so this command will frequently timeout and fail to display any status information.
|
Bugbot Autofix prepared fixes for 4 of the 4 bugs found in the latest run.
Or push these changes by commenting: |
|
Closing - slash commands already in main |
Replace permanent mem0Available = false on API errors with a cooldown- based recovery mechanism. After RECOVERY_COOLDOWN_MS (60s), the next request is allowed through to check if the service recovered. If it fails again, the cooldown resets. This prevents a single transient network error from permanently disabling the memory system for all users until restart. Resolves review threads #4, #8, #12.
Replace permanent mem0Available = false on API errors with a cooldown- based recovery mechanism. After RECOVERY_COOLDOWN_MS (60s), the next request is allowed through to check if the service recovered. If it fails again, the cooldown resets. This prevents a single transient network error from permanently disabling the memory system for all users until restart. Resolves review threads #4, #8, #12.
Replace permanent mem0Available = false on API errors with a cooldown- based recovery mechanism. After RECOVERY_COOLDOWN_MS (60s), the next request is allowed through to check if the service recovered. If it fails again, the cooldown resets. This prevents a single transient network error from permanently disabling the memory system for all users until restart. Resolves review threads #4, #8, #12.
…headers - Remove accessToken from client session object; use getToken() server-side in API routes instead (issue #1) - Add runtime check rejecting default/short NEXTAUTH_SECRET (issue #8) - Warn when BOT_API_URL is set but BOT_API_SECRET is missing (issue #9) - Add X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Strict-Transport-Security via headers() in next.config.ts (issue #10) - Propagate RefreshTokenError to session.error for downstream handling
- Differentiate requireGuildAdmin (ADMINISTRATOR only) from requireGuildModerator (ADMINISTRATOR | MANAGE_GUILD), aligning REST admin check with slash-command isAdmin (#1, #2, #12) - Add botOwners startup warning when using default upstream ID (#3) - Add SESSION_SECRET, DISCORD_CLIENT_SECRET, DISCORD_REDIRECT_URI to README deployment table (#4) - Pass actual permission level to getPermissionError so modlog denial says 'moderator' not 'administrator' (#5) - Guard _seedOAuthState with NODE_ENV production check (#6) - Add test: valid JWT with no server-side session (#7) - Add DiscordApiError class with HTTP status (#8) - Add moderatorRoleId support to isModerator (#9) - Remove no-op delete override from SessionStore (#10) - Cap oauthStates at 10k entries (#11) - Fix hasOAuthGuildPermission docstring for bitwise OR semantics (#12) - Handle dashboard URL fragment collision (#13) - Cap guildCache at 10k entries (#14) - Add SESSION_TTL_MS co-location comment with JWT expiry (#15) - Cache SESSION_SECRET via lazy getter in verifyJwt (#16) - Remove PII (username) from OAuth auth log (#17)
- Differentiate requireGuildAdmin (ADMINISTRATOR only) from requireGuildModerator (ADMINISTRATOR | MANAGE_GUILD), aligning REST admin check with slash-command isAdmin (#1, #2, #12) - Add botOwners startup warning when using default upstream ID (#3) - Add SESSION_SECRET, DISCORD_CLIENT_SECRET, DISCORD_REDIRECT_URI to README deployment table (#4) - Pass actual permission level to getPermissionError so modlog denial says 'moderator' not 'administrator' (#5) - Guard _seedOAuthState with NODE_ENV production check (#6) - Add test: valid JWT with no server-side session (#7) - Add DiscordApiError class with HTTP status (#8) - Add moderatorRoleId support to isModerator (#9) - Remove no-op delete override from SessionStore (#10) - Cap oauthStates at 10k entries (#11) - Fix hasOAuthGuildPermission docstring for bitwise OR semantics (#12) - Handle dashboard URL fragment collision (#13) - Cap guildCache at 10k entries (#14) - Add SESSION_TTL_MS co-location comment with JWT expiry (#15) - Cache SESSION_SECRET via lazy getter in verifyJwt (#16) - Remove PII (username) from OAuth auth log (#17)
Add async validateDnsResolution() that resolves a webhook hostname via DNS and checks all resolved IPs against the existing blocked ranges before fetch. This closes the TOCTOU gap where a hostname could pass string-based validation then resolve to a private IP at request time (DNS rebinding attack). Changes: - Add validateDnsResolution() with resolve4/resolve6 checks - Integrate DNS check in fireAndForgetWebhook before fetch - Normalize IPv6 hostnames by stripping brackets (Node.js v22 retains them in URL.hostname, contrary to WHATWG spec) - Add comprehensive test coverage for DNS rebinding scenarios - Update webhook tests for async DNS validation flow Addresses CodeRabbit review thread #8.
… (#83) * feat(dashboard): add Discord entity pickers Co-Authored-By: Claude Opus 4.6 <[email protected]> * feat(dashboard): add diff view and polish - ConfigDiff.tsx: visual diff component showing before/after config changes with green additions and red deletions using the `diff` library - SystemPromptEditor.tsx: textarea with real-time character count, max length warning indicator, and accessible labeling - Toast notifications via sonner: success/error toasts on save, load failures, and reset actions positioned bottom-right - ResetDefaultsButton with confirmation dialog using Radix UI Dialog - ConfigEditor.tsx: full config editing page with AI, welcome message, and moderation sections; PATCH-based save with diff preview - Config API proxy route (GET/PATCH) following established analytics proxy pattern with guild admin authorization - Dialog UI component (shadcn/ui new-york style) - Added lint script to web/package.json Co-Authored-By: Claude Opus 4.6 <[email protected]> * polish(config-editor): improve UX, accessibility, and edge case handling - Replace native checkboxes with styled toggle switches using proper role="switch" and aria-checked attributes - Add unsaved changes guard (beforeunload warning + yellow banner) - Add Ctrl/Cmd+S keyboard shortcut for saving - Block save when system prompt exceeds character limit - Rename misleading "Reset to Defaults" to "Discard Changes" with accurate dialog copy (reverts to last saved state, not factory defaults) - Add diff summary counts (+N / -N) to the pending changes card - Improve accessibility throughout: aria-labels on loading spinner, aria-describedby linking textareas to their hints, aria-invalid on over-limit prompt, aria-live on character counter, aria-hidden on decorative icons, role="region" on diff view - Memoize hasChanges and hasValidationErrors to avoid redundant JSON.stringify on every render - Validate PATCH body shape in API route before proxying upstream - Fix stale "bills-bot" prefix in guild-selection localStorage keys (missed during volvox rename) Closes #31 Co-Authored-By: Claude Opus 4.6 <[email protected]> * feat(api): add config endpoints for Issue #31 Add REST API endpoints for managing bot configuration: - GET /api/v1/config - Retrieve current config (ai, welcome, spam, moderation) - PUT /api/v1/config - Update config with schema validation Features: - Type-safe schema validation for config sections - Flatten nested objects to dot-notation paths for persistence - requireGlobalAdmin middleware (API-secret or bot-owner OAuth) - Proper HTTP error codes (400 for validation, 401/403 for auth, 500 for errors) - Added PUT to CORS methods Tests: - 35 comprehensive tests covering auth, validation, types, edge cases - Tests for validateConfigSchema and flattenToLeafPaths exports Closes #31 * feat(api): add webhook notifications for config changes - Add notifyDashboardWebhook() fire-and-forget sender to PATCH /:id/config - POST /webhooks/config-update endpoint for dashboard to push config changes - Webhook uses DASHBOARD_WEBHOOK_URL env var with 5s timeout - Add comprehensive tests for webhook functionality * feat(dashboard): add config editor with Zustand state management Add a full config editor UI at /dashboard/config with: - Proxy API routes (GET + PATCH) for bot config at /api/guilds/:guildId/config - Zustand store for config state with fetch, update, and debounced saves - Accordion-based sections for ai, welcome, spam, moderation (read-only), triage - Recursive field renderer supporting booleans, numbers, strings, arrays, objects - shadcn/ui components: accordion, input, label, switch, textarea Co-Authored-By: Claude Opus 4.6 <[email protected]> * feat(dashboard): enhance Discord entity pickers with multi-select and Zustand - Add multi-select mode to ChannelSelector and RoleSelector (multiple?: boolean prop) - Create Zustand store for caching channels/roles per guild - Add dedicated bot API endpoints: GET /:id/channels and GET /:id/roles - Add Next.js proxy routes for channels and roles - Update AGENTS.md with new key files Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: remove unused import in webhooks.js * fix: resolve all linting errors across codebase - Migrate biome config from 2.3.14 to 2.4.0 - Fix unused imports (triage.js, modAction.test.js) - Fix import ordering (auth.js, lock.js, unlock.js, ai.js, triage-respond.js, modAction.js, modAction.test.js) - Fix formatting across 19 files - Replace O(n²) spread in reduce with push (cli-process.test.js) - Use Object.hasOwn() instead of Object.prototype.hasOwnProperty (config-guild.test.js) All 1310 tests pass. * feat: add full config editing support for moderation and triage - Add moderation and triage to SAFE_CONFIG_KEYS in guilds.js, webhooks.js, and config.js making them writable via PATCH/PUT endpoints - Expand READABLE_CONFIG_KEYS to include all sections: ai, welcome, spam, moderation, triage, logging, memory, permissions - Add CONFIG_SCHEMA definitions for moderation and triage sections with full type validation - Update WritableConfigSection type to include moderation and triage - Remove moderation from READ_ONLY_SECTIONS in config-section.tsx - Update config-store.ts writable keys check - Add editable moderation section in dashboard config-editor with toggles for enabled, autoDelete, DM notifications, and escalation - Add editable triage section with fields for models, budgets, intervals, streaming, debug footer, and moderation log channel - Update all test assertions to reflect new writable sections Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix(security): add webhook URL validation, schema validation on all write paths, atomic writes - Add shared validateWebhookUrl() utility that blocks SSRF (localhost, private IPs, link-local, IPv6 loopback) and enforces https in production - Wire URL validation into config.js notifyWebhook and guilds.js notifyDashboardWebhook - Export validateSingleValue() from config.js and apply it to the PATCH endpoint in guilds.js and the POST /config-update webhook endpoint - Add path length (<=200 chars) and depth (<=10 segments) limits to guilds.js PATCH and webhooks.js POST endpoints - Refactor PUT handler in config.js to track per-field write results: returns 200 on full success, 207 on partial failure, 500 when all fail - Add comprehensive tests for all new validations and error responses Co-Authored-By: Claude Opus 4.6 <[email protected]> * refactor: extract shared config allowlist, webhook utility, and proxy helpers; remove dead code Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix(frontend): batch saves, fix race conditions, DRY constants, localStorage safety - Batch saveChanges into parallel PATCH requests grouped by section instead of sequential individual PATCHes (I5) - Add request sequence counter to Zustand config store to prevent stale PATCH responses from clobbering newer state (I6) - Centralize SYSTEM_PROMPT_MAX_LENGTH constant in types/config.ts and import in config-editor and system-prompt-editor (M2) - Wrap localStorage.getItem in try/catch for SSR safety (M3) - Fix channels.length / roles.length truthiness bug — use !== undefined instead of .length which is falsy for 0 (M5) - Replace JSON.stringify deep comparison with recursive deepEqual utility function (M8) Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix(security): mask sensitive config fields, block IPv4-mapped IPv6 SSRF, reject unknown config paths - Add SENSITIVE_FIELDS set and maskSensitiveFields utility to strip triage API keys (classifyApiKey, respondApiKey) from all GET config responses - Block SSRF via IPv4-mapped IPv6 addresses (::ffff:127.0.0.1, hex form ::ffff:7f00:1, and cloud metadata ::ffff:169.254.169.254) - Reject unknown config paths in validateSingleValue instead of silently accepting them without type checking - Add cache size limit (100 entries) to webhook URL validation cache - Guard flattenToLeafPaths against __proto__/constructor/prototype keys Co-Authored-By: Claude Opus 4.6 <[email protected]> * refactor(backend): extract shared validators and getBotOwnerIds, add webhook utility tests Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * fix(frontend): remove dead code, fix save flow, harden inputs and type guards - C4: delete 10 unused files (stores, UI components, dashboard selectors) and remove zustand, @radix-ui/react-accordion, @radix-ui/react-label, @radix-ui/react-switch from package.json - m8: replace ~85-line local GuildConfig interface with DeepPartial<BotConfig> (add DeepPartial utility to types/config.ts) - m4: harden isGuildConfig type guard to verify at least one expected section key (ai, welcome, spam) instead of just typeof === "object" - M6: fix computePatches to include top-level paths (remove incorrect fullPath.includes(".") guard that silently dropped top-level field changes) - M7: fix partial save to merge only succeeded sections into savedConfig on partial failure, preserving draft edits for failed sections; only call fetchConfig() on full success - m5: add min constraints to triage number inputs (budgets min=0, timeouts min=1, buffer/context sizes min=1) - m9: add e.returnValue = "" to beforeunload handler for modern browser support Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * test: fix 10-segment test to use valid schema path after strict validation * fix: wire default-personality.md as system prompt fallback Replace generic 'You are a helpful Discord bot.' fallback with the existing default-personality.md template, which provides the intended Volvox Bot personality, role constraints, and anti-abuse guardrails. * fix: merge default-personality into responder system prompt, remove dead template Merge personality, role, constraints, and anti-abuse from the unused default-personality.md into triage-respond-system.md (the responder's actual system prompt). Revert the fallback wiring in triage-prompt.js since personality now lives in the system prompt file where it belongs. Delete default-personality.md — no longer needed. * 📝 Add docstrings to `feat/config-editor-combined` Docstrings generation was requested by @BillChirico. The following files were modified: * `src/api/routes/auth.js` * `src/api/routes/config.js` * `src/api/routes/guilds.js` * `src/api/utils/configAllowlist.js` * `src/api/utils/validateConfigPatch.js` * `src/api/utils/validateWebhookUrl.js` * `src/commands/lock.js` * `src/commands/slowmode.js` * `src/commands/unlock.js` * `src/modules/memory.js` * `src/modules/triage-prompt.js` * `src/modules/triage-respond.js` * `src/modules/triage.js` * `src/utils/debugFooter.js` * `src/utils/permissions.js` * `web/src/app/api/guilds/[guildId]/channels/route.ts` * `web/src/app/api/guilds/[guildId]/config/route.ts` * `web/src/app/api/guilds/[guildId]/roles/route.ts` * `web/src/app/dashboard/config/page.tsx` * `web/src/components/dashboard/config-diff.tsx` * `web/src/components/dashboard/config-editor.tsx` * `web/src/components/dashboard/reset-defaults-button.tsx` * `web/src/components/dashboard/system-prompt-editor.tsx` * `web/src/components/providers.tsx` * `web/src/lib/bot-api-proxy.ts` These files were kept as they were: * `src/api/server.js` * `src/api/utils/webhook.js` These files were ignored: * `tests/api/routes/config.test.js` * `tests/api/routes/guilds.test.js` * `tests/api/routes/webhooks.test.js` * `tests/api/utils/validateWebhookUrl.test.js` * `tests/api/utils/webhook.test.js` * `tests/commands/tempban.test.js` * `tests/config-listeners.test.js` * `tests/modules/cli-process.test.js` * `tests/modules/config-guild.test.js` * `tests/utils/modAction.test.js` These file types are not supported: * `.env.example` * `AGENTS.md` * `biome.json` * `src/prompts/triage-respond-system.md` * `web/package.json` * fix(prompts): replace ambiguous 'classified' with 'triaged' (Thread #11) * fix(prompts): define concrete 'flagging' mechanism for moderation (Thread #12) * fix(prompts): add PII/credentials constraint to prevent echoing secrets (Thread #13) * fix: remove bracketed-IPv6 dead code in webhook URL validator URL.hostname already strips brackets from IPv6 addresses (new URL('http://[::1]').hostname === '::1'), so the hostname.startsWith('[') branch was unreachable dead code. Remove the bracketed-IPv6 branch and the '[::1]' entry from BLOCKED_IPV6 since it could never match. Addresses CodeRabbit review thread #9. * fix: sanitize webhook URL in warning logs to prevent credential exposure Strip query string and fragment from webhook URLs before including them in warning log messages. If a webhook URL contains tokens or API keys as query parameters, they would previously appear in logs. Now logs only origin + pathname (e.g. 'https://example.com/hook') instead of the full URL with sensitive query params. Addresses CodeRabbit review thread #10. * test(config): rename misleading 'Infinity' test name (Thread #14) * fix: normalize validateSingleValue to always return string[] Unknown config paths previously returned [{path, message}] objects while validateValue returned string[]. Callers now receive a consistent string[] in both cases, eliminating the need to handle two different shapes. * test(config): move PUT partial write handling inside config routes suite (Thread #15) * refactor: extract getGuildChannels helper to remove duplication GET /:id and GET /:id/channels shared identical channel-fetching loops with a duplicated MAX_CHANNELS constant. Extracted into a single getGuildChannels(guild) helper used by both handlers. * feat: include section (topLevelKey) in PATCH config webhook payload Downstream consumers of the DASHBOARD_WEBHOOK_URL can now use the 'section' field to selectively reload only the affected config section rather than refreshing the entire config. * fix: return promise in webhook config-update handler (async/await) The previous .then()/.catch() chain was not returned, so Express 5 could not auto-forward rejected promises to the error handler. Converted to async/await so errors propagate correctly. * feat: make JSON body size limit configurable via API_BODY_LIMIT env var Defaults to '100kb' when the env var is not set, preserving existing behaviour. Operators can now tune the limit without code changes. * test(validateWebhookUrl): strengthen cache eviction test to verify re-evaluation (Thread #16) * test(webhook): move vi.useRealTimers() to afterEach to prevent timer leak (Thread #17) * refactor: move CONFIG_SCHEMA/validateValue/validateSingleValue to utils/configValidation.js validateConfigPatch.js (utils) was importing validateSingleValue from routes/config.js — an inverted dependency. Created src/api/utils/configValidation.js as the canonical home for CONFIG_SCHEMA, validateValue, and validateSingleValue. - config.js now imports from ../utils/configValidation.js and re-exports validateSingleValue for backward compatibility with existing callers. - validateConfigPatch.js now imports from ./configValidation.js directly. * perf: split path string once in validateConfigPatchBody path.split('.') was called twice — once to extract topLevelKey and again for segments. Moved the single split to the top and derived topLevelKey from segments[0], avoiding the redundant allocation. * fix(#18): change lint script from tsc to biome check * fix(#19,#20): simplify params type; add PATCH body value check * fix(#21): add metadata export to config page * fix(#22): compute addedCount/removedCount inside useMemo loop * fix(#23,#24,#25,#26): tighten isGuildConfig; extract inputClasses; guard number inputs; rename DiscardChangesButton * fix(#27): change aria-live from polite to off on char counter * fix(#28): change Toaster theme from dark to system * fix(#29,#30): export BotApiConfig; return 504 on AbortError/TimeoutError * fix(#31): add one-time localStorage key migration from old key * fix(#32,#33,#34): SpamConfig JSDoc; collapse WritableConfigSection; fix SYSTEM_PROMPT_MAX_LENGTH JSDoc * fix: remove unused validateSingleValue import, fix biome formatting in config.js After the refactor, validateSingleValue is re-exported directly via 'export { } from' and no longer needs a local import binding. Also removed an extra blank line that biome flagged as a format error. * fix: add DNS resolution validation to prevent SSRF via DNS rebinding Add async validateDnsResolution() that resolves a webhook hostname via DNS and checks all resolved IPs against the existing blocked ranges before fetch. This closes the TOCTOU gap where a hostname could pass string-based validation then resolve to a private IP at request time (DNS rebinding attack). Changes: - Add validateDnsResolution() with resolve4/resolve6 checks - Integrate DNS check in fireAndForgetWebhook before fetch - Normalize IPv6 hostnames by stripping brackets (Node.js v22 retains them in URL.hostname, contrary to WHATWG spec) - Add comprehensive test coverage for DNS rebinding scenarios - Update webhook tests for async DNS validation flow Addresses CodeRabbit review thread #8. * fix: update test assertions for string[] return type from validateSingleValue * fix: mock validateDnsResolution in webhook integration tests After adding DNS resolution pinning in fireAndForgetWebhook, the config and guilds route tests need to mock validateDnsResolution to return true so fetch is actually called. * fix: address minor code review feedback - JSDoc, tests, caps * fix(frontend): address code review feedback - HTML, types, perf * fix(backend): address code review feedback - validation, logging, exports * fix: correct IPv6 validation for public addresses and literals * fix: restore classifier invocation in triage module * fix: address test failures in validateConfigPatch and triage-respond - Check for empty path segments before topLevelKey validation - Fix test to use valid nullable path (welcome.channelId) - Add mock cleanup between triage-respond tests * fix(validation): handle alertChannelId nullable and DNS edge cases * fix(security): prevent mask sentinel write-back and auth secret override 1. configAllowlist: Add isMasked() and stripMaskedWrites() to detect and filter out writes where sensitive fields contain the mask sentinel ('••••••••'). Prevents clients from accidentally overwriting real secrets with the placeholder returned by maskSensitiveFields(). 2. bot-api-proxy: Reorder header spread so x-api-secret is always set AFTER spreading options.headers, preventing caller-provided headers from overriding the server-side auth secret. Both fixes include comprehensive tests. * test: add missing test cases for mask sentinel, prototype pollution, DNS edge cases * refactor: simplify webhook validation for internal-only use * refactor: remove unused SSRF validation code Deleted validateWebhookUrl.js and its tests since webhooks are internal-only. Simplified webhook.js to just check URL format. * fix: prevent WebSearch notification failures from aborting response * fix: correct safeSend mock setup in triage-respond tests * fix(security): use own-property checks in config validation * fix: export MASK constant and clean up orphaned JSDoc * fix: report written sections in webhook, add roles endpoint test * fix: address remaining PR review feedback - Add nullable: true to triage.moderationLogChannel and debugFooterLevel - Add evalClient param to runResponder JSDoc - Convert SAFE_CONFIG_KEYS to Set for O(1) lookups - Reorder validation checks for consistent 400 responses - Update tests for Set-based SAFE_CONFIG_KEYS --------- Co-authored-by: Claude Opus 4.6 <[email protected]> Co-authored-by: AnExiledDev <[email protected]> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
…nsaction safety - CSV injection: escapeCsv now prefixes formula chars (=,+,-,@,\t,\r) with single quote - XP bounds: cap adjustments to ±1,000,000 - XP transaction: wrap upsert + level update in BEGIN/COMMIT/ROLLBACK - Warning filter: add AND action = 'warn' to recent warnings query in member detail - CSV export: paginate guild.members.list() for guilds > 1000 members - Pool safety: wrap getPool() in safeGetPool() with try/catch, return 503 on failure - Search total: return filteredTotal alongside total when search is active Addresses review comments #8-15 on PR #119.
* feat(dashboard): add member table component with sort and pagination * feat(dashboard): add members list page * feat(dashboard): add member detail page with stats and admin actions * feat(members): add member management API with detail, history, XP adjust, and CSV export * feat(members): mount members router, export guild middleware, remove superseded basic members endpoint * test(members): add API endpoint tests (26 tests) * fix: lint import ordering in member dashboard components * fix(members-api): add rate limiting, CSV injection protection, XP transaction safety - CSV injection: escapeCsv now prefixes formula chars (=,+,-,@,\t,\r) with single quote - XP bounds: cap adjustments to ±1,000,000 - XP transaction: wrap upsert + level update in BEGIN/COMMIT/ROLLBACK - Warning filter: add AND action = 'warn' to recent warnings query in member detail - CSV export: paginate guild.members.list() for guilds > 1000 members - Pool safety: wrap getPool() in safeGetPool() with try/catch, return 503 on failure - Search total: return filteredTotal alongside total when search is active Addresses review comments #8-15 on PR #119. * fix(dashboard): align member interfaces with backend API response shape Frontend-backend contract fixes: - MemberRow: id/displayName/avatar/messages_sent/warning_count/joinedAt (was snake_case) - Avatar: use full URL from backend directly, remove hash-based CDN helper - Pagination: cursor → nextAfter, param 'cursor' → 'after' - MemberDetail: flat response shape with stats/reputation/warnings sub-objects - SortColumn: restrict to API-supported values (messages/xp/warnings/joined) - Role color: use hexColor string directly instead of number conversion - XP progress: use next_level_xp from reputation object - CSV export: add error state instead of silent catch - Dependency array: add fetchMembers, remove eslint-disable comment - Keyboard accessibility: tabIndex={0} + onKeyDown for Enter/Space on rows - Guild context: handleRowClick depends on guildId - Search total: display filteredTotal when available Addresses review comments #1-7, #16-19 on PR #119. * feat(dashboard): add Next.js API proxy routes for member endpoints Create proxy routes following the existing bot-api-proxy pattern: - GET /api/guilds/:guildId/members — enriched member list - GET /api/guilds/:guildId/members/:userId — member detail - POST /api/guilds/:guildId/members/:userId/xp — XP adjustment - GET /api/guilds/:guildId/members/:userId/cases — mod case history - GET /api/guilds/:guildId/members/export — CSV export (streamed) All routes include guild admin authorization and proper error handling. CSV export uses 30s timeout and streams the response body. Addresses review comment #5 on PR #119. * test(members): update tests for API changes - XP tests: mock pool.connect() + client for transaction flow - Add XP bounds test (±1,000,000 limit) - Verify BEGIN/COMMIT/release called in transaction - Search test: assert filteredTotal in response * fix: lint and formatting fixes across all changed files - Use template literals instead of string concatenation (biome) - Use const for non-reassigned variables - Add button type='button' for a11y - Remove unused displayTotal variable - Use Number.isNaN over global isNaN - Format proxy routes to match biome standards * fix(members-api): add global + per-route rate limiting to satisfy CodeQL * 📝 Add docstrings to `feat/member-management` Docstrings generation was requested by @BillChirico. The following files were modified: * `src/api/routes/guilds.js` * `src/api/routes/members.js` * `web/src/app/api/guilds/[guildId]/members/[userId]/cases/route.ts` * `web/src/app/api/guilds/[guildId]/members/[userId]/route.ts` * `web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts` * `web/src/app/api/guilds/[guildId]/members/export/route.ts` * `web/src/app/api/guilds/[guildId]/members/route.ts` * `web/src/app/dashboard/members/[userId]/page.tsx` * `web/src/app/dashboard/members/page.tsx` * `web/src/components/dashboard/member-table.tsx` These files were ignored: * `tests/api/routes/guilds.test.js` * `tests/api/routes/members.test.js` * fix: correct CSV formula-injection bug in escapeCsv The escapeCsv function was discarding the original string value when prefixing with a single quote to neutralize formula injection. Now correctly preserves the value: str = `'${str}` instead of str = `'`. Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: include guildId in member row click navigation Include guildId as a query parameter when navigating to member detail page to ensure guild context is preserved across navigation. Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: use safeGetPool in all member endpoints The GET /:id/members endpoint was using raw getPool() instead of safeGetPool() with the 503 guard used by all other member endpoints. Now consistently returns 503 when database is unavailable. Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: log CSV export errors instead of silent failure Add console.error logging when CSV export fails, in addition to setting the error state for user display. Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix(members): dedupe rate limiting, add 401 handling, fix loading state, remove console.error * fix(members): reject fractional XP amounts (column is INTEGER) * test: boost branch coverage to 85% with targeted tests Raise branch coverage from 83.32% to 85.24% across 4 key files: - sentry.js: beforeSend filter, tracesSampleRate parsing, environment resolution - events.js: review/showcase/challenge button handlers, partial fetch, rate limit/link filter branches - rateLimit.js: repeat offender edge cases, permission checks, alert channel, cleanup - members.js: safeGetPool null paths (503), transaction rollback, escapeCsv edge cases New files: tests/modules/events-extra.test.js Modified: tests/modules/rateLimit.test.js, tests/sentry.init.test.js Removed unused import: src/api/index.js * fix(members): reject non-integer XP amounts with 400 The reputation.xp column is INTEGER. Fractional values like 1.5 pass the existing finite/non-zero check but get silently truncated by PostgreSQL (admin adds 1.5, only 1 is stored). Add explicit Number.isInteger check after the existing guards, returning 400 with 'amount must be an integer'. * test(members): add test for fractional XP amount returning 400 Covers the new Number.isInteger guard — sending amount: 1.5 must return 400 with error 'amount must be an integer'. * fix(members): scope membersRateLimit to member routes only The global router.use(membersRateLimit) was applied to every request hitting this router, which is mounted at /api/v1/guilds before the guilds router. This accidentally rate-limited non-member guild endpoints (e.g. /guilds/:id/analytics). Remove the global router.use and add membersRateLimit explicitly on each of the five member route definitions (export, list, detail, cases, xp) so rate limiting is scoped correctly. * fix(members-ui): fix roleColorStyle fallback for hex alpha concatenation The caller appends '40' and '15' to the result for borderColor and backgroundColor. When the fallback was 'hsl(var(--muted-foreground))' this produced 'hsl(var(--muted-foreground))40' — not valid CSS — so roles with Discord's default color (#000000) got no border/background. Replace the HSL CSS-variable fallback with a plain hex grey (#6b7280) so appending hex alpha digits produces valid 8-digit hex colours. * fix: biome formatting in members.js * fix(members): add AbortController and request sequencing to prevent stale responses Add AbortController to cancel in-flight fetch requests when a new request is triggered (e.g., from search/sort changes). Also add a monotonic request ID counter to guard against out-of-order responses overwriting newer state. - Abort previous request on each new fetchMembers call - Pass AbortSignal to fetch() - Silently ignore AbortError exceptions - Discard stale responses/errors via requestId guard - Only clear loading state for the current (non-superseded) request - Abort in-flight request on component unmount * fix(members): use Discord server-side search instead of page-local filtering Replace client-side substring filtering (which only searched within the current page of Discord results) with Discord's guild.members.search() API for server-side search across all guild members. - search param now triggers guild.members.search({ query, limit }) - Cursor pagination (nextAfter) is null during search (Discord search does not support cursor-based paging) - filteredTotal reflects actual server-side search result count - Sort still applies to the returned page (documented limitation — global sort would require DB-driven member indexing) - Updated tests to verify search() is called and filteredTotal semantics * fix(members): stream CSV export in batches to reduce memory pressure Replace the pattern of accumulating all guild members in one in-memory Map before writing CSV. Now each batch of 1000 members is fetched from Discord, enriched from the DB, written to the response, and released for GC — keeping peak memory at O(batch_size) instead of O(total_members). - Move CSV header write before the batch loop - Process and write each batch inline instead of collecting all first - Remove userIds.length > 0 guards (batch loop guarantees non-empty) - Track exportedCount incrementally - Added streaming export test * fix: button types, useCallback deps, array keys, remove duplicate tests and eslint comments --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <[email protected]>
AI Chat already uses MessageSquare. Conversations now uses MessagesSquare to distinguish the two nav items visually.
* feat(conversations): add flagged_messages migration * feat(conversations): add API endpoints for list, detail, search, flag, stats * feat(conversations): mount router in API index * feat(conversations): add conversation list and replay dashboard pages * feat(conversations): add Next.js API proxy routes * test(conversations): add API and grouping tests * fix(conversations): escape ILIKE wildcards to prevent wildcard injection * fix(conversations): remove unused _totalChars variable * fix(conversations): cap list query to 5000 rows to prevent memory exhaustion * fix(conversations): replace in-memory stats grouping with SQL aggregates * fix(conversations): bound conversation detail query by time window instead of full channel scan * style: alphabetize imports and format authorizeGuildAdmin calls * test(conversations): fix stats mock to match SQL conversation counting * test(conversations): add POST flag endpoint to guild validation test The auth test already covered all 5 endpoints including POST .../flag, but the guild-validation test only checked 4 GET endpoints, leaving the flag endpoint's guild validation untested. Resolves review thread PRRT_kwDORICdSM5xTeiw * fix(conversations): parameterize SQL interval and fix flag button a11y Thread 3: Replace string interpolation of CONVERSATION_GAP_MINUTES in the window-function SQL with a $2 parameter to avoid hardcoded literals. Passes CONVERSATION_GAP_MINUTES as a query value instead. Thread 4: Change flag button wrapper from `focus-within:opacity-100` to `group-focus-within:opacity-100` so the button becomes visible whenever any element in the message bubble group receives keyboard focus, not just when the button's own children are focused — matching the group-hover pattern and ensuring proper keyboard accessibility. Also: biome --write reformatted label.tsx and textarea.tsx (pre-existing style issues). * test: add ILIKE wildcard escape coverage for % and _ characters Adds test cases verifying that % and _ characters in conversation search queries are properly escaped before being used in ILIKE patterns, preventing them from acting as SQL wildcards. * fix: deterministic flag status for duplicate flagged_messages rows When a message has been flagged multiple times (flagged_messages has no UNIQUE constraint on message_id), the previous Map construction would silently overwrite entries in iteration order, making the displayed status non-deterministic. Order the SELECT by created_at DESC so the first row per message_id that lands in the Map is always the most recently created flag, giving a predictable 'latest wins' behaviour. * refactor: extract escapeIlike utility from logQuery inline impl Creates src/utils/escapeIlike.js as a shared, exported utility. Conversations route now imports it instead of duplicating the regex. * fix(conversations): use escapeIlike(), fix non-deterministic flag status, add 30-day default window, verify conversationId on flag POST - Import escapeIlike() instead of inline regex (DRY #4) - Default to last 30 days when no `from` filter to prevent unbounded LIMIT 5000 scan (#3) - Fix Map construction: iterate ORDER BY DESC rows and only set first occurrence per key so most-recent flag status wins (#1) - Verify flagged messageId belongs to the conversation's channel before inserting (#2) * test(conversations): add ILIKE backslash escape test and fix flag mocks for new anchor check - Add test for backslash (\) escaping in ILIKE search (#7) - Update 'flag a message' mock to include anchorCheck query result * refactor(web): add LOG_PREFIX constant to all 5 conversation proxy routes (#6) Each route previously inlined its prefix string on every call. Extracts to module-scope const matching the pattern used by config/members/roles routes. * fix(ui): use MessagesSquare icon for Conversations sidebar entry (#8) AI Chat already uses MessageSquare. Conversations now uses MessagesSquare to distinguish the two nav items visually. * fix(ui): show last 4 digits of channel snowflake instead of raw ID (#9) Raw Discord snowflakes are 18+ digit numbers. Showing `${channelId.slice(-4)}` gives a minimal but less jarring display until a proper channel name resolver is wired up. * refactor(ui): extract PAGE_SIZE constant in conversations page (#5) Replaces two hardcoded 25s with a single PAGE_SIZE = 25 constant. * docs(migration): explain missing FK on conversation_first_id (#10) conversation_first_id has no FK because conversations are not a separate table with their own PK. They are virtual groups derived from message rows. message_id already carries a FK for referential integrity. * fix(lint): suppress pre-existing biome a11y errors in label component * fix(conversations): stray $ in JSX channel display, increase query limit to 10k



Implement Discord slash commands for all bot interactions including /ask for AI chat, /help for documentation, /clear for conversation reset, and /status for bot health. This modernizes the bot to use Discord's preferred interaction method.