diff --git a/plans/2026-10-01-next-15.md b/plans/2026-10-01-next-15.md new file mode 100644 index 000000000..ed178b4b8 --- /dev/null +++ b/plans/2026-10-01-next-15.md @@ -0,0 +1,144 @@ +# Next 15: DebateLab work after Current Debate + +**Status:** Plan, partly shipped. Written 2026-10-01 against `main` at `fe6100a`. On 2026-10-02 (`main` at `791ccb0`), these items had shipped: 3 (account deletion and export, #83), 5 (pgTAP coverage, #80), 6 (backend drift check, #79), 8 (staging switcher, #81) and 13 (first-run debate, #82). Recheck paths and assumptions before executing any remaining item. + +Open follow-ups from those PRs: +- #80's tests found that anyone can read global prompts and that signed-out visitors can read the global rate limits. They also found that the `anon` and `authenticated` roles can `TRUNCATE` six tables, and that three older tests check privileges too loosely. +- #83 leaves one race open. A Checkout started in another tab while an account is being deleted can leave a subscription with no account behind it. The fix is a backstop in `stripe-webhook`. +- Staging has no Stripe secrets, so its `stripe-webhook` cannot start. + +Current Debate (#72), debate-aware AI (#75), Sentry releases (#76) and the pin-state fixes (#77) have shipped. This list covers what comes next. Items are ordered by risk: security and data protection first, then operational safety, then code health, then product polish. Each item is sized to one PR unless it says otherwise. + +## Security and data protection + +### 1. Confirm the leaked provider keys were rotated + +OpenAI, Anthropic and Jina keys were committed to git history at the start of the project. History was scrubbed on 2026-08-20, but scrubbing does not revoke a key. At that time the Supabase secrets still showed `updated_at` 2026-01-29, the day the project was created. + +- Adam: rotate the keys in each provider dashboard, or confirm they were already rotated. Confirm the Google key too. +- Then set the new values on production and staging with `supabase secrets set`. This needs Adam's authorization. +- Done when `supabase secrets list` shows a fresh `updated_at` for every provider key on both projects. Record only the date, never the value. + +If every model call now goes through OpenRouter, delete the unused direct-provider secrets instead of rotating them. + +### 2. Limit promo-code guessing + +`supabase/functions/redeem-promo/index.ts` has no attempt counter. `public.redeem_promo_code_atomic(text)` is granted to `authenticated`, so any signed-in user, including a demo-code user, can call it directly through PostgREST and skip the Edge Function. The limit therefore has to live in SQL. + +- Add a per-user failed-attempt window inside `redeem_promo_code_atomic`, modelled on `demo_access_attempts`. +- Return the same error for an unknown code and for a locked-out user. +- Prove it with a pgTAP test: N failures lock the user, success resets the count, and another user is unaffected. + +### 3. Account deletion and data export (shipped: #83) + +There is no `auth.admin.deleteUser` path and no export. Settings only clears local session data. Most users are school students in the EU, so they need both. + +- Add one Edge Function with two actions: export everything the user owns as JSON, and delete the account. Deletion cascades through debates, sources, source files in storage, results and usage rows, then deletes the auth user. +- Cancel any Stripe subscription before deleting, or refuse to delete while one is active. Pick one and document it. +- Settings gets a "Download my data" button and a "Delete account" button that asks the user to type a confirmation. +- Prove it with a pgTAP or full-stack test: after deletion, no row in any public table references the user id. Also prove one user cannot export or delete another. +- Update `src/pages/PrivacyPage.tsx` if it promises anything different. + +### 4. Tighten the Content Security Policy + +`vercel.json` `connect-src` still allows `api.openai.com`, `api.anthropic.com`, `generativelanguage.googleapis.com`, `r.jina.ai`, `s.jina.ai` and the wildcard `*.supabase.co`. The browser has not called any provider directly since AI moved server-side. + +- Remove the provider hosts. Remove `*.supabase.co` once staging and preview origins are listed explicitly; keep the production project host. +- Check Google Docs export (`oauth2.googleapis.com`, `www.googleapis.com`) before removing those. +- Add `form-action 'self'`. Drop `X-Frame-Options: SAMEORIGIN`, which contradicts `frame-ancestors 'none'`. +- Prove it with the Chromium E2E suite plus a manual prod-preview check that the browser console shows no CSP violations on every route. + +### 5. pgTAP coverage for untested tables (shipped: #80) + +These tables have RLS enabled but nothing in `supabase/tests/` asserts their policies: `shared_items`, `user_roles`, `admin_grants`, `api_keys`, `source_files`, `rate_limits`, `global_prompts`, `webhook_events`, `user_data_clear_markers`. + +- Start with `user_roles` (a user must not be able to grant themselves admin) and `shared_items` (public read only through the share code, owner-only write). +- For the deny-all tables, assert that `authenticated` and `anon` see zero rows and cannot write. +- Run through `./.superset/db-gate.sh`. + +## Operational safety + +### 6. A read-only backend drift check (shipped: #79) + +Vercel ships the frontend on merge, while migrations and Edge Functions are deployed by hand. That gap has already caused drift: on 2026-09-30 production lacked two migrations and ran pre-#75 functions. Nothing in the repo detects it. + +- Add `npm run backend:status`. It runs `supabase migration list --linked` and `supabase functions list`, compares them with `supabase/migrations/` and the function folders on the current commit, and prints what is pending. It must be read-only. +- Optionally add a step to `prod-canary.yml` that fails when production is missing a migration that `main` has. That check would need a read-only token, which is Adam's call. +- Document it in `docs/operations.md` as the first step of every backend deploy. + +### 7. Alerts that reach Adam + +The production canary runs daily, but a failure only shows in GitHub. Edge Functions log only to `console`, so backend errors never reach Sentry. + +- Route `prod-canary.yml` failures to a channel Adam reads. +- Add Sentry for the Deno Edge Functions through a small shared wrapper in `_shared/`, with the same scrubbing rules as `src/lib/observability.ts`. Start with `ai`, `ai-stream` and `stripe-webhook`. +- Alert on repeated `stripe-webhook` failures, since a missed webhook means a paying user without access. + +### 8. Make staging usable (shipped: #81) + +`staging-e2e.yml` is skipped unless `STAGING_E2E_ENABLED=true`, and a skipped job is not a pass. Staging has no migration or function sync procedure, and the admin staging switcher from the backlog was never built. + +- Write the staging sync procedure, reusing `backend:status` from item 6 against the staging ref. +- Build the admin-only switcher that sets the `debate-supabase-env` localStorage key that `src/lib/supabase/env.ts` already reads. +- Then enable the staging workflow. Applying anything to staging needs Adam's authorization. + +## Code health + +### 9. Tests for the payment path and other untested functions + +These Edge Functions have no co-located tests: `stripe-webhook`, `create-checkout`, `create-portal`, `redeem-promo`, `share`, `export-gdoc`, `demo-code-login`, `announcements`, `workspace-invites`. Some are touched indirectly by `src/test/*security*` and `webhook-event-lifecycle.test.ts`. + +- Start with money and access: `stripe-webhook` (signature rejection, idempotent replay, each subscription event), `create-checkout`, `create-portal`, then `share` and `demo-code-login`. +- Use the existing Deno test pattern in `supabase/functions/ai/*.test.ts`. + +### 10. Restore the coverage thresholds + +`vitest.config.ts` lowered branches to 80 and functions to 89. The TODO there names the gaps: `stripe.ts`, `AuthProvider.tsx`, `session-store.ts` and `ai/client.ts`. + +- Cover those branches and raise the thresholds back to 90. Do not exclude files to get there. +- Add tests for the new Current Debate code, which no test references by name: `src/lib/debate/context-state.ts`, `src/pages/CurrentDebatePage.tsx`, `src/components/features/debate/DebateForm.tsx`, `UnsortedWork.tsx` and `debate-items.ts`. Also cover `src/components/features/sparring/SparringRound.tsx` and `src/lib/supabase/edge-function-error.ts`. + +### 11. Split the client AI module + +`src/lib/ai/client.ts` is 1,560 lines and `src/lib/stores/session-store.ts` is 1,372. The server side was split per feature in #75; the client was not. + +- Move each feature's request builder and response parsing into `src/lib/ai/features/.ts` behind the same exports, so callers do not change. +- Move code without changing behavior. Existing tests must pass unchanged. +- Do `session-store.ts` in a separate PR afterwards if the first split goes well. + +### 12. Dependency and dead-code cleanup + +- `pg` and `@hocuspocus/server` sit in the client `dependencies`; they belong to `services/collaboration`. +- The package is still named `vite_react_shadcn_ts`. +- `@typescript-eslint/no-unused-vars` is turned off in `eslint.config.js`. Turn it on, fix what it finds, and run `knip` once to list unused exports and files. +- Three `no-control-regex` sanitizers (`ai/request.ts:29`, `ai-stream/index.ts:66`, `_shared/prompts/context.ts:45`) duplicate one job. Share one helper. +- Major upgrades (React 19, Tailwind 4, Recharts 3, date-fns 4, i18next 24+) are separate PRs. Plan them only after this cleanup. + +## Product polish + +### 13. First-run flow that starts with a debate (shipped: #82) + +A new user lands on `/app`, sees a "No current debate" card, and gets a tour of the tools that skips Current Debate. The first tool they open may then show the motion-switch dialog. + +- Make creating the first debate the first tour step, then point at Library and one Prepare tool. +- Do not show `DebateMotionGate` to a user who has no Current Debate yet. Offer to create one from the motion they typed. +- Prove it with a Playwright journey for a fresh account, in English and Hungarian. + +### 14. Accessibility pass + +- Add `@axe-core/playwright` checks to the existing route matrix, failing on serious and critical issues. +- Replace the `div role="button"` handlers in `FlowchartPage.tsx` and `LibraryPage.tsx` with real buttons. +- Add a skip link, and check focus return after the Review sheet and debate dialogs close. + +### 15. Close out loose plans and PWA gaps + +- Update `plans/README.md`: #75 shipped, and this plan is the new backlog. +- Decide PR #63 (Workspace import/export). Workspace is on hold and the PR has open P1 findings, so close it and keep the branch. +- Add 192px, 512px and maskable icons to `public/manifest.webmanifest`. Localize the offline page in `public/sw.js`. +- Gate `/app/admin` on the client as well. The `admin` function already returns 403 to non-admins, so this only stops non-admins from seeing an empty admin screen. + +## Not on this list + +- Running the legacy purge (`private.purge_legacy_debate_material`). Adam decided on 2026-09-30 to keep production's legacy rows. +- Collaborative Workspace. It stays hidden until Adam restarts it. +- Changing which checks are required. `live-e2e` costs money on every PR, but relaxing a required check is Adam's decision, not a cleanup. diff --git a/plans/README.md b/plans/README.md index b46027e13..ed37efcb9 100644 --- a/plans/README.md +++ b/plans/README.md @@ -1,19 +1,16 @@ # Plans -**Status:** Plan index. Plans describe intended work, not current implementation truth. Reconciled against `main` at `0bda733` on 2026-09-27. +**Status:** Plan index. Plans describe intended work, not current implementation truth. Reconciled against `main` at `791ccb0` on 2026-10-02. Every plan lives in this folder. Before executing one, recheck its paths, assumptions, product decisions, and verification commands against current source. ## In progress -- **Debate-aware AI and smaller modules (PR 2).** Every feature gets server-loaded Current Debate context with a visible Review control, each feature gets its own prompt module, and the 88 KB `supabase/functions/ai/index.ts` is split into per-feature handlers behind the same endpoints. Plan: [2026-09-25 debate AI quality modules](2026-09-25_074255-debate-ai-quality-modules.md). - -- **Voice Sparring and Crossfire.** Students speak their turns through the browser's speech recognition and the opponent answers aloud through ElevenLabs Eleven v4, with the browser voice as fallback. Plan: [2026-10-01 voice sparring and crossfire](2026-10-01-voice-sparring-crossfire.md). +Nothing is in progress. ## Backlog -- **Admin staging switcher.** `src/lib/supabase/env.ts` already resolves staging from the `debate-supabase-env` localStorage key, but no admin UI sets it. Staging migration and function sync remain operations that need Adam's authorization. -- **Document portability PR.** [Programcode/DebateLab#63](https://github.com/Programcode/DebateLab/pull/63) adds Workspace import and export. Workspace is on hold and the PR has open P1 review findings, so decide whether to close or park it. +- **Next 15.** Security and data protection, operational safety, code health, and product polish, ordered by risk. Items 3, 5, 6, 8 and 13 have shipped; the plan lists the follow-ups they left. Plan: [2026-10-01 next 15](2026-10-01-next-15.md). Item 15 covers the PR #63 decision. ## On hold @@ -23,6 +20,8 @@ Every plan lives in this folder. Before executing one, recheck its paths, assump These plans are finished. Their files were removed; git history keeps them. +- Voice Sparring and Crossfire: #84. Plan kept as the record: [2026-10-01 voice sparring and crossfire](2026-10-01-voice-sparring-crossfire.md). +- Debate-aware AI and smaller modules: #75. Plan kept as the record: [2026-09-25 debate AI quality modules](2026-09-25_074255-debate-ai-quality-modules.md). - Current Debate foundation: #72. Plan kept as the record: [2026-09-25 current debate foundation](2026-09-25_074255-current-debate-foundation.md). - Email OTP and demo-code sign-in: #40, #41. - AI control enforcement at the edge: #32, #42.