Skip to content

Commit 0141e56

Browse files
committed
feat: continue restart plan
1 parent e50c06f commit 0141e56

12 files changed

Lines changed: 985 additions & 7 deletions

File tree

new-deepnotes/PLAN_PROGRESS.md

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../
1313
| **0** — OpenAPI + Drizzle inventory | **Done** | tRPC→REST/WS map: [docs/TRPC_REST_MAP.md](./docs/TRPC_REST_MAP.md). Drizzle + migration `0000_legacy_baseline` match `postgres-init.sql` core tables. Auth/CORS/forks: [docs/AUTH_AND_CORS.md](./docs/AUTH_AND_CORS.md), [docs/CLIENT_FORKS.md](./docs/CLIENT_FORKS.md). |
1414
| **1** — Legacy repo hygiene | **Optional / n/a** | Parallel track only if still editing the old monorepo. |
1515
| **2** — Repo bootstrap | **Done** | Template DB integration test + CI `DATABASE_ADMIN_URL`; deploy doc: [docs/DEPLOY_CLOUDFLARE.md](./docs/DEPLOY_CLOUDFLARE.md). **`@deepnotes/web`:** Vitest + happy-dom + `@vue/test-utils`; `vite.config` uses `defineConfig` from `vitest/config`. Optional: Wrangler deploy job. |
16-
| **3** — REST + Drizzle features | **In progress** | Auth + account: sessions, register, public email verify, account delete, password + **email change** + **Postgres integration tests** for both (see [Phase 3 test coverage](#phase-3-test-coverage-detail)). **Next (priority):** 2FA HTTP surface → pages/groups CRUD → realtime/collab → Stripe webhook. |
16+
| **3** — REST + Drizzle features | **In progress** | Account surface includes **2FA** (`/api/users/me/2fa/...`); see [2FA HTTP routes](#2fa-http-routes-phase-3). **Next (priority):** pages + groups CRUD (per [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md)) → realtime/collab → Stripe webhook. |
1717
| **4** — Client MVP | **Not started** | Auth → list → page → Yjs → groups; crypto/libs port as needed. **Parallel:** SPA structure, OpenAPI client, small E2E smoke—see [Frontend / UI track](#frontend--ui-track). |
1818
| **5** — Cutover | **Not started** | Canary, redirect, retire `/trpc` when safe. |
1919

@@ -36,7 +36,7 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../
3636
- [x] **Rate limit:** failed login counters (`login-rate-limit.test.ts`).
3737
- [x] **Email crypto:** `encryptUserEmail` / `decryptUserEmail` + `hashUserEmail` (legacy parity cases).
3838
- [x] **Email change mailer:** `sendEmailChangeVerificationEmail` (dev skip, missing API key, Resend errors/success via mocked `fetch`).
39-
- [x] **HTTP contracts:** OpenAPI path presence; Zod for `userEmailChange*`, password change byte fields (`schemas/users.test.ts`).
39+
- [x] **HTTP contracts:** OpenAPI path presence; Zod for `userEmailChange*`, password change, **2FA** bodies + finish TOTP (`schemas/users.test.ts`).
4040
- [x] **Worker smoke:** `503` when env/DB not configured for `/api/users/me/email-change` (+ confirm), alongside other session routes.
4141
- [x] **DB integration (template Postgres):** `account-flows.integration.test.ts` (renamed from `email-change.integration.test.ts`). See [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail) for the per-case list.
4242

@@ -76,7 +76,20 @@ Integration tests use `describe.skipIf` when `DATABASE_URL` (and admin URL for `
7676
- [x] `POST /api/users/me/email-change``performUserEmailChangeRequest`: `oldLoginHash` + `newEmail`; **403** demo, **400** bad password or “email already in use” (global `email_hash` match, same as legacy); sets `encrypted_new_email` + 6-digit `email_verification_code`; Resend (subject/body like legacy) or **200** `{ "emailVerificationCode" }` when `SEND_EMAILS=false`; **204** when emailed.
7777
- [x] `POST /api/users/me/email-change/confirm``performUserEmailChangeConfirm`: one call (WS two-step collapsed); `oldLoginHash`, `emailVerificationCode` (6 digits), `newLoginHash`, `userEncrypted*Keyring` (b64, same as register/password); verifies code + password; applies new `encrypted_email` / `email_hash`, clears pending fields, PHC + rewrapped keyrings, invalidates **all** `sessions`, **204** + `buildClearSessionCookies`; optional `updateStripeCustomerEmail` in worker (matches legacy `customers.update` after commit, errors non-fatal).
7878
- [x] **`decryptUserEmail`** in `@deepnotes/session` for confirm; **`sendEmailChangeVerificationEmail`** (Resend); OpenAPI + [docs/TRPC_REST_MAP.md](./docs/TRPC_REST_MAP.md) updated.
79-
- [ ] **2FA (HTTP surface)**`POST /api/users/me/2fa/enable/request|finish`, `GET /api/users/me/2fa`, `POST /api/users/me/2fa/recovery-codes`, `POST /api/users/me/2fa/devices/forget`, `POST /api/users/me/2fa/disable` ([docs/TRPC_REST_MAP.md](./docs/TRPC_REST_MAP.md)). **Note:** `@deepnotes/session` already implements TOTP/recovery verification for **`POST /api/sessions/login`**; these routes expose enable/disable/load for the SPA.
79+
- [x] **2FA (HTTP surface)** — Hono + OpenAPI: `user-two-factor-settings.ts` (`encryptUserAuthenticatorSecret` in `session-crypto`). Routes: [2FA HTTP routes](#2fa-http-routes-phase-3). `load` is **`POST /api/users/me/2fa/load`** (password in JSON, not a `GET`[TRPC_REST_MAP](./docs/TRPC_REST_MAP.md) footnote). **Not yet in integration template DB:** 2FA enable → login with TOTP (optional follow-up; login path already uses `assertTwoFactorOk` in [two-factor.ts](./packages/session/src/two-factor.ts)).
80+
81+
### 2FA HTTP routes (Phase 3)
82+
83+
| Path | Replaces (legacy) | Request body | Success |
84+
|------|-------------------|-------------|---------|
85+
| `POST /api/users/me/2fa/enable/request` | `twoFactorAuth.enable.request` | `{ "loginHash" }` b64 | **200** `{ "secret", "keyUri" }` (pending TOTP, not yet enabled) |
86+
| `POST /api/users/me/2fa/enable/finish` | `twoFactorAuth.enable.finish` | `{ "loginHash", "authenticatorToken" }` (6 digits) | **200** `{ "recoveryCodes" }` (6 × 32-char hex) |
87+
| `POST /api/users/me/2fa/load` | `twoFactorAuth.load` | `{ "loginHash" }` | **200** `{ "secret", "keyUri" }` (2FA must already be on) |
88+
| `POST /api/users/me/2fa/recovery-codes` | `generateRecoveryCodes` | `{ "loginHash" }` | **200** new recovery codes |
89+
| `POST /api/users/me/2fa/devices/forget` | `forgetTrustedDevices` | `{ "loginHash" }` | **204** |
90+
| `POST /api/users/me/2fa/disable` | `disable` | `{ "loginHash" }` | **204** |
91+
92+
- **Parity:** Demo accounts **403**; wrong password **400** “Password is incorrect.”; TOTP fail on finish **400** “Authenticator token is incorrect.”; `otplib` `keyuri` issuer **“DeepNotes”** (same as legacy tRPC). Recovery codes: `libsodium` hex + [hashRecoveryCode / encryptRecoveryCodes](packages/session/src/crypto/session-crypto.ts) (legacy-equivalent). Forget devices: `UPDATE devices SET trusted = false` for `user_id`.
8093

8194
### Not started (Phase 3 — pages, groups, infra)
8295

@@ -132,8 +145,8 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte
132145
|---------------|------|------------------|---------------------------|
133146
| **`@deepnotes/db`** | Drizzle + migrations | `template-db.test.ts`: clone template DB, smoke SQL | More assertions on FKs / critical columns after schema grows |
134147
| **`@deepnotes/session`** | Auth, account, crypto orchestration | Unit: `login-rate-limit`, `encrypt-user-email`, `email-hash`, `send-email-change-code`. **Integration:** `account-flows.integration.test.ts` — email change + **password change** (PHC + unwrap), wrong passwords/codes, session invalidation; template `dn_test_tpl_session_email`, **`@deepnotes/db/testing/template-db`**. | **`performSessionLogin` / refresh** with template DB + device/session rows; **Redis** + `performSessionLogin` failed-login; optional **demo 403** integration |
135-
| **`@deepnotes/api`** | Zod + OpenAPI | `openapi.test.ts` (health + route registry); **`schemas/users.test.ts`** (email/password change bodies, 6-digit code) | Schemas for sessions + remaining routes; optional **snapshot** of OpenAPI fragment for drift |
136-
| **`@deepnotes/api-worker`** | Hono on Worker | `index.test.ts`: health, OpenAPI JSON, **503** when secrets/DB not bound (incl. email-change paths) | **200-path tests** with test `SessionEnv` + Hyperdrive stub + template DB (heavier CI job) |
148+
| **`@deepnotes/api`** | Zod + OpenAPI | `openapi.test.ts` (health + session + 2FA paths); **`schemas/users.test.ts`** (email/password change, 2fa finish) | Schemas for pages/groups when they land; optional OpenAPI **snapshot** |
149+
| **`@deepnotes/api-worker`** | Hono on Worker | `index.test.ts`: 503 when env missing — includes **2FA** routes in matrix | **200** tests with stub `SessionEnv` + template DB (heavier) |
137150
| **`@deepnotes/web`** | SPA | `app.test.ts` (mount `App.vue`) | Auth UI + API client as in §5.8 |
138151

139152
**Principle:** keep **fast unit tests** on pure crypto, Zod, and mail/HTTP branches; add **Postgres-backed** flows incrementally (same template pattern as `@deepnotes/db`) so Phase 3 routes do not regress silently.
@@ -166,14 +179,15 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte
166179

167180
## Phase 3 working order (suggested)
168181

169-
Use this when resuming: **(done)** account HTTP surface through email change including password change. **(next)** 2FA CRUD on `/api/users/me/2fa*`, reusing session crypto already used at login. **(then)** pages + groups from [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md) (user prefs, CRUD, group privacy/password). **(then)** long pole: **realtime + collab** (protocol, Worker/DO, no key rotation) and **Stripe** webhook + billing routes + wire `updateStripeCustomerEmail` / `deleteStripeCustomer` from account flows where applicable.
182+
Use this when resuming: **(done)** account HTTP through 2FA (incl. `load` as POST, see map). **(next)** `users.pages` + `groups` + `pages` REST from [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md). **(then)** **realtime + collab** (no key rotation) and **Stripe** + wire billing hooks on account routes.
170183

171184
---
172185

173186
## Short log (newest first)
174187

175188
| Date | Change |
176189
|------|--------|
190+
| 2026-04-27 | **2FA account HTTP:** `user-two-factor-settings.ts`, `encryptUserAuthenticatorSecret` in `session-crypto`, Zod + OpenAPI + Hono for `/api/users/me/2fa/*` (6 routes); [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md)`load` is POST not GET; see [2FA HTTP routes](#2fa-http-routes-phase-3) below. |
177191
| 2026-04-27 | **Integration tests:** expanded `account-flows.integration.test.ts` (email wrong code; password change PHC + keyring unwrap with salt from PHC; `sessions` invalidation; wrong old password). Renamed from `email-change.integration.test.ts`. PLAN_PROGRESS: detailed Phase 3 test table + matrix gaps. |
178192
| 2026-04-26 | **Integration tests:** `@deepnotes/db` exports `@deepnotes/db/testing/template-db` + `db-url`; `@deepnotes/session``email-change.integration.test.ts` (Postgres template clone, register + email change + wrong password). PLAN_PROGRESS matrix + Phase 3 checklist updated. |
179193
| 2026-04-26 | **Tests:** `@deepnotes/session``encrypt-user-email.test.ts`, `email-hash.test.ts`, `send-email-change-code.test.ts`; `@deepnotes/api``schemas/users.test.ts`; api-worker — email-change routes in `503` matrix; PLAN_PROGRESS — package test matrix + Phase 3 test checklist. |

new-deepnotes/apps/api-worker/src/index.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ describe("api-worker", () => {
3434
["POST", "/api/users/email-verification/resend"],
3535
["POST", "/api/users/me/email-change"],
3636
["POST", "/api/users/me/email-change/confirm"],
37+
["POST", "/api/users/me/2fa/enable/request"],
38+
["POST", "/api/users/me/2fa/enable/finish"],
39+
["POST", "/api/users/me/2fa/load"],
40+
["POST", "/api/users/me/2fa/recovery-codes"],
41+
["POST", "/api/users/me/2fa/devices/forget"],
42+
["POST", "/api/users/me/2fa/disable"],
3743
] as const)("returns 503 for %s %s when auth env is not configured", async (method, path) => {
3844
const res = await app.request(`http://test${path}`, { method });
3945
expect(res.status).toBe(503);

0 commit comments

Comments
 (0)