Skip to content

Slash Commands Support#8

Closed
BillChirico wants to merge 7 commits intomainfrom
auto-claude/008-slash-commands-support
Closed

Slash Commands Support#8
BillChirico wants to merge 7 commits intomainfrom
auto-claude/008-slash-commands-support

Conversation

@BillChirico
Copy link
Collaborator

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.

BillChirico and others added 7 commits February 3, 2026 20:38
… 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
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 4, 2026

Warning

Rate limit exceeded

@BillChirico has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 3 minutes and 9 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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.

📥 Commits

Reviewing files that changed from the base of the PR and between 0286a77 and 7672173.

📒 Files selected for processing (6)
  • .auto-claude-security.json
  • .auto-claude-status
  • .claude_settings.json
  • .gitignore
  • src/commands.js
  • src/index.js
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch auto-claude/008-slash-commands-support

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

@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 4 potential issues.

Bugbot Autofix is ON. A Cloud Agent has been kicked off to fix the reported issues.

"created_at": "2026-02-03T19:51:09.135836",
"project_hash": "51a4f617fc8ece9b63e20f8a9950e73b",
"inherited_from": "/Users/billchirico/Developer/bill-bot"
} No newline at end of file
Copy link

Choose a reason for hiding this comment

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

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)

Fix in Cursor Fix in Web

}
} else {
await interaction.editReply(response);
}
Copy link

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

}
} else {
await interaction.editReply(response);
}
Copy link

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

.setFooter({ text: `${client.user.tag}` })
.setTimestamp();

await interaction.reply({ embeds: [embed] });
Copy link

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

@cursor
Copy link

cursor bot commented Feb 4, 2026

Bugbot Autofix prepared fixes for 4 of the 4 bugs found in the latest run.

  • ✅ Fixed: Local development files accidentally committed to repository
    • Deleted the three local development files (.auto-claude-security.json, .auto-claude-status, .claude_settings.json) and added them to .gitignore to prevent future commits.
  • ✅ Fixed: Slash command /ask ignores AI disabled config setting
    • Added a check for config.ai?.enabled at the start of the /ask handler that returns an ephemeral error message if AI is disabled.
  • ✅ Fixed: Slash command /ask ignores channel restriction configuration
    • Added channel restriction check using config.ai?.channels in the /ask handler, consistent with the message-based AI handler logic.
  • ✅ Fixed: Status command may timeout during API health check
    • Added deferReply() call at the start of the /status handler before the API health check, and changed reply() to editReply() for the final response.

Create PR

Or push these changes by commenting:

@cursor push 557fd33f83

@BillChirico
Copy link
Collaborator Author

Closing - slash commands already in main

@BillChirico BillChirico closed this Feb 4, 2026
@BillChirico BillChirico deleted the auto-claude/008-slash-commands-support branch February 4, 2026 02:43
BillChirico added a commit that referenced this pull request Feb 16, 2026
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.
BillChirico added a commit that referenced this pull request Feb 16, 2026
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.
BillChirico added a commit that referenced this pull request Feb 16, 2026
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.
BillChirico added a commit that referenced this pull request Feb 16, 2026
…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
BillChirico added a commit that referenced this pull request Feb 17, 2026
- 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)
BillChirico added a commit that referenced this pull request Feb 17, 2026
- 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)
BillChirico added a commit that referenced this pull request Feb 24, 2026
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.
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 <[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>
BillChirico added a commit that referenced this pull request Feb 25, 2026
Issue #7: log which specific env var(s) are missing instead of a
blanket 'both are required' message.
Issue #8: use structured logging for the fetch error instead of
passing the raw error object as a positional arg.
BillChirico added a commit that referenced this pull request Feb 28, 2026
…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.
BillChirico added a commit that referenced this pull request Feb 28, 2026
* 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]>
BillChirico added a commit that referenced this pull request Feb 28, 2026
AI Chat already uses MessageSquare. Conversations now uses MessagesSquare
to distinguish the two nav items visually.
BillChirico added a commit that referenced this pull request Feb 28, 2026
* 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
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.

1 participant