Repository Public
1# SP Drive — architecture23État : décisions prises après l'audit du 2026-11-11. Ce qui est réellement construit et vérifié est suivi dans4[`progress.md`](progress.md) ; ce document décrit la cible V1 et les raisons des choix.56## 1. Ce que l'audit a établi78| Sujet | Constat vérifié le 2026-11-11 |9|---|---|10| DNS | `drive.spboucher.ai` → `51.161.112.61` (passerelle MacLustr Tunnel BHS64). Aucune route Caddy encore : le site ne répond pas. |11| Dépôt | Aucun dépôt `sp-drive` n'existait. Créé sur le portable pour l'amorçage ; après `mld stage` + `mld deploy`, la source de vérité devient le clone du nœud (règle « remote-first »). |12| Conventions SP | SP Wallet (`wallet.spboucher.ai`) : monorepo pnpm, Fastify 5 + `postgres` + zod, web Vite/React 19/Tailwind 4 servi par l'API, bundle esbuild `dist/`, PM2, PostgreSQL 17 Homebrew, sauvegarde nocturne `pg_dump`, déploiement `mld`, passkeys `@simplewebauthn`. SP Drive reprend ces conventions. SP Wallet n'est ni modifié ni appelé. |13| Nœud | **m5ma** (Mac mini M5 Pro, 15 cœurs, 24 Go, SSD interne 460 Go dont 375 Go libres, aucune app). Node 26 et pnpm présents ; PostgreSQL, ClamAV, LibreOffice et ffmpeg à installer. FileVault désactivé (ouverture de session automatique après coupure) → le chiffrement au repos est fait par l'application. |14| NAS | Non utilisé (décision du propriétaire : SSD d'un nœud). La couche de stockage reste abstraite. |15| Docker | Absent de m5ma, et la convention du cluster est PM2 + services Homebrew. Docker Compose n'est pas retenu pour la première mise en production. |16| Courriel | Aucune configuration d'envoi propre à SP Drive. Les notifications sont dans l'application ; l'envoi par courriel est conditionnel à une clé fournie par le propriétaire. |17| Contrainte macOS | Les processus PM2 ne peuvent pas ouvrir de connexions LAN (confidentialité « réseau local »). Tout SP Drive tourne donc sur un seul nœud, en `127.0.0.1` et sockets Unix. |1819## 2. Écarts motivés par rapport à l'architecture « cible » du cahier des charges2021| Cible proposée | Choix | Raison |22|---|---|---|23| Next.js | API Fastify + SPA Vite/React/TypeScript/Tailwind servie par l'API | Convention SP Wallet déjà éprouvée avec `mld` (un port, un processus, une origine). Une application derrière connexion ne gagne rien au rendu serveur ; les téléversements résumables et le flux chiffré avec `Range` demandent un contrôle direct des flux HTTP. |24| Redis (cache, files d'attente) | File de travaux PostgreSQL (`jobs`, `FOR UPDATE SKIP LOCKED`), limitation de débit en mémoire + compteurs en base | Une dépendance de moins à exploiter et à sauvegarder ; les travaux sont transactionnels avec les métadonnées (un fichier placé et son travail d'aperçu sont validés ensemble). Charge visée : quelques utilisateurs. |25| MinIO / S3 | Interface `BlobStore` + pilote système de fichiers sur le SSD du nœud | Un seul nœud, pas de LAN entre processus. Le chiffrement et l'adressage sont au-dessus du pilote : un pilote S3 pourra être ajouté sans toucher au reste. Non construit en V1. |26| URL signées | Aucune URL de stockage n'est exposée | Chaque lecture passe par l'API, qui contrôle l'autorisation puis déchiffre en flux. |27| Docker Compose | PM2 + Homebrew via `mld` | Convention du cluster ; reprise automatique après coupure déjà gérée par `mld heal`. |28| Microservices | Deux processus : `sp-drive` (API + web) et `sp-drive-worker` (antivirus, aperçus, portails, ménage) ; modules séparés dans le code | Le cahier des charges demande de ne pas créer de microservices superflus. |2930## 3. Vue d'ensemble3132```33Navigateur ──HTTPS──▶ Caddy (BHS64) ──WireGuard──▶ m5ma:8660 sp-drive (Fastify)34 │ ├─ web statique (dist/web)35 │ ├─ /api/v1/… (session)36 │ └─ /api/v1/public/… (liens, portails)37 ▼38 PostgreSQL 17 (127.0.0.1) ◀──▶ sp-drive-worker39 │ ├─ clamd (socket Unix)40 ~/sp-drive-data │ ├─ sips / LibreOffice / ffmpeg41 ├─ blobs/ (objets chiffrés) │ sous sandbox-exec42 ├─ tmp/ (téléversements en cours)43 └─ quarantine/ (dépôts anonymes avant analyse)44```4546Modules de l'API (`services/api/src`) : `auth` (passkeys, sessions, récupération, TOTP), `authz` (politiques47centralisées), `tree` (dossiers, fichiers, corbeille, versions), `uploads`, `content` (lecture, `Range`, ZIP),48`shares`, `requests` (portails), `previews`, `search`, `notify`, `audit`, `jobs`, `blob` + `crypto` (stockage).4950## 4. Modèle de données5152Tables : `users`, `quotas`, `passkeys`, `sessions`, `auth_challenges`, `recovery_codes`, `invites`, `folders`,53`files`, `file_versions`, `storage_objects`, `previews`, `uploads`, `tags`, `file_tags`, `shares`,54`share_access_events`, `upload_requests`, `upload_submissions`, `upload_items`, `audit_events`, `jobs`,55`notifications`. Migrations numérotées avec `up` et `down` (`services/api/src/migrations`).5657Règles structurantes :5859- Chaque utilisateur possède un dossier racine. Tout dossier et tout fichier porte `owner_id` ; chaque requête60 filtre sur le propriétaire de la session. Un identifiant deviné d'un autre utilisateur répond 404, pas 403.61- Unicité des noms : index uniques partiels `(dossier parent, nom normalisé)` sur les éléments vivants, pour les62 dossiers et pour les fichiers ; un fichier et un dossier ne peuvent pas porter le même nom au même endroit63 (contrôle sous verrou). Le nom normalisé est NFC + minuscules.64- Concurrence : toute modification de l'arbre d'un utilisateur prend un verrou consultatif PostgreSQL par65 propriétaire dans sa transaction. Les déplacements vérifient l'absence de cycle sous ce verrou.66- Collisions : le client choisit `fail` (409 avec le nom en conflit), `rename` (« nom (2).ext ») ou `version`67 (nouvelle version du fichier existant). Jamais d'écrasement silencieux.68- Corbeille : `trashed_at` + `trash_root` sur l'élément et ses descendants ; la restauration traite le groupe,69 renomme en cas de collision et retombe sur la racine si le parent a disparu.70- Versions : `file_versions` (numéro croissant) → `storage_objects`. Restaurer une version en crée une nouvelle.71- Objets : `storage_objects` est unique par `(owner_id, sha256, taille)`. La déduplication ne traverse jamais les72 utilisateurs : aucun téléversement n'est raccourci parce qu'un autre compte possède le même contenu.73- Quota : somme des tailles des objets du propriétaire, vérifiée sous verrou à la création d'un téléversement et74 à sa finalisation ; les dépôts des portails comptent dans le quota du propriétaire du portail.75- Suppression définitive : les lignes partent, puis un ramasse-miettes supprime les objets et aperçus orphelins.7677## 5. Stockage et chiffrement7879- Clé maîtresse de 32 octets dans un fichier hors dépôt (`~/.config/sp-drive/master.key`, mode 600), copie dans80 le dossier de secrets du propriétaire. Sans elle les fichiers sont illisibles : elle fait partie de la sauvegarde.81- Chaque objet a sa propre clé de données (aléatoire), enveloppée par la clé maîtresse (AES-256-GCM, octet de82 version). La rotation de la clé maîtresse ré-enveloppe les clés de données sans réécrire les fichiers83 (`cli rotate-master-key`).84- Contenu : AES-256-GCM par blocs de 64 Kio, nonce = index du bloc, données associées = taille totale + index +85 marqueur de dernier bloc (détecte troncature, réordonnancement et substitution). Lecture par plage possible :86 seuls les blocs demandés sont déchiffrés (vidéo, reprise de téléchargement).87- Les fichiers temporaires (téléversements en cours, quarantaine, conversions) sont en clair sur le même SSD,88 dans des dossiers en mode 700, et supprimés à la fin du traitement ou par le ménage périodique.89- Transport : TLS terminé par Caddy sur BHS64, puis WireGuard jusqu'au nœud.9091## 6. Authentification9293- Passkeys WebAuthn uniquement (pas de mot de passe de compte). Vérification utilisateur exigée ; aucune donnée94 biométrique ne quitte l'appareil. Plusieurs passkeys par compte, clés physiques acceptées.95- Création de compte par invitation à usage unique (la première est émise par la CLI sur le nœud).96- Sessions : jeton aléatoire, seul son hash est en base ; cookie `__Host-` HttpOnly, Secure, SameSite=Lax ;97 expiration absolue 30 jours, inactivité 14 jours, rotation quotidienne du jeton ; liste et révocation par appareil.98- Réauthentification (passkey, moins de 5 minutes) pour : ajouter ou retirer une passkey, régénérer les codes de99 récupération, gérer le TOTP, administrer les utilisateurs.100- Récupération : 10 codes à usage unique (hachés). Si le TOTP est activé, il est exigé avec le code. Une session101 de récupération ne sert qu'à enrôler une nouvelle passkey ; elle déclenche une notification et un événement d'audit.102- CSRF : toute requête modifiante exige l'en-tête `X-SPD-CSRF` et une origine identique ; pas de CORS.103104## 7. Liens de partage et portails de dépôt105106- Jetons de 256 bits, affichés une seule fois, stockés hachés (SHA-256). Mots de passe hachés avec scrypt.107- Lien de partage : cible fichier ou dossier ; lecture seule ou lecture + téléchargement ; expiration, nombre108 maximal de consultations et de téléchargements, usage unique, révocation immédiate, journal d'accès. Pour un109 dossier, chaque accès vérifie côté serveur que l'élément demandé est un descendant vivant du dossier partagé.110- Portail de dépôt : `/request/<jeton>`, indépendant d'un lien de partage. Le déposant ne reçoit qu'un jeton de111 soumission qui autorise uniquement l'envoi de ses propres fichiers et la lecture de l'état de sa soumission.112 Chaîne : quarantaine → signature/MIME → antivirus → placement atomique dans le dossier cible. Les politiques113 (actif, expiré, quotas) sont réévaluées à chaque bloc reçu et à la finalisation.114- Le dépôt par lien de partage n'existe pas : le dépôt passe toujours par un portail.115116## 8. Aperçus117118| Type | Méthode |119|---|---|120| PDF | pdf.js dans le navigateur (pages, zoom, recherche) |121| JPEG, PNG, WebP, GIF | affichage direct ; vignette par `sips` |122| HEIC | conversion JPEG par `sips` (worker) |123| SVG | `<img>` + réponse `Content-Security-Policy: sandbox` (aucun script ne s'exécute) |124| Vidéo, audio | lecteur natif avec `Range` ; pas de transcodage en V1 |125| Texte, Markdown, CSV, JSON, XML, code | rendu côté client, limite de taille ; Markdown assaini |126| DOCX, PPTX, ODT, ODP | LibreOffice sans interface → PDF, profil jetable, `sandbox-exec` sans réseau, délai maximal |127| XLSX | lecture par le worker → JSON par feuille, avec limites de lignes, colonnes et feuilles |128| ZIP, TAR, TAR.GZ | inventaire des entrées sans extraction, plafonds d'entrées et d'octets lus |129| Autres | message clair + téléchargement |130131Les originaux ne sont jamais modifiés. Les aperçus dérivés sont chiffrés comme les objets.132133## 9. Sécurité134135Voir [`threat-model.md`](threat-model.md). En-têtes : CSP stricte sans script en ligne, `nosniff`,136`frame-ancestors 'none'`, `Referrer-Policy: no-referrer` sur les pages publiques. Les contenus des utilisateurs137sont servis avec `Content-Security-Policy: sandbox; default-src 'none'` et en pièce jointe sauf types sûrs.138139## 10. Exploitation140141Manifeste `mld` : `sp-drive`, `sp-drive-worker`, `sp-drive-clamd`, `sp-drive-freshclam`, `sp-drive-backup`142(nocturne). Santé : `/api/v1/health/live` et `/api/v1/health/ready`. Sauvegarde : `pg_dump` + copie incrémentale143des objets chiffrés ; restauration vérifiée par `scripts/restore-test.sh`. Voir `runbooks/operations.md`.144145## 11. Hors V1146147OCR, recherche plein texte et sémantique, résumés et Q&A ; pilote S3 ; transcodage vidéo ; synchronisation148Finder ou application iOS native ; partage entre comptes internes (espaces communs).149