Skip to content

fix: address code review findings from PRs #12–#18#19

Merged
BillChirico merged 3 commits intoVolvoxLLC:mainfrom
AnExiledDev:fix/code-review-fixes
Feb 12, 2026
Merged

fix: address code review findings from PRs #12–#18#19
BillChirico merged 3 commits intoVolvoxLLC:mainfrom
AnExiledDev:fix/code-review-fixes

Conversation

@AnExiledDev
Copy link
Contributor

Summary

  • Error handling on Discord sends — Wrapped the messageCreate AI-response block in try-catch with a getUserFriendlyMessage() fallback so Discord API failures (rate limits, missing permissions, deleted channels) no longer surface as unhandled rejections
  • Safe-by-default error display — Inverted the NODE_ENV check in config.js so error details only leak when explicitly set to development, not whenever production is unset
  • Duplicate unhandledRejection handler — Removed the handler from index.js; the canonical one in events.js remains
  • Dead shutdown code — Removed registerPendingRequest/removePendingRequest and the unused pending-request wait loop from graceful shutdown
  • ChimeIn AbortController cleanup — Null out buf.abortController in the finally block so it doesn't linger until the next resetCounter()
  • Welcome activity Map eviction — Added throttled pruneStaleActivity() every 50 calls to recordCommunityActivity() so stale guild data doesn't accumulate indefinitely

Test plan

  • All 337 tests pass (npm test)
  • New tests for Discord send failure (reply + split paths)
  • New test for NODE_ENV-unset error hiding
  • New test for welcome activity pruning
  • Removed tests for deleted dead code (pending requests, duplicate handler)

Resolve six issues identified during review of the last five merged PRs:

- Wrap Discord message sends in try-catch with user-friendly fallback
- Invert NODE_ENV check to hide error details by default
- Remove duplicate unhandledRejection handler from index.js
- Remove dead graceful-shutdown request tracking code
- Null out ChimeIn AbortController in finally block
- Add throttled eviction to welcome activity Map
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 12, 2026

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes

    • Improved AI response error handling with user-friendly fallbacks and graceful failure behavior.
    • Cleared lingering per-channel processing state after evaluations.
    • Simplified shutdown sequence to avoid waiting on in-flight AI requests.
  • New Features

    • Automatic eviction/pruning of stale activity tracking data.
    • Environment-aware error messages: detailed in development, generic in production.
  • Tests

    • Added/updated tests for error sanitization, response-send failures, and eviction behavior.

Walkthrough

Sanitized config error messages by environment, removed pending-request helpers and unhandledRejection shutdown handling, cleared AbortController references after evaluation, added robust AI-response send error handling with user-friendly replies, and implemented throttled pruning of stale community activity; tests added or updated accordingly.

Changes

Cohort / File(s) Summary
Config error sanitization
src/commands/config.js, tests/commands/config.test.js
Error messages now expose err.message only when NODE_ENV === 'development'; otherwise use a generic "An internal error occurred." Added tests asserting sanitization when NODE_ENV is unset.
Shutdown / exports removed
src/index.js, tests/index.test.js
Removed pending-request tracking and exported helpers registerPendingRequest / removePendingRequest, and removed unhandledRejection handling and related graceful-shutdown wait/logging. Corresponding tests that asserted those behaviors were removed.
AbortController cleanup
src/modules/chimeIn.js
Ensure buf.abortController is reset to null in accumulate's finally block so no lingering AbortController remains after evaluation.
AI response error handling
src/modules/events.js, tests/modules/events.test.js
Wrapped message-create AI flow in try/catch; split responses are sent via channel.send and single responses via message.reply. On send failures, log the error and attempt a user-friendly reply via getUserFriendlyMessage; tests added/mocked for message.reply() and channel.send() rejection scenarios.
Activity eviction / pruning
src/modules/welcome.js, tests/modules/welcome.test.js
Added EVICTION_INTERVAL, activityCallCount, and pruneStaleActivity(cutoff) to periodically evict stale per-channel activity timestamps; introduced test-only helpers __getCommunityActivityState(guildId) and __resetCommunityActivityState() and a test that verifies eviction behavior.
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main objective of the PR: addressing code review findings from multiple prior PRs.
Description check ✅ Passed The description is well-structured and directly related to the changeset, clearly explaining each of the six main improvements with specific implementation details.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% 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

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
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: 2

