Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,6 @@ updates:
prod-deps:
applies-to: version-updates
dependency-type: production
# Pin nodemailer to its current major while next-auth's peer-dep catches up.
# Re-evaluate when next-auth 5 stable lists nodemailer@^8 as supported.
exclude-patterns:
- "nodemailer"
dev-deps:
applies-to: version-updates
dependency-type: development
Expand All @@ -34,14 +30,10 @@ updates:
prod-deps-security:
applies-to: security-updates
dependency-type: production
exclude-patterns:
- "nodemailer"
dev-deps-security:
applies-to: security-updates
dependency-type: development
ignore:
- dependency-name: "nodemailer"
update-types: ["version-update:semver-major"]
# Prisma 7 moves datasource URL out of schema.prisma into prisma.config.ts.
# Migration is non-trivial; pin until we explicitly do it.
- dependency-name: "prisma"
Expand Down
5 changes: 2 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,8 @@ jobs:

# Production deps only — dev tooling CVEs add noise without changing
# production attack surface. The allowlist in scripts/audit-check.mjs covers
# triaged, no-fix advisories (nodemailer is pinned to 7 by next-auth) so the
# gate stays meaningful instead of permanently red — it still fails on any
# NEW high/critical advisory.
# triaged, no-fix advisories so the gate stays meaningful instead of
# permanently red — it still fails on any NEW high/critical advisory.
- name: npm audit (prod, high/critical, triaged allowlist)
run: |
npm audit --json --omit=dev > npm-audit.json || true
Expand Down
7 changes: 3 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ This is a combined RMS (Resource Management System) backend + marketing site for

### Database

- PostgreSQL 16 in Docker, not exposed to host in production
- PostgreSQL 16 in Docker, published on host loopback only (`127.0.0.1:5432`) — never the public interface
- 22 Prisma models — see schema.prisma
- Migrations via `prisma db push` (no migration files, push-based)
- Seed scripts in prisma/ directory
Expand All @@ -170,9 +170,8 @@ This is a combined RMS (Resource Management System) backend + marketing site for
- **Rate limiting is in-memory:** `server/rate-limit.ts` uses a Map — resets on server restart, doesn't work across multiple instances. Acceptable for single-instance VPS deployment.
- **No email queue retry backoff:** `app/api/cron/process-emails/route.ts` retries failed emails up to 3 times with no delay between attempts — should add exponential backoff if email failures become common
- **Portal token is permanent:** Client portal tokens (cuid) never expire and can't be rotated without DB manual intervention — add rotation mechanism before handling sensitive client data
- **Accepted-risk dependency vulns (CI-allowlisted):** One transitive advisory is open with **no fix available** and is deliberately allowlisted in `scripts/audit-check.mjs` — the `npm audit` CI gate still fails on any *new* high/critical advisory, just not this one.
- **postcss GHSA-qx2v-qp2m-jg93** (moderate, XSS in CSS stringify) — transitive via Next.js's bundled postcss; not exploitable through our surface (no untrusted input to postcss stringify). Resolves when Next bumps its bundled postcss.
- **nodemailer forced to 9 via `overrides`:** the 6 nodemailer advisories (incl. one high) were *resolved* by upgrading to nodemailer 9. next-auth's `@auth/core` declares an **optional** `nodemailer@^7` peer (only for its Email provider, which we don't use — auth is Credentials-only in `server/auth/index.ts`), so `package.json` `overrides` forces the whole tree to 9 — runtime-safe, no `--legacy-peer-deps`. Drop the override when next-auth's peer range includes nodemailer 9+.
- **Prod DB password is the public dev default:** `docker-compose.yml` hardcodes `simplerms_dev` for dev *and* prod, and this repo is public. Mitigated by loopback-only port binding plus the cloud security group (only 22/80/443 reachable). Rotate it (move `DATABASE_URL` into the server's `.env.local`) if this box outlives the Holt migration.
- **nodemailer forced via `overrides`:** `"nodemailer": "$nodemailer"` forces the whole tree to our direct nodemailer (now 10). next-auth's `@auth/core` declares an **optional** `nodemailer@^7` peer used only by its Email provider, which we don't use (Credentials-only in `server/auth/index.ts`) — runtime-safe, no `--legacy-peer-deps`. Side effect: `npm audit fix` and `npm update` crash on the `$` reference — raise the override floors in `package.json` by hand instead.

### Testing & Deployment

Expand Down
1 change: 0 additions & 1 deletion docker-compose.prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,3 @@ services:

db:
restart: always
ports: []
9 changes: 6 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ services:
POSTGRES_DB: simplerms
volumes:
- postgres_data:/var/lib/postgresql/data
# Loopback only: Docker-published ports bypass host firewalls (UFW), and
# override files can't clear a base port list — `ports: []` merges, not replaces.
ports:
- "5432:5432"
- "127.0.0.1:5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U simplerms"]
interval: 5s
Expand Down Expand Up @@ -51,9 +53,10 @@ services:
mailpit:
image: axllent/mailpit:latest
restart: unless-stopped
# Loopback only: the 8025 UI serves every captured email without auth.
ports:
- "1025:1025"
- "8025:8025"
- "127.0.0.1:1025:1025"
- "127.0.0.1:8025:8025"

