Files
collective-lists/plan/fase-6-deploy-prep.md
Oier Bravo Urtasun 1f5b34d55d docs(fase-6): propuesta — deploy readiness (PWA + rate-limit + JWT rotation)
New plan file closing out the Fase 4 deferrals with specifics confirmed
by user on 2026-04-14:
  - 6.1 PWA injectManifest, MINIMAL scope (precache shell only; Supabase
    GETs stay network-only since $lib/sync + SyncBanner already cover
    offline writes + the offline indicator)
  - 6.2 manifest.webmanifest + neutral placeholder icons (192/512/180 +
    maskable 512) + iOS meta tags
  - 6.3 Kong rate-limiting — 10/min + 60/hour on /auth/v1/token per IP,
    30/min on /auth/v1/authorize per IP, 20/hour on invitations POST
    per authenticated user
  - 6.4 JWT rotation — dev rotate now + .env.production.example template
    + infra/scripts/rotate-jwt.sh
  - 6.5 Lighthouse PWA ≥ 90 + scripted rls-audit.sh (curl edition of
    Vitest's rls-audit.test.ts)
  - 6.Z verification

Explicitly out of scope (recorded at bottom of the plan):
  - Push notifications (no current use case)
  - Ephemeral-Docker CI (deferred)
  - Runtime caching of Supabase in the SW (deferred)
  - Final production icon art (placeholders land now; real art before
    public launch)

README phase table + index updated; plan total now 11–16 weeks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 03:39:32 +02:00

228 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
### Fase 6 — Deploy readiness (PWA + rate-limit + secret rotation)
**Estado: ⏳ Propuesta**
**Duración estimada: 35 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
/// <reference lib="webworker" />
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
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Colectivo" />
<link rel="apple-touch-icon" href="/icons/icon-180.png" />
```
**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=<openssl rand -base64 48 | tr -d '\n'>
PUBLIC_SUPABASE_ANON_KEY=<sign with above>
SUPABASE_SERVICE_ROLE_KEY=<sign with above>
```
- [ ] 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.