🤖 Fix all issues with AI agents
In `@tests/commands/config.test.js`:
- Around line 173-195: The test restores NODE_ENV incorrectly (setting it to the
string "undefined") — update the teardown to restore env safely by checking
originalEnv: if originalEnv === undefined then delete process.env.NODE_ENV else
set process.env.NODE_ENV = originalEnv; additionally move the test logic into a
try/finally around the call to execute (and the mocks) so the environment is
always restored; reference the test block that calls execute, uses getConfig
mock, and uses originalEnv.

In `@tests/modules/welcome.test.js`:
- Around line 139-174: The test currently only ensures no crash when eviction
runs but doesn't assert pruning occurred; update the test around
recordCommunityActivity and the stale channel to verify pruning by using or
adding a public inspector (e.g., getCommunitySnapshot or similar) to read the
stored timestamps for guild 'prune-guild' and channels 'prune-ch' and 'fresh-ch'
after the simulated time jump and repeated calls; specifically, after the
vi.restoreAllMocks() call invoke getCommunitySnapshot() (or add/export such a
helper from the welcome module) and assert that 'prune-ch' has only fresh
timestamps (i.e., no timestamps older than the activityWindowMinutes) or that a
subsequent call to recordCommunityActivity for 'prune-ch' creates a new entry
rather than reusing old timestamps, ensuring deterministic behavior regardless
of module-level activityCallCount.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c5b484b and b936877.

📒 Files selected for processing (9)
  • src/commands/config.js
  • src/index.js
  • src/modules/chimeIn.js
  • src/modules/events.js
  • src/modules/welcome.js
  • tests/commands/config.test.js
  • tests/index.test.js
  • tests/modules/events.test.js
  • tests/modules/welcome.test.js
💤 Files with no reviewable changes (1)
  • tests/index.test.js
🧰 Additional context used
📓 Path-based instructions (4)
src/**/*.js

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.js: Use ESM syntax with import/export; never use require()
Always use node: protocol when importing Node.js builtins (e.g., import { readFileSync } from 'node:fs')
Always use semicolons in code
Use single quotes for strings (enforced by Biome)
Use 2-space indentation (enforced by Biome)
Always use Winston logging via import { info, warn, error } from '../logger.js' — NEVER use console.log(), console.warn(), console.error(), or any console.* method in src/ files
Pass structured metadata when logging: info('Message processed', { userId, channelId })
Use custom error classes from src/utils/errors.js for error handling
Always log errors with context before re-throwing
Use getConfig() from src/modules/config.js to read config; use setConfigValue(key, value) to update at runtime
Use splitMessage() utility for messages exceeding Discord's 2000-character limit
No TypeScript — use plain JavaScript with JSDoc comments for documentation

Files:

  • src/modules/chimeIn.js
  • src/index.js
  • src/modules/welcome.js
  • src/modules/events.js
  • src/commands/config.js
src/modules/*.js

📄 CodeRabbit inference engine (AGENTS.md)

Check config.yourModule.enabled before processing in module handlers

Files:

  • src/modules/chimeIn.js
  • src/modules/welcome.js
  • src/modules/events.js
tests/**/*.{js,test.js}

📄 CodeRabbit inference engine (AGENTS.md)

Use Vitest for testing with pnpm test; test coverage provider is @vitest/coverage-v8

Files:

  • tests/modules/welcome.test.js
  • tests/modules/events.test.js
  • tests/commands/config.test.js
src/commands/*.js

📄 CodeRabbit inference engine (AGENTS.md)

Slash commands must export data (SlashCommandBuilder) and execute(interaction) function

Files:

  • src/commands/config.js
🧠 Learnings (4)
📚 Learning: 2026-02-11T17:18:29.603Z
Learnt from: CR
Repo: BillChirico/bills-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-11T17:18:29.603Z
Learning: Graceful shutdown is handled in `src/index.js`; use error classes and logging before throwing errors in other modules

Applied to files:

  • src/index.js
📚 Learning: 2026-02-11T17:18:29.603Z
Learnt from: CR
Repo: BillChirico/bills-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-11T17:18:29.603Z
Learning: Applies to src/**/*.js : Use `splitMessage()` utility for messages exceeding Discord's 2000-character limit