volumes:
postgres_data:
Expand Down
10 changes: 10 additions & 0 deletions docs/DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,13 @@ The AI framework is the cleaner reference implementation of the pattern.
**Why:** Upgrading nodemailer 7→9 fixes the 6 advisories (one high) that had no other fix, so the allowlist shrinks to postcss only. `@auth/core`'s nodemailer peer is **optional** and used only by next-auth's Email provider — we use the **Credentials** provider only (`server/auth/index.ts`), so next-auth never loads nodemailer. The override is therefore runtime-safe and lets `npm ci` resolve without `--legacy-peer-deps` (which hides real breakage — see LESSONS).

**Alternatives:** bump next-auth to a beta whose peer allows nodemailer 9 (rejected — bumping the auth library is riskier than a scoped override); `--legacy-peer-deps` in CI (rejected). Remove the override once next-auth's peer range includes nodemailer 9+.

---

### 2026-09-30: nodemailer 10; `shadcn` moves to devDependencies

**Decision:** Upgrade nodemailer 9 → 10 (the existing `$nodemailer` override carries it through the tree) and drop the Dependabot rules that held nodemailer to one major. Move `shadcn` from `dependencies` to `devDependencies`.

**Why:** The 9.x line has no fix for a high addressparser ReDoS (GHSA-v53p-9fqp-m79j, fixed in 10.0.6); the major-version pin is how nodemailer fell a full major behind a security fix. `shadcn` is a component CLI plus one build-time CSS import (`@import "shadcn/tailwind.css"`); the Docker builder stage installs dev deps and production ships only `.next/standalone`, so nothing from it runs on the server. Listing it as a runtime dep pulled its MCP SDK tree (hono, express, ip-address) into the production audit for code we never deploy.

**Alternatives:** keep nodemailer 9 and allowlist the ReDoS (rejected — a fix exists); keep `shadcn` in `dependencies` and chase its transitive advisories with more overrides (rejected — misclassifies build tooling as runtime).
10 changes: 10 additions & 0 deletions docs/LESSONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,16 @@ Read at session start (loaded by `/boot`). Add to it whenever:

---

## 2026-09-30 — `ports: []` in a Compose override does not unpublish a port

**What happened:** `docker-compose.prod.yml` set `db.ports: []` to keep Postgres off the host, and the docs said it was "not exposed". `docker compose config` showed `5432` still published on every interface — Compose *merges* `ports` lists across files, so an empty list adds nothing and removes nothing. Docker's iptables rules also bypass UFW. Only the cloud security group kept Postgres (with the public-repo dev password) off the internet.

**Lesson:** Verify exposure with `docker compose -f … -f … config` and an external port probe, never by reading an override. Bind in the base file (`127.0.0.1:5432:5432`); `!reset []` also works but needs Compose ≥ 2.24 on the server.

**Where it applies:** `docker-compose*.yml`, any service with `ports:`.

---

## How to add to this file

When you finish a task and a real lesson emerged, add an entry. Keep it terse. The point is to avoid repeating the mistake — not to write an essay. If the lesson is big enough to drive an architectural change, it goes in `docs/DECISIONS.md` instead. If it's about how the codebase works, update `CLAUDE.md`. If it's about how *we* work — it lives here.
Loading
Loading