security: WS1 Gate A - CourtListener off, MFA, invitaciones (W1.3, W1.10, W1.11) (#2)
* security: remove deliberate false-citation instruction from system prompt (W1.1)
* security: remove raw LLM stream logging entirely (console and file recorders) (W1.2)
* security: disable CourtListener product-wide (W1.3)
Remove US case-law from backend (router, client, chat tools, prompt
reminders, api-key provider), frontend (CaseLawPanel, events, endpoints),
Word add-in, and database configuration (tables + provider check +
legal_research_us flag). Document-citation verification (verifyCitations.ts)
and governed BYOK routing are preserved unchanged.
Verification: backend 513 pass / 15 skip, frontend 242 pass, tsc PASS,
lint 0 errors, gitleaks no leaks, zero functional CourtListener references.
Reviewed by 3 parallel Hermes subagents (security/secrets, contracts,
overreach); findings applied (env.example, README, legal_research_us).
* security: enforce MFA enrollment and invitation-only signup (W1.10, W1.11)
W1.10: mfa_on_login defaults to true for new profiles (migration
20260813_02); verify-mfa page now enrolls a TOTP factor when none is
verified (enroll -> QR -> challengeAndVerify), reusing the existing
MFA gate components.
W1.11: signup is invitation-only by default (GOTRUE_DISABLE_SIGNUP=true
in docker-compose + env examples); new POST /user/invite (requireAuth,
GoTrue inviteUserByEmail, responds only {ok:true}, never returns tokens);
new /accept-invite page + InvitePasswordSetup component (verifyOtp with
token_hash/type=invite, then sets password); login/signup pages detect
the invite flow.
Verification: backend build PASS, 513 pass / 15 skip; frontend tsc PASS,
242 pass; lint 0 errors; git diff --check clean. Reviewed by parallel
Hermes subagents: security PASS 5/5, contracts confirmed against
supabase-js types, scope in-scope (env-local package.json noise reverted).
* feat(ws1): tenancy foundations - organizations, workspaces, matters with RLS (W1.5)
Six new tables (organizations, organization_memberships, workspaces,
workspace_memberships, matters, matter_memberships) with role checks
(org_owner, workspace_admin, matter_owner, editor, viewer,
technical_operator) and row-level security verified against PostgreSQL 16:
- SECURITY DEFINER helpers (organization_role, is_organization_member,
is_workspace_admin, matter_role) avoid the infinite-recursion Postgres
rejects when policies subquery their own table.
- 15 policies enforce member visibility and owner/admin writes; anon gets
zero access; authenticated is filtered by RLS.
- schema.sql updated (fresh-DB init) alongside the incremental migration.
RLS matrix verified live: member sees own org/matter, viewer insert is
rejected by policy, outsider sees nothing, org_owner can manage members.
* feat(ws1): RLS on every public table + CI security invariants (W1.6)
- Migration 20260813_04 enables row-level security on the 16 OSS tables
that lacked it (plus workflow_open_source_submissions, contact_messages)
with to_regclass guards; schema.sql updated for fresh databases.
- New scripts/check-security-invariants.sh enforces in CI: every public
table defined in schema.sql/migrations has RLS enabled (documented
courtlistener_* exception, dropped in W1.3), and no ordinary router uses
the service_role path (createServerSupabase) without requireAuth.
Verification (PostgreSQL 16 ephemeral): schema + migrations apply clean;
0 tables without relrowsecurity; invariants script PASS (34/34 tables);
backend build PASS, suite 513/15.
* fix(ws1): remove anon execute grants on tenancy RLS helpers (W1.5 review)
Independent security review found the four SECURITY DEFINER helpers
(organization_role, is_organization_member, is_workspace_admin,
matter_role) granted EXECUTE to anon; the table-level REVOKEs do not cover
function privileges. No data leak today (helpers filter by auth.uid(), null
for anon), but the surface is unnecessary. Executes now go to
authenticated only; verified in PostgreSQL: anon grants = 0, schema applies
clean, RLS matrix still passes.
* feat(ws1): monotonic authorization epochs for revocation (W1.7)
- organizations.authorization_epoch bigint default 0 (schema.sql +
migration 20260813_05) with atomic bump via RPC
bump_authorization_epoch(org) (security definer, service_role only).
- backend/src/lib/tenancy.ts: revokeOrganizationMembership() deletes the
membership, bumps the epoch and signs the user out of Auth; and
assertEpochFresh() for long-running jobs - aborts when the epoch moved
(a revocation happened mid-job).
- TDD: 4 unit tests (mock persistence); verified in PostgreSQL 16:
schema + migration apply clean, epoch bumps 0->1->2 monotonically.
- Backend suite 517 passed / 15 skipped.
* fix(ws1): revoke PUBLIC/anon/authenticated execute on bump_authorization_epoch (W1.7 review)
Independent security review found the RPC inherited the PostgreSQL default
EXECUTE for PUBLIC; granting service_role did not make it exclusive, so any
role could bump any organization's epoch (invalidating other jobs/revokes).
Migration and schema now revoke execute from public, anon, authenticated
before granting service_role. Verified in PostgreSQL 16: leaked grants = 0,
schema applies clean.
* feat(ws1): remove email-based sharing without acceptance (W1.8)
shared_with (projects, tabular_reviews), workflow_shares and the People
share UI are removed; access is owner-only until tenancy memberships
(W1.5) and the nominal invitation flow (W1.11) take over collaboration.
- Migration 20260813_06 drops projects.shared_with + GIN index,
tabular_reviews.shared_with and workflow_shares (verified on PG16:
residuals 0|0|0|0).
- schema.sql updated (columns, index, workflow_shares table, and the
get_*_overview RPCs lose their shared branches).
- Backend: access.ts, projects/tabular/workflows/downloads/documents/
chat/projectChat routes, userDataExport/Cleanup, contextBuilders -
owner-only access, share endpoints removed (net -417 lines).
- Frontend: PeopleModal removed, sharedWith types/API calls gone,
InvitePasswordSetup effect reworked (lint fix: no sync setState in
effect).
- Tests updated: backend 510 pass/15 skip, frontend 237 pass, tsc PASS,
lint 0 errors.
* feat(ws1): authenticated downloads + upload content quarantine (W1.9)
Downloads were already authenticated server-side (GET /download/:token with
requireAuth + ensureDocAccess + S3 fetch, no signed URLs anywhere). This
commit closes the upload gap:
- New backend/src/lib/fileValidation.ts: detectFileKind() reads magic bytes
(PDF %PDF-, OOXML PK\x03\x04, OLE2 D0CF11E0) and validateUploadContent()
rejects files whose real content does not match the declared extension
(blocks executables disguised as .pdf/.docx).
- documents.ts upload: 415 when content mismatches, before any row or
object is created.
- Partial cleanup: on processing failure the uploaded object and its PDF
rendition are deleted (deleteFile) so failed/revoked uploads never linger.
- TDD: 11 new tests (real/fake/executable cases). Backend suite 521/15.
* test(ws1): tenancy RLS authorization matrix integration test (W1.12)
New gated integration test (SUPABASE_TEST_URL / SERVICE_ROLE / ANON) that
proves the full authorization matrix against real RLS on a Supabase stack:
viewer sees own org+matter, viewer insert is rejected by policy, outsider
sees nothing, org_owner manages members. Skips cleanly without the stack.
The empirical matrix was already verified against PostgreSQL 16
(hermes-verify-w15.sh: member_sees_org, member_sees_matter,
outsider_sees_nothing, owner_manages, viewer_insert_blocked - 5/5) and the
owner-only endpoint matrix is covered by access.test.ts (W1.8).
* feat(ws1): insert-only audit trail with minimal events (W1.13)
- Migration 20260813_07: audit_events table (identity pk, actor, org,
event_type, detail jsonb) + BEFORE UPDATE OR DELETE trigger that raises
for every role (service_role included); browser roles get nothing,
service_role gets insert+select. Verified on PostgreSQL 16: INSERT ok,
UPDATE blocked, DELETE blocked, row preserved.
- backend/src/lib/audit.ts: recordAuditEvent() - single write path, never
breaks the underlying operation.
- Events wired: user.invited (POST /user/invite), membership.revoked
(tenancy revocation), document.uploaded (upload 201), document.deleted
(DELETE /documents/:id), document.downloaded (GET /download/:token).
- schema.sql mirrors the table + trigger for fresh databases.
- Backend suite 521 passed / 16 skipped.
* feat(ws1): daily encrypted audit export job (W1.14)
- lib/auditExport.ts: AES-256-GCM JSONLines with deterministic daily key
rotation (HMAC(master, date)) so yesterday's export stays decryptable
without a KMS; expiredExportKeys() enforces the 30-day retention window.
- scripts/export-audit-daily.ts (npm run export:audit): reads yesterday's
audit_events (UTC window), encrypts, writes audit-exports/daily/
<date>.jsonl.enc with an AUDIT1 magic version marker, and deletes
exports older than 30 days. Cron: 0 3 * * * (documented in header).
- storage.ts: uploadFileToBucket/deleteFileFromBucket for the export
bucket (scoped, never touches the documents bucket).
- TDD: 6 tests (key derivation/rotation, encrypt-decrypt round-trip,
wrong-day key rejection, retention boundaries). Backend suite 527/16.
- Format round-trip verified (magic + iv + ciphertext).
* fix(ws1): audit never breaks the operation + drop unused event types (W1.13 review)
Independent security review flagged: recordAuditEvent only handled {error}
responses - a thrown exception would propagate and break the underlying
operation; and AuditEventType declared 9 events while only 5 are wired.
Now: full try/catch (log and continue on any failure) and the type union
matches the 5 live events (user.invited, membership.revoked,
document.uploaded/deleted/downloaded).
* fix(ws1): audit-export retention scoped to the export bucket (W1.14 review)
Independent security review found the purge used listFiles(), which
enumerates the documents bucket (R2_BUCKET_NAME), not AUDIT_EXPORT_BUCKET:
expired exports were never listed, and keys matching daily/*.jsonl.enc in
the wrong bucket could be deleted. New listFilesFromBucket(bucket, prefix)
mirrors listFiles with an explicit bucket; the job now lists and deletes
only inside the export bucket.
* fix(ws1): CI portability + re-create overview RPCs in migration 06 (W1.8 gate)
Two CI failures caught before merge:
1. check-security-invariants.sh used rg, which is not installed on GitHub
runners -> portable grep -ohE/-qE (verified: 35/35 RLS, isolation OK).
2. Migration 06 dropped shared_with but only schema.sql re-created the
get_*_overview RPCs; migrated databases kept the old definitions
referencing the dropped column -> PostgREST PGRST202. The migration now
re-creates all four overview RPC bodies (verified on PostgreSQL 16:
shared refs = 0 and get_projects_overview executes).
* chore: rebrand Mike -> LiTT (visible branding only)
User-visible strings (layout metadata, MFA factor friendly names,
placeholders, error messages), package names (litt / litt-backend) and
README (title + fork note + upstream attribution) now say LiTT. Internal
identifiers (MikeApiError, mikeApi.ts, repo/remote names) stay unchanged
to keep the diff narrow; AGPL-3.0 notices and upstream attribution are
preserved per the license (see docs/03-analisis-legal-agpl.md of
legal-ai-mx).
* fix(ws1): align tabular overview RPC callers with the shared-free signature (W1.8 gate)
W1.8 dropped p_user_email from get_tabular_reviews_overview (both the
paginated and simple variants) in schema.sql, but the gated stack test
still passed it -> PGRST202 'no function matches' on the Supabase stack
job (and the truncated RPC copy in migration 06 compounded it). Fixes:
- tabularPagination.supabase.test.ts: remove p_user_email from all rpc()
calls (10 sites) and the now-unused ownerEmail variable.
- Migration 06: replace the truncated paginated RPC copy (cut at an inner
$$;) with the full 134-line body from schema.sql.
Verified on PostgreSQL 16 (CI scenario, schema.sql only): the paginated
RPC executes with the test's exact signature and returns 0 rows without
error; build PASS; suite 527/16. | Repository | IvanJS17/mike |
|---|---|
| Author | Ivan Juarez Soria <63679095+IvanJS17@users.noreply.github.com> |
| Authored | |
| Parents | 7cbf9db5 |
| Stats | 123 files changed , +2790 , -7314 |
| Part of | WS1 security-hardening program (Gate A) |
Capture this commit into my fork
Download a Markdown prompt that tells Claude how to port this
exact commit into your working tree. Run it via
claude -p < capture-commit-79c9a92b.md
from inside the repo you want the change in.