Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
2f5769e
docs(security): add api hardening plans
Abdulkhalek-1 Apr 7, 2026
aca93d7
fix(security): harden dashboard api surface (sec-api-01..11)
Abdulkhalek-1 Apr 7, 2026
eef01c9
docs(security): add auth hardening plans
Abdulkhalek-1 Apr 7, 2026
be61054
fix(security): harden auth and session flows (sec-auth-01..09)
Abdulkhalek-1 Apr 7, 2026
bc5fe59
docs(security): add bot hardening plans
Abdulkhalek-1 Apr 7, 2026
17c8485
fix(security): harden bot runtime surface (sec-bot-01..09)
Abdulkhalek-1 Apr 7, 2026
2166835
docs(security): add data hardening plans
Abdulkhalek-1 Apr 7, 2026
51ae800
fix(security): harden data layer and persistence (sec-data-01..08)
Abdulkhalek-1 Apr 7, 2026
2ea3343
docs(security): add infra hardening plans
Abdulkhalek-1 Apr 7, 2026
30fb7d5
fix(security): harden infrastructure and CI (sec-infra-01..12)
Abdulkhalek-1 Apr 7, 2026
943d2e8
refactor(dashboard): unify icons on Lucide, drop Material Symbols font
Abdulkhalek-1 Jul 7, 2026
6a8495e
fix(dashboard): resolve UI/UX review findings (a11y, dead controls, l…
Abdulkhalek-1 Jul 7, 2026
57caaad
chore(i18n): remove unused header/sidebar keys from all locales
Abdulkhalek-1 Jul 7, 2026
74fc2ae
perf(dashboard): code-split route pages and vendor chunks
Abdulkhalek-1 Jul 7, 2026
907a04c
fix(dashboard): a11y in shared Discord selects; add volume icon
Abdulkhalek-1 Jul 7, 2026
fd1b7d8
fix(automation): editor accessibility + remove dead code
Abdulkhalek-1 Jul 7, 2026
3d2a58a
fix(dashboard): accessibility & UX sweep across feature pages
Abdulkhalek-1 Jul 7, 2026
2d4950c
fix(dashboard): harden design tokens (Discord contrast, type scale, m…
Abdulkhalek-1 Jul 7, 2026
5f9a2d5
fix(docker): mount pgdata at postgres root for Postgres 18
Abdulkhalek-1 Jul 7, 2026
919dce1
chore(deps): sync lockfile for @playwright/test and @radix-ui/react-d…
Abdulkhalek-1 Jul 7, 2026
672e354
feat(i18n): complete dashboard i18n coverage across 48 locales
Abdulkhalek-1 Jul 7, 2026
cdf6a16
fix(dashboard): language switcher scrolling, RTL, and hover
Abdulkhalek-1 Jul 7, 2026
93945ca
fix(dashboard): RTL mirroring, icon-fill corruption & clipped focus r…
Abdulkhalek-1 Jul 7, 2026
0f775ab
feat(dashboard): enable component animations via tw-animate-css
Abdulkhalek-1 Jul 7, 2026
c11021d
fix(automation): pin node detail panel to physical right in RTL
Abdulkhalek-1 Jul 7, 2026
87d7717
fix(automation): force workflow canvas to LTR so nodes render in RTL
Abdulkhalek-1 Jul 7, 2026
4cf6296
fix(dashboard): stop Radix ScrollArea content overflowing horizontally
Abdulkhalek-1 Jul 7, 2026
b8661f6
docs(dashboard): add variable text fields design spec
Abdulkhalek-1 Jul 7, 2026
d4d5c78
fix(dashboard): authorize guild admin against live Discord state
Abdulkhalek-1 Jul 8, 2026
541e3e6
feat(dashboard): add servers refresh button and owner-aware guild list
Abdulkhalek-1 Jul 8, 2026
68b66c4
chore(hooks): make test-reminder CWD-independent and convention-aware
Abdulkhalek-1 Jul 8, 2026
11f69b6
docs(dashboard): add variable text fields implementation plan
Abdulkhalek-1 Jul 8, 2026
8384932
test(dashboard): add jsdom + testing-library client test toolchain
Abdulkhalek-1 Jul 8, 2026
a638b54
feat(dashboard): add variable-field token types and tokenizer
Abdulkhalek-1 Jul 8, 2026
01f8121
feat(dashboard): add unknown-variable detection with suggestions
Abdulkhalek-1 Jul 8, 2026
d26401f
feat(dashboard): add caret insert + active-query helpers
Abdulkhalek-1 Jul 8, 2026
c287830
feat(dashboard): add variable autocomplete filtering
Abdulkhalek-1 Jul 8, 2026
a8a45a1
feat(dashboard): add per-scope variable registry with drift guard
Abdulkhalek-1 Jul 8, 2026
64c0a3a
feat(dashboard): build event-scoped automation variables
Abdulkhalek-1 Jul 8, 2026
eed0528
feat(dashboard): add pure preview template resolver
Abdulkhalek-1 Jul 8, 2026
1381f36
feat(dashboard): add VariableEditor with autocomplete, highlighting, …
Abdulkhalek-1 Jul 8, 2026
a6781a0
fix(dashboard): a11y aria-activedescendant + pristine classes in Vari…
Abdulkhalek-1 Jul 8, 2026
18a8852
feat(dashboard): add searchable VariableBrowser popover
Abdulkhalek-1 Jul 8, 2026
6b1fde6
feat(dashboard): add usePreviewContext hook
Abdulkhalek-1 Jul 8, 2026
e5039ea
feat(dashboard): add Discord-style message/embed preview
Abdulkhalek-1 Jul 8, 2026
a452406
feat(dashboard): barrel export variable-field + i18n chrome keys
Abdulkhalek-1 Jul 8, 2026
e4bddaa
feat(dashboard): variable editor + preview in welcome/farewell/DM
Abdulkhalek-1 Jul 8, 2026
7873a1f
feat(dashboard): variable editor + preview in custom commands
Abdulkhalek-1 Jul 8, 2026
ea32d8a
feat(dashboard): variable editor + preview in scheduled messages
Abdulkhalek-1 Jul 8, 2026
2bd10fd
feat(dashboard): variable editor + preview in leveling announce
Abdulkhalek-1 Jul 8, 2026
9a007a6
feat(dashboard): variable editor + preview in tempvoice name template
Abdulkhalek-1 Jul 8, 2026
a07a698
feat(automation): event-scoped variable editor in action fields
Abdulkhalek-1 Jul 8, 2026
a6a646b
fix(automation): event-scoped vars for step actions + label/id associ…
Abdulkhalek-1 Jul 8, 2026
5ceaef4
feat(automation): Discord preview for message/embed/DM actions
Abdulkhalek-1 Jul 8, 2026
b0c2022
fix(dashboard): single-border VariableEditor overlay + restore token …
Abdulkhalek-1 Jul 8, 2026
7e71b5e
feat(i18n): variable description keys for the variable browser
Abdulkhalek-1 Jul 8, 2026
ff15c6b
fix(deps): sync pnpm-lock.yaml with jsdom + testing-library devDeps
Abdulkhalek-1 Jul 8, 2026
a7fd8d8
fix(dashboard): coverage glob, commands preview gating, aria-required…
Abdulkhalek-1 Jul 8, 2026
cde7641
merge: #42 security-api hardening (sec-api-01..11)
Abdulkhalek-1 Jul 8, 2026
0b2ac1f
merge: #45 security-data layer hardening (sec-data-01..08)
Abdulkhalek-1 Jul 8, 2026
ee6fb2f
merge: #44 bot runtime hardening (sec-bot-01..09)
Abdulkhalek-1 Jul 8, 2026
ed9062d
merge: #43 auth/session/csrf/csp hardening (sec-auth-01..09)
Abdulkhalek-1 Jul 8, 2026
cb66472
merge: #46 infra/CI hardening (sec-infra-01..12)
Abdulkhalek-1 Jul 8, 2026
1df727b
merge: #47 UI/UX review remediation (Lucide, a11y, code-splitting, i18n)
Abdulkhalek-1 Jul 8, 2026
95eaa7a
merge: #48 variable-field editor + Discord preview for {variable} fields
Abdulkhalek-1 Jul 8, 2026
c85db34
fix(dashboard): attach X-CSRF-Token on mutating requests (companion t…
Abdulkhalek-1 Jul 8, 2026
fe2ca56
chore(deps): reconcile pnpm-lock.yaml with merged manifests (add safe…
Abdulkhalek-1 Jul 8, 2026
c5ba7ca
test: green inherited PR/main test debt after merging #42-#48
Abdulkhalek-1 Jul 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
16 changes: 15 additions & 1 deletion .claude/hooks/test-reminder.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ if ! echo "$LAST_MESSAGE" | grep -qiE '(created|wrote|added|implemented|built|up
exit 0
fi

# git returns repo-root-relative paths, but this hook may run from any CWD, so
# resolve file existence against the repo root rather than the current dir.
REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || echo ".")

# Find source files modified in the working tree (staged + unstaged)
CHANGED_SRC_FILES=$(git diff --name-only HEAD 2>/dev/null || git diff --name-only --cached 2>/dev/null || echo "")

Expand Down Expand Up @@ -56,7 +60,17 @@ while IFS= read -r file; do
continue
fi

if [ -n "$TEST_FILE" ] && [ ! -f "$TEST_FILE" ]; then
if [ -n "$TEST_FILE" ] && [ ! -f "$REPO_ROOT/$TEST_FILE" ]; then
# This repo consolidates a feature's tests into <feature>.test.ts within the
# mirrored test directory (e.g. features/guilds/routes.ts is covered by
# tests/server/features/guilds/guilds.test.ts). Accept that convention —
# a <parentDirName>.test.ts alongside the expected path — but nothing broader,
# so genuinely untested files elsewhere are still flagged.
TEST_DIR=$(dirname "$TEST_FILE")
PARENT_DIR=$(basename "$(dirname "$file")")
if [ -f "$REPO_ROOT/$TEST_DIR/$PARENT_DIR.test.ts" ]; then
continue
fi
MISSING_TESTS="${MISSING_TESTS}\n - ${file} → missing ${TEST_FILE}"
fi
done <<< "$ALL_FILES"
Expand Down
4 changes: 2 additions & 2 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
node_modules
dist
.env
.env.dev
.env.prod
.env.*
!.env.example
.git
.gitignore
*.md
Expand Down
48 changes: 45 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ LOG_LEVEL=info

# Database (PostgreSQL) - points to Docker container hostname
DATABASE_URL=postgresql://fluxcore:fluxcore@postgres:5432/fluxcore
# Optional dev overrides for the local postgres container — defaults shown
# POSTGRES_USER=fluxcore
# POSTGRES_PASSWORD=fluxcore
# POSTGRES_DB=fluxcore

# === Vite dev server (dashboard) ===
# Defaults to 127.0.0.1 (loopback only). Set to 0.0.0.0 ONLY when running
# inside Docker — exposing Vite to all interfaces on a bare-metal host is
# unsafe. The docker-compose dashboard service sets this automatically.
# VITE_HOST=0.0.0.0

# === Bot only ===

Expand All @@ -24,7 +34,9 @@ GUILD_ID=

# Port for the bot's internal HTTP cache sync server
BOT_SYNC_PORT=3001
# Shared secret for cache sync authentication (generate a random string)
# Shared secret for cache sync authentication.
# REQUIRED in production (NODE_ENV=production); auto-generated in development.
# Must be at least 32 characters. Generate with: openssl rand -hex 32
BOT_SYNC_SECRET=
# URL the dashboard uses to reach the bot's sync server (Docker service name)
BOT_SYNC_URL=http://bot:3001
Expand All @@ -43,8 +55,10 @@ DASHBOARD_SESSION_SECRET=
LAVALINK_HOST=lavalink
# Lavalink server port
LAVALINK_PORT=2333
# Lavalink server password (must match lavalink/application.yml)
LAVALINK_PASSWORD=youshallnotpass
# Lavalink server password (REQUIRED).
# Must match the LAVALINK_SERVER_PASSWORD env var the lavalink container reads.
# Generate with: openssl rand -base64 32
LAVALINK_PASSWORD=

# === Infrastructure (production only) ===

Expand All @@ -57,3 +71,31 @@ DASHBOARD_DOMAIN=localhost
# Backup schedule (cron expression, default: daily at 2am)
BACKUP_SCHEDULE=0 2 * * *
BACKUP_RETENTION_DAYS=7

# === Dev tools profile (pgadmin) ===
# Required when running: docker compose --profile tools up pgadmin
# PGADMIN_EMAIL=admin@fluxcore.local
# PGADMIN_PASSWORD=

# === Production secrets (Docker secrets — DO NOT commit values) ===
# In production, place each secret as a file under ./secrets/<name>:
# secrets/discord_token
# secrets/dashboard_client_secret
# secrets/dashboard_session_secret
# secrets/bot_sync_secret
# secrets/lavalink_password
# secrets/postgres_password
# Generate strong values with: openssl rand -base64 32
#
# The Node services read secrets via *_FILE env vars (see packages/config
# resolveSecretFiles). Example overrides if running outside docker-compose:
# DISCORD_TOKEN_FILE=/run/secrets/discord_token
# DASHBOARD_CLIENT_SECRET_FILE=/run/secrets/dashboard_client_secret
# DASHBOARD_SESSION_SECRET_FILE=/run/secrets/dashboard_session_secret
# BOT_SYNC_SECRET_FILE=/run/secrets/bot_sync_secret
# LAVALINK_PASSWORD_FILE=/run/secrets/lavalink_password
# POSTGRES_PASSWORD_FILE=/run/secrets/postgres_password
# DATABASE_PASSWORD_FILE=/run/secrets/postgres_password # alias for deployments
#
# If DATABASE_URL is unset, it is constructed at startup from POSTGRES_USER,
# POSTGRES_PASSWORD (or POSTGRES_PASSWORD_FILE), POSTGRES_HOST, POSTGRES_DB.
143 changes: 143 additions & 0 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
name: Security

on:
push:
branches: [main]
pull_request:
schedule:
- cron: "0 6 * * 1" # Mondays 06:00 UTC

permissions:
contents: read
security-events: write

jobs:
pnpm-audit:
name: pnpm audit (high+)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10.28.0
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Audit (fail on high or critical)
run: pnpm audit --audit-level=high

gitleaks:
name: gitleaks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Run gitleaks
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITLEAKS_ENABLE_UPLOAD_ARTIFACT: true

trivy-bot:
name: trivy (bot image)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build bot image
run: docker build --target production-bot -t fluxcore-bot:ci .
- name: Trivy scan
uses: aquasecurity/trivy-action@0.28.0
with:
image-ref: fluxcore-bot:ci
severity: HIGH,CRITICAL
exit-code: "1"
ignore-unfixed: true
vuln-type: os,library
format: sarif
output: trivy-bot.sarif
- name: Upload SARIF
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: trivy-bot.sarif
category: trivy-bot

trivy-dashboard:
name: trivy (dashboard image)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build dashboard image
run: docker build --target production-dashboard -t fluxcore-dashboard:ci .
- name: Trivy scan
uses: aquasecurity/trivy-action@0.28.0
with:
image-ref: fluxcore-dashboard:ci
severity: HIGH,CRITICAL
exit-code: "1"
ignore-unfixed: true
vuln-type: os,library
format: sarif
output: trivy-dashboard.sarif
- name: Upload SARIF
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: trivy-dashboard.sarif
category: trivy-dashboard

trivy-fs:
name: trivy (filesystem & config)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Trivy filesystem scan (config, IaC, secrets)
uses: aquasecurity/trivy-action@0.28.0
with:
scan-type: fs
scan-ref: .
severity: HIGH,CRITICAL
exit-code: "1"
ignore-unfixed: true
scanners: vuln,secret,config

forbidden-strings:
name: forbidden strings
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Block re-introduction of default lavalink password
run: |
if grep -RIn --exclude-dir=.git --exclude-dir=node_modules --exclude='*.md' 'youshallnotpass' .; then
echo "::error::The literal 'youshallnotpass' must never be committed."
exit 1
fi
- name: Block public postgres exposure
run: |
if grep -RIn --include='docker-compose*.yml' -E '"(0\.0\.0\.0:)?5432:5432"' .; then
echo "::error::Postgres must only bind 127.0.0.1, never 0.0.0.0 or unbound."
exit 1
fi

no-env-files:
name: no env files in repo
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Assert no .env files are tracked
run: |
tracked=$(git ls-files | grep -E '^\.env(\..+)?$' | grep -v '^\.env\.example$' || true)
if [ -n "$tracked" ]; then
echo "::error::These env files must not be committed:"
echo "$tracked"
exit 1
fi
- name: Assert .dockerignore covers env files
run: |
grep -qE '^\.env\*?$|^\.env\.\*$' .dockerignore || {
echo "::error::.dockerignore must include '.env' and '.env.*'"
exit 1
}
7 changes: 5 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
node_modules/
dist/
.env
.env.dev
.env.prod
.env.*
!.env.example
*.js.map
*.d.ts
!src/**/*.d.ts
Expand All @@ -26,3 +26,6 @@ out/
apps/dashboard/tests/e2e/.auth/
# E2E HTML report
apps/dashboard/tests/e2e/.report/
# Production Docker secrets — keep the directory but never commit values
/secrets/*
!/secrets/.gitkeep
11 changes: 7 additions & 4 deletions .gitleaks.toml
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
title = "FluxCore Gitleaks Configuration"
title = "FluxCore gitleaks config"

[extend]
useDefault = true

[allowlist]
description = "Allow common false positives"
description = "Test fixtures and example placeholders"
paths = [
'''\.env\.example''',
'''.*\.test\.(ts|js)$''',
'''packages/systems/tests/.*''',
'''docs/.*''',
'''\.env\.example$''',
'''pnpm-lock\.yaml''',
]
]
9 changes: 6 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,17 @@ COPY apps/bot/package.json ./apps/bot/
COPY apps/dashboard/package.json ./apps/dashboard/
COPY packages/database/prisma ./packages/database/prisma/
COPY packages/database/prisma.config.ts ./packages/database/
RUN pnpm install --frozen-lockfile
RUN pnpm install --frozen-lockfile \
&& chown -R node:node /app

FROM deps AS development
COPY . .
COPY --chown=node:node . .
USER node
CMD ["pnpm", "dev"]

FROM deps AS test
COPY . .
COPY --chown=node:node . .
USER node
CMD ["pnpm", "test"]

# ============================================
Expand Down
32 changes: 26 additions & 6 deletions apps/bot/src/events/guildMemberAdd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { sendLogEmbed } from "@fluxcore/systems/logging/sender";
import { formatMemberJoin } from "@fluxcore/systems/logging/formatter";
import { getWelcomeConfig } from "@fluxcore/systems/welcome/config";
import { buildWelcomeEmbed } from "@fluxcore/systems/welcome/builder";
import { generateWelcomeImage, createStorageAdapter } from "@fluxcore/systems/welcome/image";
import { AttachmentBuilder } from "discord.js";
import { generateWelcomeImage, createStorageAdapter, sanitizeDisplayName } from "@fluxcore/systems/welcome/image";
import { AttachmentBuilder, DiscordAPIError } from "discord.js";
import { getAntiRaidConfig } from "@fluxcore/systems/antiraid/config";
import { recordJoin } from "@fluxcore/systems/antiraid/tracker";
import { executeRaidAction, lockdownGuild } from "@fluxcore/systems/antiraid/actions";
Expand Down Expand Up @@ -133,7 +133,24 @@ const event: Event<"guildMemberAdd"> = {
});
if (rolesToAdd.length > 0) {
await member.roles.add(rolesToAdd, "Auto-role on join").catch((err) => {
logger.error(`Failed to assign auto-roles in guild ${member.guild.id}`, err instanceof Error ? err : new Error(String(err)));
if (err instanceof DiscordAPIError) {
if (err.code === 50013) {
logger.warn(
`auto-role: missing permissions in guild ${member.guild.id} (role likely moved above bot between cache check and API call)`,
);
return;
}
if (err.code === 10011) {
logger.warn(
`auto-role: unknown role in guild ${member.guild.id} (role was deleted between cache check and API call)`,
);
return;
}
}
logger.error(
`Failed to assign auto-roles in guild ${member.guild.id}`,
err instanceof Error ? err : new Error(String(err)),
);
});
}
}
Expand All @@ -149,15 +166,18 @@ const event: Event<"guildMemberAdd"> = {
if (welcomeConfig.welcomeImageEnabled) {
try {
const storage = createStorageAdapter();
const safeUsername = sanitizeDisplayName(member.user.username, 32);
const safeDisplayName = sanitizeDisplayName(member.displayName, 80);
const safeGuildName = sanitizeDisplayName(member.guild.name, 80);
const imageBuffer = await generateWelcomeImage({
settings: welcomeConfig.welcomeImageConfig,
member: {
username: member.user.username,
displayName: member.displayName,
username: safeUsername,
displayName: safeDisplayName,
avatarUrl: member.user.displayAvatarURL({ extension: "png", size: 256 }),
},
guild: {
name: member.guild.name,
name: safeGuildName,
iconUrl: member.guild.iconURL({ size: 256 }) ?? undefined,
memberCount: member.guild.memberCount,
},
Expand Down
Loading
Loading