Skip to content
Merged
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
14 changes: 14 additions & 0 deletions docs/PROGRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,20 @@ bodies total 37,095 bytes. **This retires the Option C frozen-tail invariant by
existed only to police a boundary inside an unreviewable single line, and the append-only rule above
supersedes it. No bespoke hash is needed for future updates: the diff is the proof.

### 2026-10-07 - #137 PR A applied to production after the merge and proven on the live database: the 25 grant lines flipped exactly, the six policies gone, everything else byte-identical, the definer functions and every refusal witnessed inside a transaction that rolled itself back; `public-grants` now a required check

**Row #137 is not edited: the owner's brief for this step allowed Section 7 additions only. Its text still reads "NOT BUILT", which is now true of PR B and PR C and no longer of PR A; the block below supersedes that sentence until the row is rewritten when #137 closes. Outside this file: nothing in the repository; on production, the one file `20261007_public_grants_dead_writes_and_anon.sql`, run in the SQL editor by the agent on the owner's written instruction of 2026-10-07 (the editor's "Potential issue detected" dialog for the DROP POLICY statements confirmed by the agent under that instruction); and on GitHub, `public-grants` added to the required status checks of `main`. Rows 42, 69 and 81 are not touched, and Section 7 takes no deletions.**

**ORDER AS RECORDED.** PR #236 merged as `7265eef` at 07:21:33Z (final head `816b4bf`; CI green on it, `public-grants` 131 of 131). The live baseline was re-read at ~07:10Z before the merge (previous block). The file was loaded into the editor from the merged commit itself (`raw.githubusercontent.com/.../7265eef/...`, SHA-256 `d616675b9c2fc64f...`, equal to `git show 7265eef:` of the file) and run once at ~07:23:50Z: "Success. No rows returned". Vercel deployed `7265eef` (success; nothing in the deploy applies migrations) and Railway built the ingestion image (success). The `typecheck` workflow's first run on `main` at `7265eef` (run 37586832596) had `public-grants` green, and only then was the protection changed.

**THE LIVE READ AFTER APPLY (~07:25Z, `scripts/sql/read-public-grants.sql`, 13 cells, 149 lines; 159 before: the ten lines of the six dropped policies, two of which spanned three lines).** Compared in the page, line by line, against the fixture read of the green `public-grants` run (the expected side): **two live lines differ, both the platform objects the audit recorded** (`rls_auto_enable()` and the `supabase_admin`/`realtime` default ACL with grant options), **and one expected line is absent, the image's version of that same `realtime` row**. Every other line is identical: `1.relations`, the eleven touched tables now `{postgres=arwdDxtm/postgres,anon=rDxtm/postgres,authenticated=arwdDxtm/postgres,service_role=arwdDxtm/postgres}` for `profiles`, `knowledge_bases`, `documents`, `conversations`, `messages`, `chunks`, `quizzes`, `quiz_items` (the baseline minus exactly `a`, `w`, `d` on `anon`) and `{postgres=arwdDxtm/postgres,anon=rDxtm/postgres,authenticated=rDxtm/postgres,service_role=arwdDxtm/postgres}` for `waitlist`, `usage_counters`, `study_events` (minus `a`, `w`, `d` on both API roles), `subscriptions` and `account_deletion_orphans` unchanged, every owner `postgres`, RLS on, not forced; `2.privs`, the fourteen flipped lines now `SELECT=true INSERT=false UPDATE=false DELETE=false` and the other 25 unchanged (`service_role` all `true` everywhere); `3.policies`, exactly the fourteen repository policies minus `Anyone can join waitlist`: 13 policies, one per table except `chunks` 3, `waitlist` 0, `account_deletion_orphans` 0, each body hash-identical to the baseline's; `4.defacl` the six `public` rows unchanged (default privileges are PR C); `5.functions`, `6.definers`, `7.triggers`, `8.views`, `9.colacl`, `10.sequences`, `11.roles`, `12.schema` unchanged. **Each changed grant flipped exactly as intended and nothing else moved.**

