Revue d'application · Lecture seule
Le site est accessible et fonctionnel — pas de blocage anti-bot, pas de page vide. Un visiteur qui tape l'URL voit une page soignée :
| Page | /auth/signed-out (redirigé depuis /) |
| Titre | "This workspace is private" |
| Message | "Access is by personal invitation. If you are part of the programme team, ask your programme contact for an invitation link." |
| Logos | Flexstone Partners × ComColors (branding soigné) |
| Design | Split-screen : visuel architecture + carte d'invitation. Responsive, dark navy. |
OK Pas de problème de première impression. La page de renvoi est volontairement publique et bien conçue. C'est un message clair et professionnel, pas une page vide ni une erreur.
Ce n'est pas un blocage anti-bot ni une SPA cassée. L'explication :
/auth/signed-out (pages) ou 401 (API).curl https://.../ retourne un 302 Location: /auth/signed-out avec un body vide. Sans -L, on ne voit rien./health, /api, /manifest.json, /robots.txt — tout est intercepté par le Worker et renvoie le HTML "private workspace". Un crawler qui teste des routes génériques ne trouve donc jamais de contenu distinct.robots.txt = Disallow: / + x-robots-tag: noindex, nofollow sur toutes les réponses. Les crawlers bien éduqués s'arrêtent immédiatement.Vérifié par curl -L : la racine renvoie bien la page "private workspace" en 200. Le site fonctionne parfaitement pour un humain qui a un lien d'invitation.
| Stack edge | Cloudflare Workers + D1 (SQLite edge) + custom domain |
| Worker | flexstone-phase1-hub (workers.dev + custom domain) |
| D1 | flexstone-hub (région WEUR) |
| HTTPS | HSTS 1 an, TLS via Cloudflare |
| En-têtes sécu | CSP stricte, X-Frame-Options DENY, nosniff, no-referrer, Permissions-Policy, COOP same-origin |
| Cache | private, no-cache (API : no-store) — pas de cache partagé |
| Frontend | Preact 10.29 + htm 3 (JSX-like sans build) · SPA vanilla, zero framework build step |
| Backend | 1 seul fichier : src/worker.js (1 019 lignes) — Cloudflare Worker |
| Base de données | D1 (SQLite edge) · 5 migrations · pas d'ORM, SQL préparé direct |
| Hébergement | Cloudflare Workers · custom domain oneflex-transformation-hub.com |
| Build | Aucun. JS natif en modules ES. npm run deploy = gates + stamp + wrangler deploy |
| Tests | 0 tests unitaires · Gates : scan confidentialité + lint vues + syntaxe check |
Browser (Preact SPA)
│ fetch() same-origin, cookie __Host-hub_session
▼
Cloudflare Worker (src/worker.js)
│ 1. isPublicPath() ? → servir les assets publics
│ 2. authenticate() → session cookie → SHA-256 → D1 sessions
│ 3. membership check → D1 members table
│ 4. /api/* → handler API (CRUD items, comments, settings, content, admin)
│ 5. sinon → servir le fichier statique depuis public/
▼
D1 (SQLite) — items, comments, users, sessions, invites, members,
settings, activity, content_docs, content_revisions
Pas de sign-in classique (email/mot de passe). L'admin crée un membre → le Worker génère un lien d'invitation à usage unique (token 256 bits, stocké en SHA-256, valide 14 jours) → le membre ouvre le lien → session cookie __Host-hub_session (45 jours, sliding, HttpOnly, Secure, SameSite=Lax).
| Étape | Fichier | Détail |
|---|---|---|
| Invitation | worker.js L831-840 | createInvite() — token aléatoire 32 octets, hash SHA-256 en DB, URL /auth/invite#token |
| Rachat | worker.js L162-194 | redeemInvite() — POST /auth/redeem, race-safe (UPDATE … WHERE used_at IS NULL), crée la session |
| Session | worker.js L144-160 | readSession() — cookie → SHA-256 → lookup D1, sliding expiry (renouvelée si >6h) |
| Auth | worker.js L85-129 | authenticate() — 3 sources : dev (localhost), Cloudflare Access JWT (optionnel, désactivé), session cookie. Puis check membership. |
| Logout | worker.js L196-205 | POST /auth/logout — révoque la session en DB, efface le cookie |
| Page signed-out | public/auth/signed-out.html | Page publique, message "This workspace is private" |
| Page invite | public/auth/invite.html + auth.js | Lit le token du fragment URL (jamais loggé), POST /auth/redeem |
| Gestion membres | worker.js L781-881 | Admin-only : lister, inviter, révoquer, changer rôle. Protection "last admin" et "cannot lock yourself out" |
| Client SPA | public/app/lib/api.js | Chaque write porte x-hub-request: 1 (anti-CSRF). credentials: same-origin |
Option Cloudflare Access (JWT RS256) : le code est présent et complet (vérification de signature, issuer, audience, exp/nbf, cache JWKS 10 min) mais ACCESS_TEAM_DOMAIN et ACCESS_AUD sont vides dans wrangler.jsonc → désactivé en production. L'auth repose 100 % sur le système d'invitations interne.
| Aspect | État |
|---|---|
| Secrets en DB | OK Tokens d'invitation et IDs de session stockés en SHA-256, jamais en clair |
| Cookie session | OK __Host- prefix, HttpOnly, Secure, SameSite=Lax, Path=/ |
| CSRF | OK Header x-hub-request: 1 requis sur tous les writes + vérification Origin si présent |
| XSS | OK CSP stricte (default-src 'self', pas de unsafe-eval), Preact échappe par défaut, cleanText() strippe les contrôle chars |
| SQL injection | OK 100 % de requêtes préparées avec .bind(), aucun string concat |
| Rate limiting | ABSENT Aucun throttle sur /auth/redeem, /api/*, ni sur l'invitation. Voir Risque #1. |
| Confidentialité | OK Double filtre : scan-sensitive.mjs (gate deploy, ~50 patterns bloquants) + SENSITIVE_PATTERNS côté Worker (demande de confirmation à l'éditeur) |
| En-têtes HTTP | OK CSP, HSTS, X-Frame-Options DENY, nosniff, no-referrer, Permissions-Policy, COOP, x-robots-tag |
| Validation input | OK LIMITS constants (title 300, text 120, data 40k, comment 5k), regex email, whitelist de kind/status/org |
| Concurrence | OK Optimistic locking (version sur items), race-safe sur redeem (UPDATE WHERE used_at IS NULL) |
| Backup | OK /api/export (admin) + D1 Time Travel 30 jours |
| Route | Vue | Rôle |
|---|---|---|
/ | Overview | Tableau de bord : where we are, activité récente |
/programme | Programme | Plan Phase 1 — calendrier, semaines, jalons |
/deliverables | Deliverables | Kanban des livrables (drag & drop entre colonnes de statut) |
/interviews | Interviews | Conversations partenaires (à planifier, planifiées, tenues) |
/decision-map | Decision map | Carte des décisions (proposées, décidées, remplacées) |
/actions | Actions | Actions & décisions opérationnelles |
/rhythm | Rhythm | Gouvernance & rythme (notes hebdo partagées, annonces) |
/ways-of-working | Ways | Manières de travailler |
/context | Context | Contexte partagé (documents éditables par admin) |
/roadmap | Roadmap | Roadmap 24 mois |
/people | People | Équipe + accès workspace (admin : inviter, révoquer) |
/library | Library | Méthodes & glossaire |
/activity | Activity | Feed d'activité (créations, modifications, commentaires) |
/content-editor | Editor | Éditeur de contenu (admin uniquement) avec révisions |
/p/:slug | Custom | Pages personnalisées définies dans le contenu (dynamique) |
5 sections de navigation : Where we are · The plan · The work · What we are designing · Background. Command palette (⌘K) pour chercher dans les pages, livrables et documents.
| Tests unitaires | Aucun — pas de framework de test, pas de suite |
| Gates deploy | scan-sensitive.mjs (confidentialité) + lint-views.mjs (structure vues) + node --check (syntaxe tous les JS) |
| État actuel | Gates passants — "Confidentiality scan: clean" · "views lint: clean" |
| Déploiement | npm run deploy → gates → stamp-version.mjs (cache-busting) → wrangler deploy |
| Environnements | Production uniquement (pas de staging). wrangler dev --local pour le dev local. |
| CI/CD | Aucun — pas de GitHub Actions, pas de pipeline automatisé. Deploy manuel via CLI. |
| Git | Repo local, pas de remote configuré. Dernier commit : nav restructure (5 sections). |
1. Pas de rate limiting sur l'auth et l'API
Élevé/auth/redeem n'a aucun throttle. Un attaquant peut tenter des dizaines de milliers de tokens par seconde (le hash SHA-256 rend le brute-force théorique impossible, mais le volume de requêtes peut saturer le Worker et générer du bruit en logs). Idem pour l'API en général.
Ajouter un rate limit simple par IP sur /auth/redeem (ex. 10 tentatives / 5 min) et un limite globale sur l'API. Cloudflare Workers a un r2 ou kv pour compter, ou un header cf-connecting-ip + un Map en mémoire (multi-instance mais suffisant pour ce volume).
2. Aucune batterie de tests — régression silencieuse possible
MoyenLe Worker fait 1 019 lignes de logique critique (auth, CRUD, admin, content) sans aucun test. Les gates vérifient la syntaxe et la confidentialité, pas le comportement. Un bug introduit ne sera détecté que par un utilisateur.
Avant lundi : au minimum 5 tests manuels structurés (invitation → login → CRUD item → commentaire → logout) documentés dans un fichier docs/smoke-test.md. À terme : vitest + miniflare pour les tests du Worker.
3. PWA non installable (manifest.json renvoyé par le Worker)
Moyenmanifest.webmanifest n'est pas dans la liste des chemins publics. Un visiteur qui tente d'installer l'app sur son téléphone reçoit le HTML "private workspace" au lieu du manifest JSON → l'installation PWA échoue silencieusement. Le <link rel="manifest"> dans index.html pointe vers une route protégée.
Soit ajouter /manifest.webmanifest à PUBLIC_EXACT dans le Worker (le manifest ne contient pas de données sensibles), soit retirer le <link rel="manifest"> si la PWA n'est pas un objectif. Actuellement c'est un demi-état qui peut confuser.
4. Single point of failure — un seul admin, pas de remote git
MoyenLe repo n'a pas de remote Git configuré (pas de GitHub/GitLab). Tout le code vit sur cette machine. L'unique admin est timothee.jullien@comcolors.com. Si la machine est perdue ou l'admin indisponible, le workspace est bloqué (le mécanisme d'urgence existe dans DEPLOY.md mais nécessite un accès CLI à l'account Cloudflare).
Pusher le repo sur un remote Git (GitHub privé) avant la livraison. Ajouter un second admin (ex. un contact Flexstone ou Howard) via l'interface People → Workspace access.
5. Pas de staging — les changements vont direct en production
MoyenIl n'y a qu'un seul environnement. Chaque wrangler deploy met à jour la prod. Les migrations D1 s'appliquent directement sur la base de production. Un bug de migration ou un deploy cassé impacte immédiatement tous les utilisateurs connectés.
Pour la livraison, c'est acceptable (l'équipe est petite, le volume faible). À noter dans le runbook : en cas de problème, wrangler rollback + D1 Time Travel (30 jours). Documenter la procédure de rollback dans DEPLOY.md.
npm run gates pour vérifier l'état du CI (passant) sans déclencher de deploy.