Applied to files:

  • src/modules/events.js
  • tests/modules/events.test.js
📚 Learning: 2026-02-11T17:18:29.603Z
Learnt from: CR
Repo: BillChirico/bills-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-11T17:18:29.603Z
Learning: Module handlers must be registered in `src/modules/events.js` to wire them to Discord events

Applied to files:

  • tests/modules/events.test.js
📚 Learning: 2026-02-11T17:18:29.603Z
Learnt from: CR
Repo: BillChirico/bills-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-11T17:18:29.603Z
Learning: Applies to src/**/*.js : Use custom error classes from `src/utils/errors.js` for error handling

Applied to files:

  • tests/modules/events.test.js
🧬 Code graph analysis (5)
src/index.js (3)
src/modules/ai.js (1)
  • stopConversationCleanup (335-341)
src/logger.js (2)
  • info (216-218)
  • error (230-232)
src/db.js (1)
  • closeDb (149-160)
src/modules/events.js (3)
src/utils/errors.js (2)
  • message (46-46)
  • getUserFriendlyMessage (114-159)
src/modules/ai.js (4)
  • response (406-417)
  • generateResponse (379-450)
  • config (34-34)
  • config (49-49)
src/utils/splitMessage.js (3)
  • needsSplitting (59-61)
  • chunks (29-29)
  • splitMessage (24-51)
src/commands/config.js (1)
src/modules/config.js (1)
  • err (30-30)
tests/modules/events.test.js (3)
src/modules/welcome.js (1)
  • message (117-123)
src/utils/errors.js (2)
  • message (46-46)
  • getUserFriendlyMessage (114-159)
src/utils/splitMessage.js (2)
  • needsSplitting (59-61)
  • splitMessage (24-51)
tests/commands/config.test.js (2)
src/modules/config.js (1)
  • getConfig (130-132)
src/commands/config.js (3)
  • execute (158-178)
  • content (315-315)
  • content (350-350)
🔇 Additional comments (7)
src/modules/chimeIn.js (1)

283-286: LGTM — proper cleanup of abortController in the finally block.

This ensures the AbortController reference doesn't linger after evaluation completes normally, complementing the abort-and-null in resetCounter.

src/commands/config.js (1)

258-265: LGTM — consistent environment-aware error messaging across all three handlers.

The inverted check correctly restricts error detail exposure to explicit development mode only, preventing accidental leakage of internal details (e.g., database connection strings) in production or unset environments.

src/modules/welcome.js (1)

75-100: LGTM — throttled eviction prevents unbounded Map growth.

The approach of pruning every 50 calls with the current activity window cutoff is clean and avoids per-call overhead. Deleting from a Map during for...of iteration is safe in JavaScript.

src/modules/events.js (1)

97-133: Solid error handling for Discord send failures.

The try/catch correctly wraps all Discord API calls in the AI response path, and the nested fallback with silent catch on line 130 is appropriate — if the initial send failed due to permissions or a deleted channel, the fallback will also fail gracefully.

src/index.js (1)

203-228: Clean simplification of the shutdown sequence.

The removal of the pending-request wait loop streamlines shutdown without losing essential steps (stop timers → save state → close DB → disconnect → exit). The ordering is correct.

tests/modules/events.test.js (2)

247-281: Good coverage of the new error paths.

Both tests correctly exercise the failure scenarios — reply rejection triggering the getUserFriendlyMessage fallback, and split-send rejection where both send and the fallback reply fail. The assertions match the implementation behavior.


34-37: LGTM — mock for getUserFriendlyMessage properly set up.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

AnExiledDev and others added 2 commits February 12, 2026 03:46
process.env coerces undefined to the string "undefined" — use delete
instead, wrapped in try/finally for robustness.
coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 12, 2026
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: 2

