Mobile responsiveness: several flex rows had a `truncate` child without `min-w-0`, so a long value (member/door name, channel name, published form URL) would not shrink and pushed the row past the viewport on a phone. Added `min-w-0` (with `flex-1`/`shrink-0` where needed) on the dashboard presence + door-self-entry rows, the comms channel list, and the form-builder URL. Door inbound poll: `/api/cron/door-ingest` now polls connections with Promise.allSettled instead of a sequential loop, so one tick is bounded to the slowest single poll rather than the sum of all per-call timeouts; each connection stays isolated and the run is idempotent. Docs: refreshed ARCHITECTURE (door epic marked complete P1-P5), the README access-control feature bullet + project-status pointer, a historical banner on the older AUDIT.md, a door-epic CHANGELOG entry, and the rolling HANDOFF audit log (passes 65-68). No migration; no behavior change for existing users.
27 KiB
Codebase Audit
Historical snapshot. This is a full-codebase audit as of 2026-05-14, before the certifications, classes, equipment, Stripe-dues, notifications, member-portal, and door (cards / connections / inbound ingest / API-call buttons) work landed. For audit findings, deploy state, and the running session log AFTER this date, see HANDOFF.md (newest entry on top) — it carries the rolling per-pass security audits.
Date: 2026-05-13 through 2026-05-14 (three passes)
Scope: every file outside node_modules, .next, and .git.
Method: read each source file, cross-check against the live database type definitions in types/database.ts, the canonical schema in scripts/schema.sql, and the column map in DB_SCHEMA_MAP.md.
This document supersedes the older AUDIT.md, IMPLEMENTATION_STATUS.md, and docs/PRODUCTION_AUDIT.md for findings as of this date. Those files remain as historical context.
- Pass 1 (2026-05-13): fresh-deploy blockers and deployment artifacts.
- Pass 2 (2026-05-13): validation schema enum drift, RLS privilege escalation, dashboard task filter bug, dead duplicate routes, auth function duplication.
- Pass 3 (2026-05-14): architecture refactor. Split monolithic
lib/actions.tsby domain, introducedlib/permissions.tsandlib/auth-helpers.ts, wired validation into every action with a schema, added 76 real tests, deleted remaining dead code, addedscripts/README.md.
All three passes are reflected below.
1. What the system is
A multi-tenant member-management app for hackerspaces. Each tenant is a spaces row; every other table is keyed by space_id. Authentication uses Supabase Auth (email + password, OAuth wiring is stubbed). All data access goes through Supabase with row-level security enforced at the database. The Next.js app uses server components for reads, server actions for writes, and one client-side Realtime subscription for chat.
Stack:
| Layer | Stack |
|---|---|
| Framework | Next.js 16 (App Router), React 19, TypeScript 5.7 |
| Styling | Tailwind v4 (CSS-native config), shadcn/ui, Lucide icons |
| Backend | Next.js server actions, one REST route under /api/paypal/sync, one /api/health route |
| Data | Supabase Postgres + Auth + Realtime |
| Tests | Vitest (unit), Playwright (E2E) |
| Hosting | Vercel, DigitalOcean App Platform, or self-hosted Droplet (Dockerfile + compose ship with the repo) |
2. Repo layout
/
app/
(app)/ Protected routes. Layout enforces auth + membership.
dashboard/ Server component, parallel counts.
tasks/ Server page + client component.
projects/ Kanban-style board.
members/ Admin/board only mutations.
payments/ Treasurer/admin/board only.
comms/ Realtime chat (Supabase channel subscription).
contacts/ Member-writable CRUD.
ops/ Knowledge base, secrets, area leads.
ops/[id]/ KB entry detail + editor.
ops/new/ KB entry create form.
import/ CSV import UI (partial).
settings/ Space + integrations.
(landing)/ Public landing page.
auth/callback/route.ts OAuth code exchange.
api/
health/route.ts Liveness probe (added this audit).
paypal/sync/route.ts Per-space PayPal pull.
login/, signup/ Public auth pages.
layout.tsx, page.tsx Root layout, root redirect.
components/
ui/ shadcn primitives.
app-sidebar.tsx The actual sidebar (role-gated).
app-shell.tsx DEAD: not imported by any page.
task-claim-button.tsx Used by tasks-client.
theme-provider.tsx next-themes wrapper.
hooks/
use-mobile.ts, use-toast.ts
lib/
supabase/
client.ts Browser client.
server.ts Server component client (cookie-bound).
admin.ts Service-role client. Bypasses RLS.
proxy.ts Middleware session refresher.
actions.ts All mutation server actions.
auth-actions.ts Auth-specific server actions.
validations.ts Zod schemas (most are not wired up, see findings).
security.ts Sanitisation, rate limiting, helpers.
utils.ts Tailwind classname merge.
types.ts Re-exports from generated database types.
types/
database.ts Generated Supabase types. Source of truth for column shape.
middleware.ts Auth gate. Public routes list lives here.
scripts/
schema.sql Canonical, idempotent, full-schema deploy.
014_member_user_id_nullable.sql Most recent incremental migration.
patch-dashboard.mjs, fix-actions.js, fix-sidebar.js, fix-sidebar.mjs DEAD scripts.
__tests__/ Vitest specs.
e2e/ Playwright specs.
docs/ Architecture, deployment, database, API, audit.
Dockerfile, docker-compose.yml, .dockerignore Self-host artifacts.
.do/app.yaml DigitalOcean App Platform spec.
.env.example Env var template.
CLAUDE.md Working agreement.
3. Issues fixed in this audit
3.1 space_members.user_id was NOT NULL but the app needs it nullable
The schema declared:
user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE
But addMember() (lib/actions.ts:302) and importMembers() (lib/actions.ts:897) insert rows with no user_id. These are "offline" members added by an admin or CSV-imported, who have not signed up yet. On a fresh Supabase project, both code paths fail with a NOT NULL violation.
Fix applied:
scripts/schema.sql: droppedNOT NULLonspace_members.user_id. Added a comment explaining the choice.scripts/014_member_user_id_nullable.sql: new incremental migration for existing databases.
The (space_id, user_id) unique constraint still works because Postgres treats NULL as distinct, so multiple offline members with user_id IS NULL coexist. The partial unique index on (space_id, email) (WHERE email IS NOT NULL) prevents duplicate offline-member emails.
3.2 Realtime publication statements were not idempotent
The closing two lines of schema.sql were:
ALTER PUBLICATION supabase_realtime ADD TABLE public.comms_messages;
ALTER PUBLICATION supabase_realtime ADD TABLE public.comms_channels;
ALTER PUBLICATION ... ADD TABLE errors if the table is already in the publication, which breaks the "idempotent" promise the file makes in its header. Wrapped both in DO $$ ... EXCEPTION WHEN duplicate_object THEN NULL; END $$ blocks.
3.3 RLS policies were not idempotent
Section 6 of schema.sql does CREATE POLICY ... without IF NOT EXISTS (which Postgres 15 does not support). Re-running the file on a database that already has policies errored with policy ... already exists. Added a block of DROP POLICY IF EXISTS statements before the CREATE POLICY block.
Net result of 3.1, 3.2, 3.3: scripts/schema.sql now runs cleanly on both a fresh Supabase project and an existing one. The header comment claiming idempotency is now true.
3.4 Missing deployment artifacts
The deployment guide instructed users to "create" a Dockerfile, a docker-compose file, and to manually edit next.config.mjs. None of these artifacts existed in the repo. App Platform users had no spec to reference.
Added:
Dockerfile(multi-stage, standalone output, runs as non-root, healthcheck).docker-compose.yml(service definition with healthcheck, env_file)..dockerignore(excludes git, node_modules, dev artifacts, secrets)..do/app.yaml(App Platform spec with health check path)..env.example(every variable the app reads, with comments).app/api/health/route.ts(liveness probe used by Docker, App Platform, and operators).- Updated
middleware.tsto include/api/healthin the public routes list so the healthcheck does not redirect to/login. - Updated
docs/DEPLOYMENT.mdto reference the bundled artifacts instead of asking users to write their own. Also removed the incorrect note about manually editingnext.config.mjs(the file already keysoutput: 'standalone'offDOCKER_BUILD=1, which the Dockerfile sets).
A fresh clone is now: pnpm install, copy .env.example to .env.local, run scripts/schema.sql in Supabase, pnpm dev.
3.5 Dashboard "Quick Chores" filter was wrong
app/(app)/dashboard/page.tsx:49 filtered tasks with .neq('status', 'done') to find open work. The actual task_status enum has both done (legacy) and completed, and completeTask() writes 'completed'. Completed tasks therefore leaked into the dashboard's open-tasks list. Replaced with a positive filter on the open statuses: open, claimed, in_progress, overdue, due_today, blocked.
3.6 Zod validation schemas had wrong enum values
lib/validations.ts declared enums that disagreed with the database in six places. The schemas were mostly unused (only createTask and taskId were wired), so the bugs were latent. Both fixed:
createTaskSchema.type: was free-form string, nowenum(['task','chore']).createTaskSchema.recurrence: missingbiweekly. Added.updateProjectStatusSchema.status: hadplanning, active, paused, completed, cancelled. Replaced with the realbacklog, in_progress, review, done, blocked.addMemberSchema.roleandupdateMemberSchema.role: missingassociate. Added.createContactSchema.contact_type: hadvendor, sponsor, partner, city, media, other. Replaced with the realvendor, supplier, partner, landlord, city.createKbEntrySchema.visibility: hadpublic, members, board, admin. Replaced with the realall_members, board, admin_only.
Also tightened: logCashPaymentSchema (removed unused from_identifier, added member_id), updateSpaceSettingsSchema (added slug), upsertAreaLeadSchema (matched the action's parameter shape).
3.7 Privilege escalation on space_members self-update
The members_update RLS policy let a member update their own row including role, tier, status, approved, and has_card_access. An authenticated member could PATCH their own membership via PostgREST and become an admin.
Fixed with a BEFORE UPDATE trigger (prevent_member_self_role_change) that rejects any change to protected columns when the row belongs to the current user and the current user is not already admin/board/treasurer in that space. Service-role calls (no auth.uid()) and privileged self-edits are unaffected. Applied to schema.sql and shipped as incremental migration scripts/015_prevent_member_self_role_change.sql.
3.8 Validation wired into the highest-risk actions
The Zod schemas were imported by lib/actions.ts but never actually invoked except by createTask and claimTask. Wired the now-correct schemas into:
createTask(already)claimTask(already)createProjectupdateProjectStatusaddMemberlogCashPaymentlinkPaymentToMember
Remaining unwired actions are noted in section 4.
3.9 Duplicate auth functions in lib/actions.ts
Lines 30 to 47 of lib/actions.ts redeclared signIn, signOut, and getUser, each a slightly worse copy of the version in lib/auth-actions.ts. No call site imported them from actions.ts. Removed.
3.10 Dead one-off scripts removed
Deleted scripts/fix-actions.js, scripts/fix-sidebar.js, scripts/fix-sidebar.mjs, and scripts/patch-dashboard.mjs. All four had hardcoded /vercel/share/v0-project paths and were one-time patches that have long since landed in the tree.
3.11 types/database.ts did not match the schema for user_id
After making space_members.user_id nullable in scripts/schema.sql, the generated types were stale. Updated Row.user_id to string | null and Insert.user_id / Update.user_id to optional nullable. Regenerate properly with supabase gen types typescript on next schema sync.
3.12 Signup page copy referenced channels that are not created
The "Create Space" panel told users "Default channels (general, announcements, random) will be created automatically." The schema's create_default_channels trigger creates general, announcements, ops. Updated the copy to match.
3.13 Monolithic lib/actions.ts split by domain
lib/actions.ts was 914 lines and held 29 exported server actions for every entity in the system. Maintenance friction was high. The duplication was severe: 25+ copies of "get user, then look up active member, then check role" and "insert activity_log row" boilerplate.
Refactored into lib/actions/ containing:
tasks.ts,projects.ts,members.ts,contacts.ts,payments.ts,knowledge-base.ts,secrets.ts,area-leads.ts,settings.ts,imports.ts— one file per entity, each carrying'use server'.index.ts— barrel that re-exports every action so the public import path@/lib/actionsremains stable.
Every client component that imports from @/lib/actions was verified to keep working. The barrel is plain re-exports; Next.js preserves the 'use server' marking from the source files.
3.14 Shared helpers in lib/auth-helpers.ts and lib/permissions.ts
Two new modules absorb the repeated boilerplate.
lib/permissions.ts:
ROLES,ADMIN_ROLES,TREASURER_ROLES,ALL_ROLES,ACTIVE_STATUSESas const tuples.RoleandMemberStatustype aliases.hasRole(role, allowed)helper.
lib/auth-helpers.ts:
getAuthMember(supabase): returns the active member or null.requireMember(supabase): returns{ ok: true, member } | { ok: false, error }.requireMemberWithRole(supabase, allowed, label?): same plus role gate.parseInput(schema, input): Zod parse with a discriminated result.logActivity(supabase, member, action, entityType, entityId, details?): fire-and-forget activity log insert.
The result types use the canonical ok: boolean discriminator so TypeScript narrows reliably under strict: true.
3.15 Validation wired into every action that has a schema
After pass 2, only 7 actions called their schemas. Pass 3 wired the remaining 11:
updateMember,approveMember,removeMembercreateContact,updateContact,deleteContactcreateKbEntry,updateKbEntry,deleteKbEntrycreateSecret,deleteSecretupsertAreaLeadupdateSpaceSettings,saveIntegrationdisconnectIntegration(inline string check, no Zod schema needed)importMembers,importPaymentsCsv(array-level guard + filter)
Bad input now returns a clean error message instead of a database-level failure.
3.16 Real tests, not assertion theatre
The existing __tests__/actions.test.ts and __tests__/utils.test.ts were inline value assertions that never imported project code. Kept for compatibility, but four new files exercise the real units:
| File | What it covers |
|---|---|
__tests__/validations.test.ts |
Every Zod schema, asserting real DB enum values pass and legacy ones fail. 37 tests. |
__tests__/permissions.test.ts |
Role constants, ACTIVE_STATUSES, hasRole. 10 tests. |
__tests__/security.test.ts |
sanitizeString, escapeHtml, stripHtml, sanitizeEmail, sanitizeUrl, sanitizeSlug, hasSqlInjectionPatterns, isValidUuid, truncate, checkRateLimit, validateContentLength. 25 tests. |
__tests__/auth-helpers.test.ts |
parseInput parse/error path. 4 tests. |
Total: 202 tests across 7 files, all green.
3.17 Vitest config fixed
vitest.setup.tshad JSX inside a.tsfile (esbuild refused: "Expected '>' but found '{'"). Replaced thenext/imagemock withReact.createElement.vitest.config.tswas picking up Playwright e2e specs and trying to run them. Added explicitincludeandexcludeglobs.
3.18 Final dead-code sweep
Deleted:
app/auth/login/page.tsx(dead duplicate login at/auth/loginlinking to non-existent/auth/onboarding).components/app-shell.tsx(no importer; hardcoded badge counts; no role gating).
3.19 Shared types consolidated in lib/types.ts
lib/types.ts already re-exported the generated row and enum types. Added:
MemberSummary: common projection used by sidebars and props.ActionResult<T>: standard server-action return shape.- Re-exports of
Role,ROLES,ADMIN_ROLES,TREASURER_ROLES,ALL_ROLES,ACTIVE_STATUSES,hasRolefromlib/permissions.ts. - Re-exports of
Member,Result,MemberResult,ServerSupabasefromlib/auth-helpers.ts.
One-stop import for any consumer of typed entities.
3.20 Migration conventions documented
Added scripts/README.md covering: canonical vs incremental files, naming, the idempotency rules (IF NOT EXISTS, DO ... EXCEPTION, DROP POLICY IF EXISTS, etc), the security baseline for new tables, and verification queries for fresh deploys.
4. Open issues (not fixed in this pass)
These are documented for the next person, not silently shipped. Each one has a concrete location and a concrete fix.
4.1 (was open: dead scripts) RESOLVED in pass 2. See 3.10.
4.2 (was open: dead components/app-shell.tsx) RESOLVED in pass 3. See 3.18.
4.3 (was open: dead app/auth/login) RESOLVED in pass 3. See 3.18.
4.4 (was open: validation not wired everywhere) RESOLVED in pass 3. See 3.15.
4.4a Supabase TypeScript generic drift
@supabase/ssr and @supabase/supabase-js emit SupabaseClient with 3 generics. Our types/database.ts was generated against an older signature. The result: 38 lib/actions/* and 103 app/(app)/* TypeScript errors of the form Argument of type '{...}' is not assignable to parameter of type 'never'.
These errors already existed in the pre-refactor codebase. They are masked by next.config.mjs: typescript.ignoreBuildErrors: true. The runtime is unaffected: Supabase's PostgREST client accepts the values fine.
Fix: regenerate types/database.ts with the latest Supabase CLI (supabase gen types typescript --project-id <ref>). Then drop typescript.ignoreBuildErrors.
4.5 signUp metadata is not consumed by the auth trigger
lib/auth-actions.ts:signUp passes metadata keys full_name, action, space_name, space_slug, city, invite_code. The handle_space_signup trigger in schema.sql reads space_id, role, display_name. Different keys, so the trigger short-circuits and does nothing. That is actually fine because createSpace and joinSpace insert the membership themselves using the admin client. But the trigger is then dead code unless someone passes space_id in data. Either:
- Document the trigger as a future-use safety net and add a code comment, or
- Remove the trigger and stop pretending it does anything.
4.6 Several documented features are UI-only or unfinished
| Feature | Status | Location |
|---|---|---|
| GitHub / Google OAuth | Client wired (signInWithOAuth) but needs provider config in Supabase Auth |
app/login/page.tsx:31 |
| Webhook endpoint | URL displayed in settings, no handler | (none) |
| Zeffy, Venmo, Stripe sync | Config storage only, no API code | lib/actions.ts:saveIntegration |
| CSV import | UI uploads a file but does not process it | app/(app)/import/import-client.tsx |
| Email notifications | None | (none) |
| Task recurrence auto-rollover | Cron job not implemented | (none) |
| Secrets encryption | Plain text in DB | secrets.value |
None of these are runtime errors. They are missing-feature stubs. They are listed here so deployers know what is and is not real. Pass 1's audit incorrectly listed OAuth as "UI only, no handlers"; the client-side OAuth call is actually wired. What is missing is the provider configuration in the Supabase dashboard.
4.7 package.json name is "my-project"
Cosmetic. Should be "hackerspace-management". Not a deploy blocker.
4.8 tsconfig.json has "strict": true but next.config.mjs has typescript.ignoreBuildErrors: true
The build does not fail on type errors. This is a v0 default that should be removed once the codebase passes pnpm exec tsc --noEmit cleanly. The pass-2 schema and types edits move the needle. Remaining issues are minor.
4.9 Default channels: 3 vs 4
schema.sql create_default_channels inserts 3 channels: general, announcements, ops. Older docs (DB_SCHEMA_MAP.md, docs/ARCHITECTURE.md) say 4 channels (general, announcements, random, facilities). The schema is current; the older docs are stale. The signup page copy was fixed in pass 2. Update DB_SCHEMA_MAP.md and docs/ARCHITECTURE.md on next docs pass.
4.10 (was open: members RLS privilege escalation) RESOLVED in pass 2. See 3.7.
5. Database column map versus code use
I cross-checked every column accessed in lib/actions.ts, lib/auth-actions.ts, app/api/paypal/sync/route.ts, and the client components named in app/(app)/*. Findings:
space_members.last_paid_at: present in schema, used bylinkPaymentToMemberandimportMembers. OK.space_members.last_payment_at: legacy duplicate. Schema keeps both. Code reads either with a fallback (members-client.tsx).knowledge_base.is_pinnedvspinned: schema has both, code usesis_pinned. OK.secrets.titlevslabel: schema has both, code writes both fields (createSecretatlib/actions.ts:701). OK.payments.statusvslink_status: schema has both, code useslink_status. OK.integrations.configvscredentials/settings: schema has all three for legacy compatibility, code usesconfig. OK.tasks.task_type: column matches enum name. Server actions writetask_type, nevertype. OK.member_statusfilter in app uses('current', 'unverified', 'late'). Schema enum matches. OK.
No drift between active code paths and the schema beyond what is in section 4.
6. Security posture
| Control | State |
|---|---|
| RLS on every table | Yes |
| Service role key isolated to server | Yes, but lib/actions.ts:30-47 and other call sites should be reviewed periodically |
| Input validation | Partial. createSpace sanitises. createTask and claimTask use Zod. The rest do not |
| Rate limiting | Present in lib/security.ts as in-memory map. Wired into signIn and createSpace. Not persistent. For multi-instance deploys, switch to Redis (Upstash) |
| Secrets encryption at rest | None. secrets.value and integrations.config are plain text. Encrypt before merging to production |
| CSRF / Origin check | Implicit through Next server actions + same-origin cookies. No explicit Origin check |
| Audit log | activity_log table is populated by most write actions |
Privilege escalation on space_members update |
Possible per 4.11, fix before production |
7. Test posture
__tests__/utils.test.ts: assertion-only tests of helpers, no real imports of project code. Useful as smoke tests, not as coverage.__tests__/actions.test.ts: mocked Supabase client, tests the call shape of server actions. Does not exercise RLS, does not hit a database.__tests__/components.test.tsx: not present, but listed inTESTING.md. The doc lies.e2e/auth.spec.tsande2e/critical-flows.spec.ts: present, Playwright. Not verified to pass.
Recommended next steps: stand up a Supabase local instance for integration tests, add a CI job that runs pnpm test and pnpm test:e2e, fix TESTING.md to reflect reality.
8. Performance notes
app/(app)/layout.tsxmakes three sequential Supabase calls (member, taskCount, paymentCount). The latter two can run inPromise.all.app/(app)/dashboard/page.tsxruns multiple queries; check whether they parallelise. (Not audited line-by-line, see file.)- Realtime: only
comms_messagesandcomms_channelsare in the publication. Good. - Indexes: 22 indexes are defined in
schema.sql, covering every foreign key and common filter (tasks.status,payments.transaction_date,comms_messages.channel_id). Good.
9. Deployment readiness summary
| Target | Ready? | Notes |
|---|---|---|
| Local dev | Yes | pnpm install && pnpm dev, with .env.local filled |
| Vercel | Yes | Push to GitHub, connect, set env vars |
| DigitalOcean App Platform | Yes | Use .do/app.yaml or the UI flow |
| DigitalOcean Droplet (Docker) | Yes | Dockerfile and compose ship with the repo |
| Database fresh deploy | Yes | One file: scripts/schema.sql |
| Database upgrade from existing | Yes | Run new incremental migrations in scripts/ only |
A fresh deploy will succeed. The open issues in section 4 are quality and feature gaps, not deploy blockers.
10. What changed across the three passes
Pass 1 (deployment hardening)
Edited:
scripts/schema.sql: user_id nullable, realtime idempotent, RLS policies idempotent.middleware.ts:/api/healthadded to public routes.docs/DEPLOYMENT.md: env section, App Platform section, Droplet section (no longer asks user to write the Dockerfile/compose), post-deploy checklist (health probe), upgrading section.
Added:
CLAUDE.mdDockerfile,.dockerignore,docker-compose.yml.do/app.yaml.env.exampleapp/api/health/route.tsscripts/014_member_user_id_nullable.sqldocs/AUDIT.md(this file)docs/HANDOFF.mdREADME.mdrewritten
Pass 2 (application repairs)
Edited:
app/(app)/dashboard/page.tsx: open-task filter now lists open statuses positively instead ofneq('status','done')(was leakingcompletedtasks).app/signup/page.tsx: default channel list in copy now readsgeneral, announcements, opsto match the trigger.lib/validations.ts: aligned enums for tasks, projects, members, contacts, KB; tightened settings, area-lead, and cash-payment schemas.lib/actions.ts: removed duplicatesignIn,signOut,getUser(kept inauth-actions.ts); wiredcreateProject,updateProjectStatus,addMember,logCashPayment,linkPaymentToMemberto their Zod schemas.scripts/schema.sql: addedprevent_member_self_role_changetrigger to stop privilege escalation on self-update.types/database.ts:space_members.user_idnowstring | nullto match the schema.
Added:
scripts/015_prevent_member_self_role_change.sql
Removed:
scripts/fix-actions.js,scripts/fix-sidebar.js,scripts/fix-sidebar.mjs,scripts/patch-dashboard.mjs
No business rules, no UI styling, no test code were changed. Pass 2 edits are bug fixes, security hardening, schema alignment, and dead-code removal.
Pass 3 (architecture refactor + real tests)
Added:
lib/permissions.ts(role constants,hasRole)lib/auth-helpers.ts(requireMember,requireMemberWithRole,parseInput,logActivity)lib/actions/directory with 11 files (one per domain, plusindex.tsbarrel)__tests__/validations.test.ts(37 tests)__tests__/permissions.test.ts(10 tests)__tests__/security.test.ts(25 tests)__tests__/auth-helpers.test.ts(4 tests)scripts/README.md
Edited:
lib/types.ts(now re-exports helpers + permissions for one-stop import)vitest.config.ts(proper include/exclude globs)vitest.setup.ts(replaced inline JSX in a .ts file withReact.createElement)
Removed:
lib/actions.ts(replaced bylib/actions/directory; barrel preserves the public import path)app/auth/login/page.tsx(dead duplicate)components/app-shell.tsx(unused, hardcoded badges)
Test suite: 202 tests, 7 files, all passing.
Pass 3 changes UI nothing, schema nothing, business rules nothing. They are pure organisation, test coverage, and dead-code removal.