Ctrl K
ComunidadGuías…

STACK.md

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# STACK.md — Reconnaissance (Phase 0)

État des lieux établi en lisant le code réel, avant l'implémentation du système
d'authentification avancé (passkeys, vérif email, 2FA, approbation cross-device).

## Stack détectée

| Domaine | Réalité |
|---|---|
| Framework | **Next.js 16** (App Router), React 19, TypeScript |
| Paquets | **npm** (`package-lock.json`) |
| ORM / DB | **Prisma 7** → PostgreSQL · client généré dans `src/generated/prisma` |
| Auth | **Auth.js v5** (`next-auth@5 beta`), **stratégie JWT** |
| Middleware | **`src/proxy.ts`** (Next 16 a renommé `middleware` → `proxy`) |
| UI | Tailwind 4 · shadcn/ui (Radix) · Motion · sonner (toasts) · lucide |
| Email | `resend` installé + `AUTH_RESEND_KEY`/`EMAIL_FROM` présents dans `.env`, **mais jamais câblés** (aucun helper/template). `.env.example` ne les liste pas. |
| Rate-limit | **Inexistant** |

## Flux d'authentification actuel

- `src/auth.ts` : config complète (Node) — PrismaAdapter + `session.strategy:"jwt"`.
  Providers : Google, Discord (`allowDangerousEmailAccountLinking`), Credentials (bcrypt).
- `src/auth.config.ts` : config edge — callbacks `jwt`/`session` (attachent `id`),
  `authorized` protège `/drive /setup /settings /admin /stats /shares /backup`.
- `src/proxy.ts` : applique `authorized` sur toutes les routes hors `api`/static.
- Sessions **non stockées en DB** (JWT). La table `Session` existe mais dort.
- Inscription `POST /api/auth/register` : crée l'user, **n'envoie aucun email**,
  `emailVerified` reste `null`. La page `/login` a déjà l'état `?verify=1` câblé.
- Flux natif iOS : OAuth navigateur système → code HMAC stateless
  (`lib/auth/native-code.ts`) → `GET /api/native-auth/exchange` **forge le cookie
  de session via `encode()` de `next-auth/jwt`** puis redirige vers `/drive`.
  ⭐ Ce mécanisme `encode()` est réutilisable pour ouvrir une session APRÈS un
  challenge multi-facteur, sans passer par `authorize()`.

## Modèle de données (extrait pertinent — `prisma/schema.prisma`)

- `User` possède déjà : `emailVerified`, `password` (bcrypt), `vaultPin` (bcrypt),
  `vaultSalt` (sel PBKDF2). Manquent : `lastLoginAt`, `twoFactorEnabled`, etc.
- `Webhook.encKey` : clé de chiffrement **par drive** (AES-256-GCM), stockée
  chiffrée côté serveur avec `ENCRYPTION_KEY`.

## Flux de la clé E2EE (point critique)

**L'E2EE est déjà entièrement découplé du mot de passe de login :**

1. **Fichiers normaux** → clé aléatoire **par drive**, générée client
   (`crypto.getRandomValues`), stockée *chiffrée* côté serveur (`ENCRYPTION_KEY`)
   dans `Webhook.encKey`, **re-téléchargée après authentification** via
   `/api/webhooks` (`lib/auth/sync.ts`). Le facteur de login n'intervient jamais.
2. **Coffre-fort** → clé dérivée du **PIN** via PBKDF2 (200k itérations, SHA-256)
   + `vaultSalt`. Indépendante du mot de passe.

➡️ **Conséquence : passkeys / OAuth / cross-device ne cassent rien.** Aucun
wrapping multi-facteur ni PRF WebAuthn n'est nécessaire.

⚠️ **Nuance** : le serveur détient la clé des fichiers normaux (ce n'est donc PAS
du zero-knowledge — c'est déjà le cas aujourd'hui, et c'est ce qui permet les
partages publics par déchiffrement serveur). Seul le coffre est zero-knowledge.

## Décisions validées avec le porteur du projet

1. **Modèle E2EE : inchangé** (la clé reste récupérée du serveur après login).
2. **Rate-limiting : table PostgreSQL** (aucune dépendance externe).
3. **Cross-device : polling** (3–5 s) ; push iOS native laissée en TODO documenté.

## Décision d'architecture centrale : JWT « step-up »

Le JWT porte un palier `level: "pending" | "full"`. À la fin du 1er facteur, le
token est `pending` ; `proxy.ts` bloque les routes protégées et redirige vers le
challenge tant que le 2e facteur requis (email 24 h / 2FA / approbation
cross-device) n'est pas levé ; une route API « promeut » le token en `full`.
Uniforme pour Credentials, OAuth et passkey ; réutilise l'`encode()` déjà en place.