🤖 Fix all issues with AI agents
In `@src/modules/welcome.js`:
- Around line 22-43: The two test-only helpers __getCommunityActivityState and
__resetCommunityActivityState are exported unconditionally; gate their exports
so they are only available in non-production environments (e.g., when
process.env.NODE_ENV !== 'production') or behind a clearly named dev-only flag.
Update the module to conditionally export these functions (or export a single
devHelpers object) based on NODE_ENV, and keep normal runtime exports unchanged
so production consumers cannot call __getCommunityActivityState or
__resetCommunityActivityState.

In `@tests/modules/welcome.test.js`:
- Around line 141-188: The test currently calls vi.restoreAllMocks() and
__resetCommunityActivityState() at the end which won't run if an assertion
throws; ensure cleanup always runs by moving those calls into an afterEach hook
(or wrapping the test body in try/finally) so mocks are restored and community
state reset regardless of failures — update the test that uses
recordCommunityActivity, __getCommunityActivityState, vi.spyOn(Date, 'now'),
vi.restoreAllMocks, and __resetCommunityActivityState to use afterEach cleanup
or try/finally.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7661eb6 and 68e16cf.

📒 Files selected for processing (2)
  • src/modules/welcome.js
  • tests/modules/welcome.test.js
🧰 Additional context used
📓 Path-based instructions (3)
src/**/*.js

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.js: Use ESM syntax with import/export; never use require()
Always use node: protocol when importing Node.js builtins (e.g., import { readFileSync } from 'node:fs')
Always use semicolons in code
Use single quotes for strings (enforced by Biome)
Use 2-space indentation (enforced by Biome)
Always use Winston logging via import { info, warn, error } from '../logger.js' — NEVER use console.log(), console.warn(), console.error(), or any console.* method in src/ files
Pass structured metadata when logging: info('Message processed', { userId, channelId })
Use custom error classes from src/utils/errors.js for error handling
Always log errors with context before re-throwing
Use getConfig() from src/modules/config.js to read config; use setConfigValue(key, value) to update at runtime
Use splitMessage() utility for messages exceeding Discord's 2000-character limit
No TypeScript — use plain JavaScript with JSDoc comments for documentation

Files:

  • src/modules/welcome.js
src/modules/*.js

📄 CodeRabbit inference engine (AGENTS.md)

Check config.yourModule.enabled before processing in module handlers

Files:

  • src/modules/welcome.js
tests/**/*.{js,test.js}

📄 CodeRabbit inference engine (AGENTS.md)

Use Vitest for testing with pnpm test; test coverage provider is @vitest/coverage-v8

Files:

  • tests/modules/welcome.test.js
🧬 Code graph analysis (1)
tests/modules/welcome.test.js (1)
src/modules/welcome.js (3)
  • __resetCommunityActivityState (39-43)
  • recordCommunityActivity (66-105)
  • __getCommunityActivityState (27-34)
🔇 Additional comments (4)
src/modules/welcome.js (3)

11-14: LGTM!

The throttled eviction constants are well-defined. The module-level counter with a constant interval is a clean approach for periodic pruning.


98-105: LGTM!

The periodic eviction trigger is clean — counter increment, threshold check, reset, and prune. The cutoff value reuses the already-computed threshold from the current call, which is correct.


111-123: LGTM!

The pruning logic is sound. Since timestamps are appended chronologically, checking only the last element against the cutoff is sufficient to determine staleness. Deleting from a Map during for...of iteration is safe per the ES6 specification.

tests/modules/welcome.test.js (1)

12-13: LGTM!

New test helper imports align with the exports added in the source module.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@BillChirico BillChirico merged commit 9ce58de into VolvoxLLC:main Feb 12, 2026
1 check passed
BillChirico added a commit that referenced this pull request Feb 25, 2026
… (#83)

* feat(dashboard): add Discord entity pickers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* refactor: extract shared config allowlist, webhook utility, and proxy helpers; remove dead code

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* refactor(backend): extract shared validators and getBotOwnerIds, add webhook utility tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>
Co-authored-by: AnExiledDev <AnExiledDev@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
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.

2 participants