diff --git a/README.md b/README.md index 057254a..da350fb 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ | Fase 3 — Tareas y Notas | ✅ Completa | | Fase 4 — Búsqueda y Pulido | 🟡 Búsqueda + RLS audit completos; PWA install/push/rate-limit/JWT rotation diferidos a sprint de deploy | | Fase 5 — Rediseño UX Mobile | ✅ Completa en scope automatizable (shell + masthead + big-button create + row redesign + selection mode + sticky detail chrome); presence avatars en cards + M-swipe/SEL-long-press touch E2E diferidos a WebKit install | +| Fase 6 — Deploy readiness | ⏳ Propuesta (PWA injectManifest + manifest + iconos placeholder + Kong rate-limit + rotación JWT + Lighthouse audit). Push notifications retiradas del scope. | | Suite de tests (pgTAP + Vitest + Playwright) | ✅ 233 tests verdes (34 pgTAP + 140 integración + 15 unit + 44 E2E), 2 skipped, ejecutables con `just test-all` | --- @@ -41,8 +42,9 @@ Las fases ya cerradas (0, 1, 2a) reflejan este patrón retroactivamente: la secc | Fase 3 | [fase-3-tareas-notas.md](plan/fase-3-tareas-notas.md) | 1–2 semanas | | Fase 4 | [fase-4-busqueda-pulido.md](plan/fase-4-busqueda-pulido.md) | 1 semana | | Fase 5 | [fase-5-mobile-ux.md](plan/fase-5-mobile-ux.md) | 1–2 semanas | +| Fase 6 | [fase-6-deploy-prep.md](plan/fase-6-deploy-prep.md) | 3–5 días | -**Total estimado: 10–15 semanas (con Fase 5 mobile)** +**Total estimado: 11–16 semanas (con Fase 6 deploy prep)** Los mockups de referencia viven en [`design/`](design/) (organizados por pantalla — ver `design/slate_collective/DESIGN.md` para la dirección "Monolith Editorial"). diff --git a/plan/fase-6-deploy-prep.md b/plan/fase-6-deploy-prep.md new file mode 100644 index 0000000..a5ce7ad --- /dev/null +++ b/plan/fase-6-deploy-prep.md @@ -0,0 +1,227 @@ +### Fase 6 — Deploy readiness (PWA + rate-limit + secret rotation) +**Estado: ⏳ Propuesta** +**Duración estimada: 3–5 días** +**Objetivo: cerrar los ítems diferidos de Fase 4 que bloquean un despliegue público del MVP.** + +Esta fase cierra los ítems que en Fase 4 se dejaron para "sprint de deploy": PWA real (SW custom, manifest, iconos), rate-limiting en Kong, rotación de JWT, y validación Lighthouse + auditoría RLS manual. Notificaciones push se retiran explícitamente del scope (decisión del 2026-04-14; sin caso de uso actual). + +Sigue el patrón TDD donde el cambio es programático (rate-limit, SW registration, manifest served). La auditoría Lighthouse + el chequeo manual de rotación quedan como tareas documentadas — no son test automatizables. + +--- + +#### 6.0 Tests primero — escribir antes de implementar + +**Vitest (`packages/test-utils/tests/`):** + +- [ ] `rate-limit.test.ts` — carga el plugin de Kong con POST a `/auth/v1/token` 11 veces seguidas desde la misma IP, espera `429` en el intento 11. Segunda prueba: 21 POSTs a `/rest/v1/collective_invitations` desde el mismo usuario autenticado en menos de una hora, espera `429`. + +**Playwright (`apps/web/tests/e2e/`):** + +- [ ] `pwa.test.ts`: + - P-01: `manifest.webmanifest` accesible en `/manifest.webmanifest` con status 200 y `application/manifest+json` content-type; contiene `name`, `short_name`, `start_url`, `display: standalone`, al menos un icono 192×192 y uno 512×512. + - P-02: tras login, un `navigator.serviceWorker.getRegistration()` en la página devuelve un SW registrado y con `active.state === 'activated'`. + +**Manual / scripted (Justfile):** + +- [ ] `just lighthouse` — arranca el build de producción local, corre Lighthouse CLI contra `http://localhost:3000`, exige score PWA ≥ 90, score Accessibility ≥ 90, score Best Practices ≥ 90. Falla el recipe si alguno cae. Se ejecuta **después** de `just build + just serve`. + +--- + +#### 6.1 PWA `injectManifest` migration (scope: minimal) + +**Decisión de scope:** estrategia minimal confirmada por el usuario: +- Precache del shell + assets generados por Vite (JS/CSS/fonts/iconos) +- Sin runtime caching de Supabase — las rutas `/rest/v1/*` siguen netowrk-only +- Las escrituras offline ya las cubre `$lib/sync` (`pending_ops` IndexedDB) + `online` event flush + +**Cambios:** + +- [ ] Crear `apps/web/src/sw.ts` (no `service-worker.ts` — SvelteKit intercepta ese nombre y bloquea imports externos; ver gotcha #10 en CLAUDE.md): + ```ts + /// + import { precacheAndRoute } from 'workbox-precaching'; + declare const self: ServiceWorkerGlobalScope; + precacheAndRoute(self.__WB_MANIFEST); + // skip waiting on first install so updates land without a double reload + self.addEventListener('install', () => void self.skipWaiting()); + self.addEventListener('activate', (e) => e.waitUntil(self.clients.claim())); + ``` +- [ ] Actualizar `apps/web/vite.config.ts` — cambiar `@vite-pwa/sveltekit` a `strategies: 'injectManifest'`: + ```ts + SvelteKitPWA({ + strategies: 'injectManifest', + srcDir: 'src', + filename: 'sw.ts', + injectManifest: { + globPatterns: ['client/**/*.{js,css,ico,png,svg,webp,woff,woff2}'], + }, + manifest: { … }, // see 6.2 + devOptions: { enabled: true, type: 'module', navigateFallback: '/' } + }) + ``` +- [ ] Retirar el comentario en `vite.config.ts` que justifica `generateSW` (ya no aplica). +- [ ] Añadir `workbox-precaching` como dep de `apps/web` si no lo trae ya `@vite-pwa/sveltekit`. +- [ ] Probar con `just build + just serve` que `/sw.js` se sirve y precachea el manifest. + +**Criterio de aceptación:** `navigator.serviceWorker.getRegistration()` devuelve un SW activo tras cargar la app en una build de producción. La app carga offline sin errores visibles (los fetch de Supabase fallan graciosamente y la `SyncBanner` aparece). + +--- + +#### 6.2 Manifest + iconos + meta tags iOS + +- [ ] `apps/web/static/manifest.webmanifest` (o dejar que `SvelteKitPWA` lo genere desde la config): + ```json + { + "name": "Colectivo", + "short_name": "Colectivo", + "description": "Gestión colaborativa del hogar", + "theme_color": "#0f172a", + "background_color": "#f7f9fb", + "display": "standalone", + "orientation": "portrait", + "start_url": "/", + "scope": "/", + "icons": [ + { "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" }, + { "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" }, + { "src": "/icons/icon-512-maskable.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } + ] + } + ``` +- [ ] Iconos placeholder — `apps/web/static/icons/`: + - `icon-192.png`, `icon-512.png`, `icon-512-maskable.png`, `icon-180.png` (iOS touch icon) + - Placeholder neutro: fondo `#0f172a`, glyph "C" blanca centrada. Generar vía ImageMagick / script en `infra/scripts/generate-placeholder-icons.sh`. **Documentar en `static/icons/README.md` que deben reemplazarse antes del lanzamiento público.** +- [ ] Añadir meta tags iOS en `src/app.html`: + ```html + + + + + ``` + +**Criterio de aceptación:** en Lighthouse (`just lighthouse`) la categoría "Installability" pasa sus auditorías (manifest válido, iconos ≥ 192 y ≥ 512, SW registrado, start_url responde 200). + +--- + +#### 6.3 Kong rate-limiting + +**Límites confirmados:** + +| Endpoint | Límite | Ventana | +|----------|--------|---------| +| `/auth/v1/token` | 10 | 1 min / IP | +| `/auth/v1/token` | 60 | 1 hora / IP | +| `/auth/v1/authorize` | 30 | 1 min / IP | +| `/rest/v1/collective_invitations` POST | 20 | 1 hora / usuario autenticado | + +Kong ships with `rate-limiting` plugin; política `local` (sin Redis) es suficiente para single-instance. Config en `infra/kong.yml`: + +```yaml +plugins: + - name: rate-limiting + service: auth-token-login + config: + minute: 10 + hour: 60 + policy: local + fault_tolerant: true + + - name: rate-limiting + service: auth-authorize + config: + minute: 30 + policy: local + + - name: rate-limiting + route: invitations-post + config: + hour: 20 + policy: local + limit_by: consumer # requires JWT claim-to-consumer mapping +``` + +- [ ] Actualizar `infra/kong.yml` con las rutas específicas `auth-token-login` y `auth-authorize` (hoy ambas viven bajo un único servicio `/auth/v1/*` con path matching). Separar en routes diferenciadas. +- [ ] Para `limit_by: consumer` en invitaciones — Kong necesita resolver el JWT `sub` a un consumer. La alternativa simple: `limit_by: header` usando `Authorization` (se comporta como limit-per-token, que en PKCE rota cada hora pero sirve). +- [ ] Tests en `packages/test-utils/tests/rate-limit.test.ts` verifican que la respuesta 11/21 es 429 con headers `X-RateLimit-Remaining`. +- [ ] Restart de kong: `docker compose -f infra/docker-compose.dev.yml up -d --force-recreate kong`. + +**Criterio de aceptación:** los tests pasan; el 11º login desde la misma IP devuelve 429; la 21ª invitación del mismo usuario devuelve 429. + +--- + +#### 6.4 Rotación de JWT + template de producción + +**Dev rotation:** + +- [ ] Generar nuevo `SUPABASE_JWT_SECRET` (≥ 32 chars): + ```bash + openssl rand -base64 48 | tr -d '\n' + ``` +- [ ] Firmar nuevas `ANON_KEY` y `SERVICE_ROLE_KEY` con ese secret (payloads `role=anon` / `role=service_role`, `iss=supabase-demo`, `exp` futuro). Script sugerido: `infra/scripts/rotate-jwt.sh` que toma el nuevo secret y emite los tres valores. +- [ ] Actualizar **ambos** ficheros a la vez (memory `project_env_keys_trap`): + - root `.env` → `SUPABASE_JWT_SECRET`, `PUBLIC_SUPABASE_ANON_KEY`, `SUPABASE_SERVICE_ROLE_KEY` + - `apps/web/.env.development` → `PUBLIC_SUPABASE_ANON_KEY` +- [ ] `docker compose up -d --force-recreate kong auth rest realtime`. +- [ ] Sesiones existentes quedan invalidadas — todos los usuarios de dev tienen que re-loguearse una vez. Documentado en el commit message. + +**Production template (no commit de secretos reales):** + +- [ ] Crear `apps/web/.env.production.example` y `.env.production.example` (root) con: + ``` + # Regenerate every 90 days (or on security incident). + # See infra/scripts/rotate-jwt.sh for the generation procedure. + SUPABASE_JWT_SECRET= + PUBLIC_SUPABASE_ANON_KEY= + SUPABASE_SERVICE_ROLE_KEY= + … + ``` +- [ ] Añadir `infra/scripts/rotate-jwt.sh` — bash script que: + 1. genera un nuevo secret + 2. usa `node`/`python` para firmar los tres JWT (`anon`, `service_role`, optionally `authenticated` for tests) + 3. imprime los valores para que el operador los pegue en el `.env` de prod + 4. **no** toca ficheros — el operador decide qué hacer con ellos +- [ ] Documentar el procedimiento al final de `CLAUDE.md` o en un nuevo `infra/README.md`: cuándo rotar (cadencia + incidentes), cómo, qué contenedores reiniciar, qué sesiones se invalidan. + +**Criterio de aceptación:** `.env.production.example` existe con placeholders + comentarios. `infra/scripts/rotate-jwt.sh` genera un trio válido (verificable firmando un token y descifrándolo). El stack dev sigue verde tras la rotación (cambiar el secret y que `just test-all` vuelva a verde confirma que no hay valores hardcodeados sueltos). + +--- + +#### 6.5 Lighthouse + auditoría RLS manual + +- [ ] `just lighthouse` recipe: + ```makefile + lighthouse: + pnpm --filter @colectivo/web build + (pnpm --filter @colectivo/web run preview --port 3000 &) ; sleep 3 + pnpm exec lighthouse http://localhost:3000 \ + --only-categories=pwa,accessibility,best-practices \ + --output=html --output-path=./lighthouse-report.html \ + --chrome-flags='--headless' + pkill -f 'vite preview --port 3000' + ``` +- [ ] Añadir `lighthouse` como devDep en `apps/web`. +- [ ] Documentar umbrales: PWA ≥ 90, Accessibility ≥ 90, Best Practices ≥ 90. +- [ ] RLS curl audit — script `infra/scripts/rls-audit.sh` que firma un JWT como Eva y hace GET a `/rest/v1/shopping_items`, `/rest/v1/shopping_lists`, `/rest/v1/tasks`, `/rest/v1/notes`, `/rest/v1/collectives` — todos deben devolver `[]`. Es la versión manual del test de integración `rls-audit.test.ts`. + +**Criterio de aceptación:** Lighthouse report archivado en `lighthouse-report.html` con los tres scores ≥ 90. `rls-audit.sh` imprime "OK" en todas las tablas. + +--- + +#### 6.Z Verificación final + +- [ ] `just test-all` → 0 failures (incluye los nuevos de 6.0: PWA install + rate-limit). +- [ ] `just lighthouse` → PWA ≥ 90. +- [ ] `infra/scripts/rls-audit.sh` → OK en todos los endpoints. +- [ ] Manual — PWA instalable en iOS Safari (Add to Home Screen) y Android Chrome (prompt). +- [ ] Commit con rotación de JWT + recordatorio en el mensaje sobre re-login obligatorio. + +**Criterio de aceptación:** todos los tests pasan; Lighthouse verde; la app se puede instalar; RLS auditado. Listo para desplegar a VPS con `just deploy`. + +--- + +### Scope explícito fuera + +- Notificaciones push — retiradas del plan (decisión 2026-04-14, sin caso de uso actual). +- CI con Docker efímero corriendo `just test-all` — diferido (prioridad menor ahora que el deploy está cerca). +- Runtime caching de Supabase en el SW — diferido (la SyncBanner + pending_ops cubre el MVP offline story). +- Iconos finales de producción — placeholders esta vez; reemplazar antes del lanzamiento.