7.8 KiB
Study Desk — Overnight Read-Only Audit: Executive Summary
Status: COMPLETE (2026-08-06)
Scope and method
Comprehensive read-only audit of the Study repository (Next.js 16 / React 19 / Prisma 7 / SQLite, self-hosted single-user study app). Ten specialist subagents audited: (1) Prisma schema/migrations/transactions/cascades/drift, (2) quiz scoring/attempts/retakes/progress, (3) flashcards/SRS/progress/ordering, (4) auth/protected routes/API authorization/sharing, (5) imports/exports/generation/Zod validation, (6) material groups/library/drag-drop/deletion, (7) frontend state/races/localStorage/boundaries, (8) test quality, (9) Docker/env/production/recovery, (10) adversarial review challenging every Critical/High finding and hunting missed issues. The parent agent independently re-verified all Critical/High claims (code tracing, git history, temp-DB migration chain, DB introspection, Prisma-shaped SQL probes). The Arcade feature set was excluded per scope unless it affects the main app, shared dependencies, database integrity, build, or deployment.
Constraints honored
- No source code, tests, migrations, config, package files, lockfiles, docs, or databases modified. Final
git status --short: only untracked.reasonix/andaudit-results/;git diff --checkclean;src/generated/prisma/byte-identical before/after all prisma commands (sha256). - All reports and diagnostic artifacts live under
audit-results/(reports +tmp/:audit-migration.db,inspect-db.cjs,reproduce-drift.cjs,validate-findings.cjs,generated-before.sha256). - Existing databases: none found in the workspace; the migration-chain test used a brand-new temp DB.
Baseline verification
| Check | Result |
|---|---|
npm test |
✅ PASS — 30 tests / 6 files (20 arcade out of scope, 10 spacedRepetition) |
npm run lint |
⚠️ 28 problems (9 errors, 19 warnings) — all pre-existing baseline |
npm run build |
✅ PASS — full route table incl. "ƒ Proxy (Middleware)" |
npx prisma validate |
✅ PASS |
| Migration chain on fresh temp DB | ✅ PASS — 4/4 migrations apply, but the resulting DB is missing MaterialGroup + groupId columns (drift, see below) |
Findings (53 total: 2 Critical, 5 High, 18 Medium, 25 Low, 3 Observation)
Full detail in FINDINGS.json (validated) and CONFIRMED_FINDINGS.md.
Critical
- FIN-01 — Schema/migration drift (Confirmed, runtime-verified).
prisma/schema.prismadefinesMaterialGroupandDeck/QuizSet/ShareLink.groupId(added by commit7af0935, which also rewrote a committeddev.dbviaprisma db push) but no migration creates them. Every DB built byprisma migrate deploy— thepredevhook and the Docker entrypoint — lacks these objects; the generated client then fails every Deck/QuizSet/ShareLink/MaterialGroup query (no such column: Deck.groupId,no such table: MaterialGroup— reproduced against a temp DB). Any fresh install/deploy is non-functional. The app only works on the developer's db-push-synced local database. - FIN-02 — Hardcoded session-secret fallback (High-confidence inference).
src/lib/auth.ts:15andsrc/proxy.ts:8-9fall back to the public constant"dev-session-secret-change-in-production-must-be-32-chars"wheneverSESSION_SECRETis unset — and the shippeddocker-compose.ymlpasses${SESSION_SECRET}, which is empty by default. Anyone with the source can forge an iron-session cookie ({isAuthenticated:true, sessionGeneration:1}) and fully bypass authentication on such deployments.
High
- FIN-03 — "Retake Missed" persists as a full attempt with a wrong lower score (in-viewer retake never sets the
retakeIdsprop; server scores all unanswered questions as 0) and pollutes SEQUENTIAL progress. - FIN-04 — Deleting a card mid-session bricks the flashcard study session (
filterAndClampOrderis dead code; resume never filters stale ids; no skip UI). - FIN-05 — Production compose is unreachable: container listens on 3726 (
ENV PORT=3726), compose publishes 3000:3000. - FIN-06 — No
.dockerignore: Windows-host builds inject win32 native modules (better-sqlite3, argon2) into the Linux image. - FIN-07 —
docker-compose.override.ymlauto-merges: plaindocker compose upsilently runs dev mode in production.
Medium (selected)
First-login provisioning takeover + dead ADMIN_PASSWORD_HASH (FIN-08); proxy auth bypass for dot-containing paths (FIN-09); password-reset abuse (FIN-10); unvalidated attempt answersJson (FIN-11); finish double-submit duplicates attempts (FIN-12); progress PATCH/DELETE race (FIN-13); unsequenced progress autosaves (FIN-14); completed session resumes on last card (FIN-15); SRS review endpoint lacks due/new-card-limit enforcement (FIN-16); unbounded DB scans (FIN-17); library optimistic mutations without rollback (FIN-18); unvalidated groupId/sortOrder incl. cross-class group membership (FIN-19); shared-quiz reshuffle on re-render (FIN-20); unvalidated /api/progress + unguarded JSON.parse crashes (FIN-21); entrypoint migrate fragility (FIN-22); non-reproducible installs (FIN-23); no healthcheck/backup (FIN-24); CI ships without tests/lint/prisma/smoke gates (FIN-25).
Low / Observation
25 Low (e.g. SATA div-by-zero NaN, historical score recompute, card-edit endpoints without Zod, no import size caps, sortOrder duplicates after group deletion, keyboard cross-group moves impossible, restart DELETE race, P2002 → 500, git history containing dev.db, .gitignore gaps, dashboard fetch crash, empty slug from slugify, shared-viewer localStorage key collisions) and 3 Observations (group sortOrder desc convention, collapse flash, shared quizzes ship the answer key by design).
Verification highlights
- FIN-01 disproved attempts: checked whether the client avoids selecting
groupId(it doesn't — generatedDeck.tsincludes it in every payload), whether pages swallow errors (they don't), and whether any migration creates the objects (grep = 0 hits). Confirmed via live probes instead: seeaudit-results/tmp/reproduce-drift.cjsoutput in VERIFICATION_LOG.md. - Adversarial review refuted the frontend auditor's "no auth on API routes" claim (proxy is compiled middleware — verified in
.nextartifacts), weakened the entrypoint crash-loop framing (fresh-volumemigrate deploysucceeds), and downgraded the optimistic-reorder severity. It independently confirmed all other Critical/High findings and added 10 missed issues, merged into the final list.
Repository areas NOT fully audited
- Arcade feature internals (out of scope; shared-DB/build/deploy impact only — verified in migrations).
- Real browser interaction (no browser available): drag-drop, resume flows, share pages, keyboard a11y, themes — code-verified; flagged for runtime confirmation in UNVERIFIED_RISKS.md.
- Container build/run (docker CLI unavailable): FIN-06, FIN-22, FIN-24 need a container run.
- Deep git-history diff of every commit; full content audit of the committed
dev.dbblobs (no sqlite3 CLI; no credentials found in the checked blobs). - Runtime performance on a populated database (FIN-17 not measured).
- CI/remote execution (Forgejo pipeline analyzed statically).
Recommended priority (when fixes are authorized)
- FIN-01 — generate and commit the missing migration (
prisma migrate dev --name add_material_groups); add a CI drift gate. Everything else depends on a working fresh deployment. - FIN-02 / FIN-05 / FIN-06 / FIN-07 — harden the deployment path: require
SESSION_SECRET, fix the port mapping, add.dockerignore, rename the dev override, and smoke-test the image in CI. - FIN-03 / FIN-04 — the two most user-visible application bugs (wrong retake scores; stuck flashcard sessions).
- Then the Medium cluster (validation, races, SRS enforcement, progress handling) and finally Low items + TEST_GAPS.md.