**THE REAL PATHS, ON THE REAL TABLES, AS THE ROLES PostgREST USES, WITH NOTHING LEFT WRITTEN.** One `DO` block in the editor, read-only row counts before and after. Inside it, as `postgres`: a throwaway `auth.users` row (a random id, an `@example.invalid` address) whose `handle_new_user` trigger made its profile (`1`); then `set local role authenticated` with `request.jwt.claims` carrying that id (`auth.uid()` matches: `true`): `increment_usage('query')` → `1`, again → `2`, `increment_usage('upload')` → `1`, `record_study_event('question_asked')` → a uuid; the student's own `usage_counters` row readable (`1`) and own `study_events` row readable (`1`): the definer functions write and the SELECT policies serve, exactly as `src/lib/rate-limit.ts`, `src/lib/study-events.ts`, the dashboard and the streak use them. Then, each in its own sub-block: `INSERT waitlist` → `42501 permission denied for table waitlist`; `INSERT usage_counters` → `42501 permission denied for table usage_counters`; `UPDATE usage_counters` → `42501 ...`; `INSERT study_events` → `42501 permission denied for table study_events`; `DELETE study_events` → `42501 ...`. Then `set local role anon`: `INSERT waitlist` → `42501 permission denied for table waitlist` (the audit's §1.3 hole, closed); `INSERT knowledge_bases`, `UPDATE documents`, `DELETE messages`, `INSERT profiles` → each `42501 permission denied for table ...`; `increment_usage` and `record_study_event` as `anon` → `P0001 not authenticated` (the functions' own guard, as before). The block ends with `raise exception` carrying that text, so the whole transaction, the throwaway account included, rolled back. **Row counts before (07:29:38Z) and after (07:31:24Z): `waitlist` 1, `usage_counters` 16, `study_events` 31, `auth.users` 13, `profiles` 13, policies in `public` 13: identical.** No real account was used; the three protected addresses were not touched.

**`public-grants` REQUIRED.** After the first green run on `main` (`7265eef`), `PATCH /repos/.../branches/main/protection/required_status_checks` with `{"strict": false, "contexts": ["tsc", "db-types", "entitlement-read", "entitlement-rls", "public-grants"]}` (the four existing contexts plus the new one; `strict` kept `false`). Read back: contexts as sent, `enforce_admins` `true`, required reviews `0`, linear history `false`, force pushes and deletions `false`: nothing else in the protection changed. The branch `fix/137-grants-pr-a` was deleted locally and on origin.

**WHAT CLOSES AND WHAT DOES NOT.** PR A of #137 is done: built, merged, applied, read back and proven. Row #137 stays open for PR B (the unused `authenticated` verbs: `knowledge_bases`, `conversations`, `messages`, `quiz_items` UPDATE and DELETE; `chunks`, `quizzes` UPDATE) and PR C (default privileges, `profiles`, EXECUTE from PUBLIC). PR B needs no new audit: its six lines are the only changes to the harness's `INTENDED` matrix, and the harness already exercises every verb `authenticated` keeps, so an over-reach goes red on its first run.

### 2026-10-07 - #137 PR A built: INSERT, UPDATE and DELETE revoked from both API roles on `waitlist`, `usage_counters` and `study_events` and from `anon` on every other table; the open waitlist INSERT policy and the five hand-made duplicates dropped; the grants matrix proven in CI as `public-grants`; the production baseline re-read and the exact rollback recorded before anything is applied

**Row #137 is not edited (still open; PR A is one of its three steps). Outside this file: `supabase/migrations/20261007_public_grants_dead_writes_and_anon.sql` (new, two REVOKEs and six DROP POLICY IF EXISTS), its `apply` line in `supabase/migration-order.txt`, `scripts/verify-public-grants.mjs` (new, the proof), and the job `public-grants` in `.github/workflows/typecheck.yml` (new, its own job, 15 minutes, not yet required). Nothing is applied to production by this PR; the apply is the step after merge, by hand in the SQL editor, by the agent on the owner's written instruction of 2026-10-07. PR B (the unused `authenticated` verbs) and PR C (default privileges, `profiles`, EXECUTE from PUBLIC) are not built. No auth config, template, env, function body or other table. Rows 42, 69 and 81 are not touched, and Section 7 takes no deletions.**
Expand Down
Loading