add teams

EBO <eric.bouhana@softalys.com> committé le 2026-05-08 14:57

d4d6828cb65fae19b6a70ce6dc6d44a4e9c355fd

1 parent(s)

27 fichiers modifiés +705 -2649
D .specify/memory/constitution.md
+0 -150
@@ -1,150 +0,0 @@
1 -<!--
2 -Sync Impact Report:
3 -- Version change: (none) → 0.5.2 (initial placement)
4 -- Source: Rustwarden/constitution.md — constitution complète importée pour le nouveau projet
5 -- Modified principles: N/A (placement initial)
6 -- Added sections: Constitution complète (Core Principles I–V, Technical Constraints, Development Workflow, Governance)
7 -- Removed sections: None
8 -- Templates requiring updates:
9 - ✅ plan-template.md (Constitution Check / gates — aligné, pas de modification)
10 - ✅ spec-template.md (scope/requirements — aligné, pas de modification)
11 - ✅ tasks-template.md (catégorisation tâches — aligné, pas de modification)
12 - ✅ .cursor/commands/*.md (référence .specify/memory/constitution.md — valide)
13 -- Follow-up TODOs: None
14 --->
15 -
16 -# Rustwarden Constitution
17 -
18 -## Core Principles
19 -
20 -### I. Qualité de Code
21 -
22 -Toutes les fonctions publiques DOIVENT être documentées avec `///` (doc comments Rust). Le code DOIT respecter les bonnes pratiques Rust (clippy warnings = 0). Les structures de données DOIVENT utiliser `#[derive(Debug, Clone)]` où approprié. La gestion d'erreurs DOIT utiliser `Result<T, E>` avec des types d'erreur explicites.
23 -
24 -**Rationale**: La documentation et la qualité du code facilitent la maintenance, la collaboration et réduisent les bugs. Rust offre des outils puissants (clippy, rustfmt) qui doivent être utilisés.
25 -
26 -### II. Architecture Axum et SeaORM
27 -
28 -Toutes les routes HTTP DOIVENT être définies avec Axum uniquement (`axum::Router`). Toutes les interactions avec la base de données DOIVENT passer par SeaORM (aucun raw SQL autorisé). Les migrations DOIVENT être gérées via le système de migrations SeaORM (`migrations/src/`). Le serveur DOIT utiliser Tokio comme runtime async (requis par Axum).
29 -
30 -**Documentation SeaORM** (amendement v0.4.1): Toutes les interactions avec la base de données DOIVENT être implémentées en référence à la [documentation officielle SeaORM](https://www.sea-ql.org/SeaORM/docs/index/). Avant d'implémenter une nouvelle fonctionnalité de base de données (requêtes, relations, transactions, etc.), la documentation SeaORM DOIT être consultée pour garantir l'utilisation des bonnes pratiques et des patterns recommandés. Les exemples de code et les patterns documentés dans la documentation SeaORM DOIVENT être privilégiés par rapport aux approches personnalisées.
31 -
32 -**Implémentation obligatoire des handlers serveur** (amendement v0.5.0): TOUJOURS implémenter les handlers serveur pour que l'API soit fonctionnelle. Les handlers stub (retournant `501 Not Implemented`) sont INTERDITS dans le code de production. Chaque route définie DOIT avoir un handler complet qui :
33 -- Injecte l'état nécessaire (`State<DatabaseConnection>`, `Extension<Claims>`, etc.)
34 -- Utilise les services métier appropriés
35 -- Retourne des réponses fonctionnelles avec données réelles
36 -- Gère les erreurs de manière appropriée
37 -
38 -Les handlers stub sont uniquement autorisés temporairement pendant le développement incrémental, mais DOIVENT être remplacés par des implémentations complètes avant que la feature soit considérée comme terminée. Toute documentation d'API (OpenAPI, guides, exemples) qui référence un endpoint DOIT correspondre à un handler serveur fonctionnel.
39 -
40 -**Rationale**: Axum et SeaORM sont les frameworks standard pour Rust, offrant sécurité, performance et maintenabilité. L'utilisation exclusive de ces outils garantit la cohérence de l'architecture. La documentation officielle SeaORM contient les meilleures pratiques, les patterns recommandés et les solutions aux problèmes courants. En s'y référant systématiquement, on évite les erreurs d'implémentation, on améliore la maintenabilité et on garantit la compatibilité avec les futures versions de SeaORM. L'implémentation complète des handlers serveur est essentielle pour que l'API soit réellement utilisable par les clients. Des handlers stub créent une fausse impression de fonctionnalité et empêchent les tests d'intégration réels. Cette règle garantit que toute documentation d'API correspond à une implémentation fonctionnelle, évitant les frustrations des développeurs clients.
41 -
42 -### III. Développement Incrémental
43 -
44 -Chaque fonctionnalité DOIT être complète et testée avant de passer à la suivante. L'architecture DOIT être modulaire (structure de dossiers claire, séparation des responsabilités). Les user stories DOIVENT être indépendantes et testables individuellement.
45 -
46 -**Rationale**: Le développement incrémental permet de livrer de la valeur rapidement, facilite les tests et réduit la complexité. Chaque étape doit être validée avant de continuer.
47 -
48 -### IV. Sécurité par Défaut
49 -
50 -Tous les secrets DOIVENT être stockés dans des variables d'environnement, JAMAIS dans le code. La validation et la sanitization des entrées utilisateur DOIVENT être effectuées systématiquement. L'authentification et l'autorisation RBAC DOIVENT être implémentées pour toutes les routes protégées.
51 -
52 -**JWT Authentication Standard** (amendement v0.3.1): Le système DOIT extraire et vérifier les JWT tokens depuis le header `Authorization` au format `Bearer <token>`. Cette extraction DOIT être la méthode principale pour l'authentification API. Les cookies HttpOnly peuvent être utilisés comme méthode alternative pour les clients web, mais le header Authorization a priorité pour permettre aux clients API de surcharger l'authentification par cookie.
53 -
54 -**Conformité ANSSI Rust** (amendement v0.4.0): Le code DOIT respecter les recommandations du [Guide ANSSI pour le développement sécurisé en Rust](https://anssi-fr.github.io/rust-guide/). Spécifiquement :
55 -- Le code DOIT compiler avec `#![deny(warnings)]` activé (ou au minimum `#![warn(warnings)]`)
56 -- Toute utilisation de `unsafe` DOIT être justifiée et documentée avec un commentaire `// SAFETY:` expliquant pourquoi `unsafe` est nécessaire et quels invariants doivent être respectés
57 -- Les `unwrap()` et `expect()` sont INTERDITS dans le code de production (sauf dans les tests unitaires). Toutes les erreurs DOIVENT être gérées explicitement avec `Result<T, E>` et l'opérateur `?`
58 -- L'utilisation de `std::mem::uninitialized` est INTERDITE (dépréciée). L'utilisation de `std::mem::MaybeUninit` DOIT être justifiée pour chaque cas d'usage
59 -- Les outils `cargo-audit` et `cargo-outdated` DOIVENT être exécutés régulièrement. Les vulnérabilités détectées DOIVENT être corrigées avant chaque release
60 -- Toutes les considérations de sécurité, invariants et garanties DOIVENT être documentées
61 -
62 -Voir `docs/ANSSI_COMPLIANCE_REPORT.md` pour le rapport de conformité détaillé et les actions prioritaires.
63 -
64 -**Rationale**: La sécurité est critique pour un système cloud. Les secrets dans le code sont une vulnérabilité majeure. La validation des entrées prévient les attaques par injection. L'authentification JWT via header Authorization est le standard de l'industrie pour les APIs REST et permet une authentification stateless sécurisée. Les guidelines ANSSI représentent les meilleures pratiques pour le développement sécurisé en Rust. Leur respect garantit que les garanties de sécurité offertes par Rust ne sont pas invalidées par une utilisation incorrecte du langage.
65 -
66 -### V. Tests et Validation Continue
67 -
68 -Tous les composants DOIVENT avoir des tests unitaires et d'intégration. La couverture de tests DOIT être > 80% (objectif > 90% pour les composants critiques). Les tests DOIVENT être exécutés avant chaque commit (`cargo test`). Les tests DOIVENT suivre les bonnes pratiques Rust (structure `#[cfg(test)]`, tests d'intégration dans `tests/integration/`).
69 -
70 -**Rationale**: Les tests garantissent la qualité et la fiabilité du code. Une couverture élevée réduit les régressions et facilite les refactorings. L'exécution automatique des tests avant commit maintient la qualité du codebase.
71 -
72 -## Technical Constraints
73 -
74 -### Framework Requirements
75 -
76 -- **Rust Version**: Rust 1.91.1+ (Edition 2024) - **IMPORTANT**: Version minimale requise pour Axum/SeaORM
77 -- **Web Framework**: Axum 0.8.7+ (dernière version stable)
78 -- **ORM**: SeaORM 2.0.x+ (requis pour toutes les interactions DB)
79 -- **Runtime**: Tokio 1.x (requis par Axum)
80 -- **Database**: PostgreSQL 18+ (recommandé)
81 -
82 -### Development Environment
83 -
84 -**Docker pour Base de Données** (amendement v0.5.2): En développement, la base de données PostgreSQL DOIT être lancée via Docker depuis le dossier `database/` à la racine du projet. Le développeur DOIT utiliser le fichier `database/docker-compose.yml` pour démarrer la base de données. La base de données est lancée et gérée par le développeur (pas automatiquement par le système). Cette configuration garantit un environnement de développement cohérent et isolé, facilite le partage de configuration entre développeurs, et permet un reset rapide de la base de données pour les tests.
85 -
86 -**Rationale**: L'utilisation de Docker pour PostgreSQL en développement standardise l'environnement de développement, évite les conflits de versions entre développeurs, et simplifie la configuration initiale. Le développeur garde le contrôle sur le démarrage/arrêt de la base de données, permettant une meilleure gestion des ressources et une flexibilité dans le workflow de développement.
87 -
88 -### Code Quality Requirements
89 -
90 -- Toutes les fonctions publiques DOIVENT avoir une documentation `///`
91 -- `cargo clippy -- -D warnings` DOIT passer sans erreur
92 -- `cargo fmt` DOIT être appliqué avant chaque commit
93 -- Les erreurs DOIVENT utiliser des types explicites (`Result<T, AppError>`)
94 -- `#![deny(warnings)]` DOIT être activé dans `src/lib.rs` ou `src/main.rs` (conformité ANSSI)
95 -- Les `unwrap()` et `expect()` sont INTERDITS dans le code de production (conformité ANSSI)
96 -- Toute utilisation de `unsafe` DOIT être documentée avec `// SAFETY:` (conformité ANSSI)
97 -
98 -### Testing Requirements
99 -
100 -- Tests unitaires dans le même fichier avec `#[cfg(test)]` ou dans `tests/unit/`
101 -- Tests d'intégration dans `tests/integration/`
102 -- Tests de contrat dans `tests/contract/`
103 -- Couverture > 80% (objectif > 90% pour auth, security, core services)
104 -
105 -### Security Requirements
106 -
107 -- Pas de secrets hardcodés (variables d'environnement uniquement)
108 -- Validation de toutes les entrées utilisateur
109 -- Hashing des mots de passe (bcrypt, jamais en clair)
110 -- JWT tokens: extraction depuis header `Authorization: Bearer <token>` (priorité 1)
111 -- Cookies HttpOnly pour les clients web (fallback, priorité 2)
112 -- Rate limiting sur les endpoints d'authentification
113 -- Logging de tous les événements d'authentification (succès et échecs)
114 -- Conformité aux guidelines ANSSI Rust (voir `docs/ANSSI_COMPLIANCE_REPORT.md`)
115 -- Code `unsafe` minimisé et documenté avec `// SAFETY:`
116 -- Gestion d'erreurs explicite (pas de panics en production)
117 -
118 -## Development Workflow
119 -
120 -### Code Review Process
121 -
122 -Toutes les pull requests DOIVENT être revues avant merge. Les reviewers DOIVENT vérifier la conformité avec cette constitution. Les violations constitutionnelles DOIVENT être corrigées avant merge.
123 -
124 -### Quality Gates
125 -
126 -- ✅ `cargo build` passe sans erreur
127 -- ✅ `cargo test` passe (tous les tests)
128 -- ✅ `cargo clippy -- -D warnings` passe
129 -- ✅ `cargo fmt --check` passe
130 -- ✅ Couverture de tests > 80%
131 -- ✅ Documentation complète (`cargo doc` génère sans warnings)
132 -- ✅ `cargo audit` ne signale pas de vulnérabilités (avant release, conformité ANSSI)
133 -- ✅ `cargo outdated` a été vérifié (avant release, conformité ANSSI)
134 -- ✅ Aucun `unwrap()`/`expect()` dans le code de production (sauf tests, conformité ANSSI)
135 -- ✅ Tous les blocs `unsafe` sont documentés avec `// SAFETY:` (conformité ANSSI)
136 -
137 -### Testing Strategy
138 -
139 -1. **TDD recommandé**: Écrire les tests avant l'implémentation pour les nouvelles fonctionnalités
140 -2. **Tests unitaires**: Pour chaque fonction publique, tester les cas normaux et d'erreur
141 -3. **Tests d'intégration**: Pour chaque endpoint API, tester les scénarios complets
142 -4. **Tests de contrat**: Pour chaque endpoint public, valider le format de réponse
143 -
144 -## Governance
145 -
146 -Cette constitution prévaut sur toutes les autres pratiques du projet. Les amendements nécessitent une documentation, une approbation et un plan de migration si nécessaire.
147 -
148 -**Version**: 0.5.2 | **Ratified**: 2025-12-09 | **Last Amended**: 2025-03-05
149 -
150 -**Compliance**: Toutes les PRs et reviews DOIVENT vérifier la conformité avec cette constitution. La complexité DOIT être justifiée. Utiliser les templates dans `.specify/templates/` pour la documentation des features.
D .specify/scripts/bash/check-prerequisites.sh
+0 -166
@@ -1,166 +0,0 @@
1 -#!/usr/bin/env bash
2 -
3 -# Consolidated prerequisite checking script
4 -#
5 -# This script provides unified prerequisite checking for Spec-Driven Development workflow.
6 -# It replaces the functionality previously spread across multiple scripts.
7 -#
8 -# Usage: ./check-prerequisites.sh [OPTIONS]
9 -#
10 -# OPTIONS:
11 -# --json Output in JSON format
12 -# --require-tasks Require tasks.md to exist (for implementation phase)
13 -# --include-tasks Include tasks.md in AVAILABLE_DOCS list
14 -# --paths-only Only output path variables (no validation)
15 -# --help, -h Show help message
16 -#
17 -# OUTPUTS:
18 -# JSON mode: {"FEATURE_DIR":"...", "AVAILABLE_DOCS":["..."]}
19 -# Text mode: FEATURE_DIR:... \n AVAILABLE_DOCS: \n ✓/✗ file.md
20 -# Paths only: REPO_ROOT: ... \n BRANCH: ... \n FEATURE_DIR: ... etc.
21 -
22 -set -e
23 -
24 -# Parse command line arguments
25 -JSON_MODE=false
26 -REQUIRE_TASKS=false
27 -INCLUDE_TASKS=false
28 -PATHS_ONLY=false
29 -
30 -for arg in "$@"; do
31 - case "$arg" in
32 - --json)
33 - JSON_MODE=true
34 - ;;
35 - --require-tasks)
36 - REQUIRE_TASKS=true
37 - ;;
38 - --include-tasks)
39 - INCLUDE_TASKS=true
40 - ;;
41 - --paths-only)
42 - PATHS_ONLY=true
43 - ;;
44 - --help|-h)
45 - cat << 'EOF'
46 -Usage: check-prerequisites.sh [OPTIONS]
47 -
48 -Consolidated prerequisite checking for Spec-Driven Development workflow.
49 -
50 -OPTIONS:
51 - --json Output in JSON format
52 - --require-tasks Require tasks.md to exist (for implementation phase)
53 - --include-tasks Include tasks.md in AVAILABLE_DOCS list
54 - --paths-only Only output path variables (no prerequisite validation)
55 - --help, -h Show this help message
56 -
57 -EXAMPLES:
58 - # Check task prerequisites (plan.md required)
59 - ./check-prerequisites.sh --json
60 -
61 - # Check implementation prerequisites (plan.md + tasks.md required)
62 - ./check-prerequisites.sh --json --require-tasks --include-tasks
63 -
64 - # Get feature paths only (no validation)
65 - ./check-prerequisites.sh --paths-only
66 -
67 -EOF
68 - exit 0
69 - ;;
70 - *)
71 - echo "ERROR: Unknown option '$arg'. Use --help for usage information." >&2
72 - exit 1
73 - ;;
74 - esac
75 -done
76 -
77 -# Source common functions
78 -SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
79 -source "$SCRIPT_DIR/common.sh"
80 -
81 -# Get feature paths and validate branch
82 -eval $(get_feature_paths)
83 -check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1
84 -
85 -# If paths-only mode, output paths and exit (support JSON + paths-only combined)
86 -if $PATHS_ONLY; then
87 - if $JSON_MODE; then
88 - # Minimal JSON paths payload (no validation performed)
89 - printf '{"REPO_ROOT":"%s","BRANCH":"%s","FEATURE_DIR":"%s","FEATURE_SPEC":"%s","IMPL_PLAN":"%s","TASKS":"%s"}\n' \
90 - "$REPO_ROOT" "$CURRENT_BRANCH" "$FEATURE_DIR" "$FEATURE_SPEC" "$IMPL_PLAN" "$TASKS"
91 - else
92 - echo "REPO_ROOT: $REPO_ROOT"
93 - echo "BRANCH: $CURRENT_BRANCH"
94 - echo "FEATURE_DIR: $FEATURE_DIR"
95 - echo "FEATURE_SPEC: $FEATURE_SPEC"
96 - echo "IMPL_PLAN: $IMPL_PLAN"
97 - echo "TASKS: $TASKS"
98 - fi
99 - exit 0
100 -fi
101 -
102 -# Validate required directories and files
103 -if [[ ! -d "$FEATURE_DIR" ]]; then
104 - echo "ERROR: Feature directory not found: $FEATURE_DIR" >&2
105 - echo "Run /speckit.specify first to create the feature structure." >&2
106 - exit 1
107 -fi
108 -
109 -if [[ ! -f "$IMPL_PLAN" ]]; then
110 - echo "ERROR: plan.md not found in $FEATURE_DIR" >&2
111 - echo "Run /speckit.plan first to create the implementation plan." >&2
112 - exit 1
113 -fi
114 -
115 -# Check for tasks.md if required
116 -if $REQUIRE_TASKS && [[ ! -f "$TASKS" ]]; then
117 - echo "ERROR: tasks.md not found in $FEATURE_DIR" >&2
118 - echo "Run /speckit.tasks first to create the task list." >&2
119 - exit 1
120 -fi
121 -
122 -# Build list of available documents
123 -docs=()
124 -
125 -# Always check these optional docs
126 -[[ -f "$RESEARCH" ]] && docs+=("research.md")
127 -[[ -f "$DATA_MODEL" ]] && docs+=("data-model.md")
128 -
129 -# Check contracts directory (only if it exists and has files)
130 -if [[ -d "$CONTRACTS_DIR" ]] && [[ -n "$(ls -A "$CONTRACTS_DIR" 2>/dev/null)" ]]; then
131 - docs+=("contracts/")
132 -fi
133 -
134 -[[ -f "$QUICKSTART" ]] && docs+=("quickstart.md")
135 -
136 -# Include tasks.md if requested and it exists
137 -if $INCLUDE_TASKS && [[ -f "$TASKS" ]]; then
138 - docs+=("tasks.md")
139 -fi
140 -
141 -# Output results
142 -if $JSON_MODE; then
143 - # Build JSON array of documents
144 - if [[ ${#docs[@]} -eq 0 ]]; then
145 - json_docs="[]"
146 - else
147 - json_docs=$(printf '"%s",' "${docs[@]}")
148 - json_docs="[${json_docs%,}]"
149 - fi
150 -
151 - printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s}\n' "$FEATURE_DIR" "$json_docs"
152 -else
153 - # Text output
154 - echo "FEATURE_DIR:$FEATURE_DIR"
155 - echo "AVAILABLE_DOCS:"
156 -
157 - # Show status of each potential document
158 - check_file "$RESEARCH" "research.md"
159 - check_file "$DATA_MODEL" "data-model.md"
160 - check_dir "$CONTRACTS_DIR" "contracts/"
161 - check_file "$QUICKSTART" "quickstart.md"
162 -
163 - if $INCLUDE_TASKS; then
164 - check_file "$TASKS" "tasks.md"
165 - fi
166 -fi
D .specify/scripts/bash/common.sh
+0 -156
@@ -1,156 +0,0 @@
1 -#!/usr/bin/env bash
2 -# Common functions and variables for all scripts
3 -
4 -# Get repository root, with fallback for non-git repositories
5 -get_repo_root() {
6 - if git rev-parse --show-toplevel >/dev/null 2>&1; then
7 - git rev-parse --show-toplevel
8 - else
9 - # Fall back to script location for non-git repos
10 - local script_dir="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11 - (cd "$script_dir/../../.." && pwd)
12 - fi
13 -}
14 -
15 -# Get current branch, with fallback for non-git repositories
16 -get_current_branch() {
17 - # First check if SPECIFY_FEATURE environment variable is set
18 - if [[ -n "${SPECIFY_FEATURE:-}" ]]; then
19 - echo "$SPECIFY_FEATURE"
20 - return
21 - fi
22 -
23 - # Then check git if available
24 - if git rev-parse --abbrev-ref HEAD >/dev/null 2>&1; then
25 - git rev-parse --abbrev-ref HEAD
26 - return
27 - fi
28 -
29 - # For non-git repos, try to find the latest feature directory
30 - local repo_root=$(get_repo_root)
31 - local specs_dir="$repo_root/specs"
32 -
33 - if [[ -d "$specs_dir" ]]; then
34 - local latest_feature=""
35 - local highest=0
36 -
37 - for dir in "$specs_dir"/*; do
38 - if [[ -d "$dir" ]]; then
39 - local dirname=$(basename "$dir")
40 - if [[ "$dirname" =~ ^([0-9]{3})- ]]; then
41 - local number=${BASH_REMATCH[1]}
42 - number=$((10#$number))
43 - if [[ "$number" -gt "$highest" ]]; then
44 - highest=$number
45 - latest_feature=$dirname
46 - fi
47 - fi
48 - fi
49 - done
50 -
51 - if [[ -n "$latest_feature" ]]; then
52 - echo "$latest_feature"
53 - return
54 - fi
55 - fi
56 -
57 - echo "main" # Final fallback
58 -}
59 -
60 -# Check if we have git available
61 -has_git() {
62 - git rev-parse --show-toplevel >/dev/null 2>&1
63 -}
64 -
65 -check_feature_branch() {
66 - local branch="$1"
67 - local has_git_repo="$2"
68 -
69 - # For non-git repos, we can't enforce branch naming but still provide output
70 - if [[ "$has_git_repo" != "true" ]]; then
71 - echo "[specify] Warning: Git repository not detected; skipped branch validation" >&2
72 - return 0
73 - fi
74 -
75 - if [[ ! "$branch" =~ ^[0-9]{3}- ]]; then
76 - echo "ERROR: Not on a feature branch. Current branch: $branch" >&2
77 - echo "Feature branches should be named like: 001-feature-name" >&2
78 - return 1
79 - fi
80 -
81 - return 0
82 -}
83 -
84 -get_feature_dir() { echo "$1/specs/$2"; }
85 -
86 -# Find feature directory by numeric prefix instead of exact branch match
87 -# This allows multiple branches to work on the same spec (e.g., 004-fix-bug, 004-add-feature)
88 -find_feature_dir_by_prefix() {
89 - local repo_root="$1"
90 - local branch_name="$2"
91 - local specs_dir="$repo_root/specs"
92 -
93 - # Extract numeric prefix from branch (e.g., "004" from "004-whatever")
94 - if [[ ! "$branch_name" =~ ^([0-9]{3})- ]]; then
95 - # If branch doesn't have numeric prefix, fall back to exact match
96 - echo "$specs_dir/$branch_name"
97 - return
98 - fi
99 -
100 - local prefix="${BASH_REMATCH[1]}"
101 -
102 - # Search for directories in specs/ that start with this prefix
103 - local matches=()
104 - if [[ -d "$specs_dir" ]]; then
105 - for dir in "$specs_dir"/"$prefix"-*; do
106 - if [[ -d "$dir" ]]; then
107 - matches+=("$(basename "$dir")")
108 - fi
109 - done
110 - fi
111 -
112 - # Handle results
113 - if [[ ${#matches[@]} -eq 0 ]]; then
114 - # No match found - return the branch name path (will fail later with clear error)
115 - echo "$specs_dir/$branch_name"
116 - elif [[ ${#matches[@]} -eq 1 ]]; then
117 - # Exactly one match - perfect!
118 - echo "$specs_dir/${matches[0]}"
119 - else
120 - # Multiple matches - this shouldn't happen with proper naming convention
121 - echo "ERROR: Multiple spec directories found with prefix '$prefix': ${matches[*]}" >&2
122 - echo "Please ensure only one spec directory exists per numeric prefix." >&2
123 - echo "$specs_dir/$branch_name" # Return something to avoid breaking the script
124 - fi
125 -}
126 -
127 -get_feature_paths() {
128 - local repo_root=$(get_repo_root)
129 - local current_branch=$(get_current_branch)
130 - local has_git_repo="false"
131 -
132 - if has_git; then
133 - has_git_repo="true"
134 - fi
135 -
136 - # Use prefix-based lookup to support multiple branches per spec
137 - local feature_dir=$(find_feature_dir_by_prefix "$repo_root" "$current_branch")
138 -
139 - cat <<EOF
140 -REPO_ROOT='$repo_root'
141 -CURRENT_BRANCH='$current_branch'
142 -HAS_GIT='$has_git_repo'
143 -FEATURE_DIR='$feature_dir'
144 -FEATURE_SPEC='$feature_dir/spec.md'
145 -IMPL_PLAN='$feature_dir/plan.md'
146 -TASKS='$feature_dir/tasks.md'
147 -RESEARCH='$feature_dir/research.md'
148 -DATA_MODEL='$feature_dir/data-model.md'
149 -QUICKSTART='$feature_dir/quickstart.md'
150 -CONTRACTS_DIR='$feature_dir/contracts'
151 -EOF
152 -}
153 -
154 -check_file() { [[ -f "$1" ]] && echo " ✓ $2" || echo " ✗ $2"; }
155 -check_dir() { [[ -d "$1" && -n $(ls -A "$1" 2>/dev/null) ]] && echo " ✓ $2" || echo " ✗ $2"; }
156 -
D .specify/scripts/bash/create-new-feature.sh
+0 -313
@@ -1,313 +0,0 @@
1 -#!/usr/bin/env bash
2 -
3 -set -e
4 -
5 -JSON_MODE=false
6 -SHORT_NAME=""
7 -BRANCH_NUMBER=""
8 -ARGS=()
9 -i=1
10 -while [ $i -le $# ]; do
11 - arg="${!i}"
12 - case "$arg" in
13 - --json)
14 - JSON_MODE=true
15 - ;;
16 - --short-name)
17 - if [ $((i + 1)) -gt $# ]; then
18 - echo 'Error: --short-name requires a value' >&2
19 - exit 1
20 - fi
21 - i=$((i + 1))
22 - next_arg="${!i}"
23 - # Check if the next argument is another option (starts with --)
24 - if [[ "$next_arg" == --* ]]; then
25 - echo 'Error: --short-name requires a value' >&2
26 - exit 1
27 - fi
28 - SHORT_NAME="$next_arg"
29 - ;;
30 - --number)
31 - if [ $((i + 1)) -gt $# ]; then
32 - echo 'Error: --number requires a value' >&2
33 - exit 1
34 - fi
35 - i=$((i + 1))
36 - next_arg="${!i}"
37 - if [[ "$next_arg" == --* ]]; then
38 - echo 'Error: --number requires a value' >&2
39 - exit 1
40 - fi
41 - BRANCH_NUMBER="$next_arg"
42 - ;;
43 - --help|-h)
44 - echo "Usage: $0 [--json] [--short-name <name>] [--number N] <feature_description>"
45 - echo ""
46 - echo "Options:"
47 - echo " --json Output in JSON format"
48 - echo " --short-name <name> Provide a custom short name (2-4 words) for the branch"
49 - echo " --number N Specify branch number manually (overrides auto-detection)"
50 - echo " --help, -h Show this help message"
51 - echo ""
52 - echo "Examples:"
53 - echo " $0 'Add user authentication system' --short-name 'user-auth'"
54 - echo " $0 'Implement OAuth2 integration for API' --number 5"
55 - exit 0
56 - ;;
57 - *)
58 - ARGS+=("$arg")
59 - ;;
60 - esac
61 - i=$((i + 1))
62 -done
63 -
64 -FEATURE_DESCRIPTION="${ARGS[*]}"
65 -if [ -z "$FEATURE_DESCRIPTION" ]; then
66 - echo "Usage: $0 [--json] [--short-name <name>] [--number N] <feature_description>" >&2
67 - exit 1
68 -fi
69 -
70 -# Trim whitespace and validate description is not empty (e.g., user passed only whitespace)
71 -FEATURE_DESCRIPTION=$(echo "$FEATURE_DESCRIPTION" | xargs)
72 -if [ -z "$FEATURE_DESCRIPTION" ]; then
73 - echo "Error: Feature description cannot be empty or contain only whitespace" >&2
74 - exit 1
75 -fi
76 -
77 -# Function to find the repository root by searching for existing project markers
78 -find_repo_root() {
79 - local dir="$1"
80 - while [ "$dir" != "/" ]; do
81 - if [ -d "$dir/.git" ] || [ -d "$dir/.specify" ]; then
82 - echo "$dir"
83 - return 0
84 - fi
85 - dir="$(dirname "$dir")"
86 - done
87 - return 1
88 -}
89 -
90 -# Function to get highest number from specs directory
91 -get_highest_from_specs() {
92 - local specs_dir="$1"
93 - local highest=0
94 -
95 - if [ -d "$specs_dir" ]; then
96 - for dir in "$specs_dir"/*; do
97 - [ -d "$dir" ] || continue
98 - dirname=$(basename "$dir")
99 - number=$(echo "$dirname" | grep -o '^[0-9]\+' || echo "0")
100 - number=$((10#$number))
101 - if [ "$number" -gt "$highest" ]; then
102 - highest=$number
103 - fi
104 - done
105 - fi
106 -
107 - echo "$highest"
108 -}
109 -
110 -# Function to get highest number from git branches
111 -get_highest_from_branches() {
112 - local highest=0
113 -
114 - # Get all branches (local and remote)
115 - branches=$(git branch -a 2>/dev/null || echo "")
116 -
117 - if [ -n "$branches" ]; then
118 - while IFS= read -r branch; do
119 - # Clean branch name: remove leading markers and remote prefixes
120 - clean_branch=$(echo "$branch" | sed 's/^[* ]*//; s|^remotes/[^/]*/||')
121 -
122 - # Extract feature number if branch matches pattern ###-*
123 - if echo "$clean_branch" | grep -q '^[0-9]\{3\}-'; then
124 - number=$(echo "$clean_branch" | grep -o '^[0-9]\{3\}' || echo "0")
125 - number=$((10#$number))
126 - if [ "$number" -gt "$highest" ]; then
127 - highest=$number
128 - fi
129 - fi
130 - done <<< "$branches"
131 - fi
132 -
133 - echo "$highest"
134 -}
135 -
136 -# Function to check existing branches (local and remote) and return next available number
137 -check_existing_branches() {
138 - local specs_dir="$1"
139 -
140 - # Fetch all remotes to get latest branch info (suppress errors if no remotes)
141 - git fetch --all --prune 2>/dev/null || true
142 -
143 - # Get highest number from ALL branches (not just matching short name)
144 - local highest_branch=$(get_highest_from_branches)
145 -
146 - # Get highest number from ALL specs (not just matching short name)
147 - local highest_spec=$(get_highest_from_specs "$specs_dir")
148 -
149 - # Take the maximum of both
150 - local max_num=$highest_branch
151 - if [ "$highest_spec" -gt "$max_num" ]; then
152 - max_num=$highest_spec
153 - fi
154 -
155 - # Return next number
156 - echo $((max_num + 1))
157 -}
158 -
159 -# Function to clean and format a branch name
160 -clean_branch_name() {
161 - local name="$1"
162 - echo "$name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//'
163 -}
164 -
165 -# Resolve repository root. Prefer git information when available, but fall back
166 -# to searching for repository markers so the workflow still functions in repositories that
167 -# were initialised with --no-git.
168 -SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
169 -
170 -if git rev-parse --show-toplevel >/dev/null 2>&1; then
171 - REPO_ROOT=$(git rev-parse --show-toplevel)
172 - HAS_GIT=true
173 -else
174 - REPO_ROOT="$(find_repo_root "$SCRIPT_DIR")"
175 - if [ -z "$REPO_ROOT" ]; then
176 - echo "Error: Could not determine repository root. Please run this script from within the repository." >&2
177 - exit 1
178 - fi
179 - HAS_GIT=false
180 -fi
181 -
182 -cd "$REPO_ROOT"
183 -
184 -SPECS_DIR="$REPO_ROOT/specs"
185 -mkdir -p "$SPECS_DIR"
186 -
187 -# Function to generate branch name with stop word filtering and length filtering
188 -generate_branch_name() {
189 - local description="$1"
190 -
191 - # Common stop words to filter out
192 - local stop_words="^(i|a|an|the|to|for|of|in|on|at|by|with|from|is|are|was|were|be|been|being|have|has|had|do|does|did|will|would|should|could|can|may|might|must|shall|this|that|these|those|my|your|our|their|want|need|add|get|set)$"
193 -
194 - # Convert to lowercase and split into words
195 - local clean_name=$(echo "$description" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/ /g')
196 -
197 - # Filter words: remove stop words and words shorter than 3 chars (unless they're uppercase acronyms in original)
198 - local meaningful_words=()
199 - for word in $clean_name; do
200 - # Skip empty words
201 - [ -z "$word" ] && continue
202 -
203 - # Keep words that are NOT stop words AND (length >= 3 OR are potential acronyms)
204 - if ! echo "$word" | grep -qiE "$stop_words"; then
205 - if [ ${#word} -ge 3 ]; then
206 - meaningful_words+=("$word")
207 - elif echo "$description" | grep -q "\b${word^^}\b"; then
208 - # Keep short words if they appear as uppercase in original (likely acronyms)
209 - meaningful_words+=("$word")
210 - fi
211 - fi
212 - done
213 -
214 - # If we have meaningful words, use first 3-4 of them
215 - if [ ${#meaningful_words[@]} -gt 0 ]; then
216 - local max_words=3
217 - if [ ${#meaningful_words[@]} -eq 4 ]; then max_words=4; fi
218 -
219 - local result=""
220 - local count=0
221 - for word in "${meaningful_words[@]}"; do
222 - if [ $count -ge $max_words ]; then break; fi
223 - if [ -n "$result" ]; then result="$result-"; fi
224 - result="$result$word"
225 - count=$((count + 1))
226 - done
227 - echo "$result"
228 - else
229 - # Fallback to original logic if no meaningful words found
230 - local cleaned=$(clean_branch_name "$description")
231 - echo "$cleaned" | tr '-' '\n' | grep -v '^$' | head -3 | tr '\n' '-' | sed 's/-$//'
232 - fi
233 -}
234 -
235 -# Generate branch name
236 -if [ -n "$SHORT_NAME" ]; then
237 - # Use provided short name, just clean it up
238 - BRANCH_SUFFIX=$(clean_branch_name "$SHORT_NAME")
239 -else
240 - # Generate from description with smart filtering
241 - BRANCH_SUFFIX=$(generate_branch_name "$FEATURE_DESCRIPTION")
242 -fi
243 -
244 -# Determine branch number
245 -if [ -z "$BRANCH_NUMBER" ]; then
246 - if [ "$HAS_GIT" = true ]; then
247 - # Check existing branches on remotes
248 - BRANCH_NUMBER=$(check_existing_branches "$SPECS_DIR")
249 - else
250 - # Fall back to local directory check
251 - HIGHEST=$(get_highest_from_specs "$SPECS_DIR")
252 - BRANCH_NUMBER=$((HIGHEST + 1))
253 - fi
254 -fi
255 -
256 -# Force base-10 interpretation to prevent octal conversion (e.g., 010 → 8 in octal, but should be 10 in decimal)
257 -FEATURE_NUM=$(printf "%03d" "$((10#$BRANCH_NUMBER))")
258 -BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"
259 -
260 -# GitHub enforces a 244-byte limit on branch names
261 -# Validate and truncate if necessary
262 -MAX_BRANCH_LENGTH=244
263 -if [ ${#BRANCH_NAME} -gt $MAX_BRANCH_LENGTH ]; then
264 - # Calculate how much we need to trim from suffix
265 - # Account for: feature number (3) + hyphen (1) = 4 chars
266 - MAX_SUFFIX_LENGTH=$((MAX_BRANCH_LENGTH - 4))
267 -
268 - # Truncate suffix at word boundary if possible
269 - TRUNCATED_SUFFIX=$(echo "$BRANCH_SUFFIX" | cut -c1-$MAX_SUFFIX_LENGTH)
270 - # Remove trailing hyphen if truncation created one
271 - TRUNCATED_SUFFIX=$(echo "$TRUNCATED_SUFFIX" | sed 's/-$//')
272 -
273 - ORIGINAL_BRANCH_NAME="$BRANCH_NAME"
274 - BRANCH_NAME="${FEATURE_NUM}-${TRUNCATED_SUFFIX}"
275 -
276 - >&2 echo "[specify] Warning: Branch name exceeded GitHub's 244-byte limit"
277 - >&2 echo "[specify] Original: $ORIGINAL_BRANCH_NAME (${#ORIGINAL_BRANCH_NAME} bytes)"
278 - >&2 echo "[specify] Truncated to: $BRANCH_NAME (${#BRANCH_NAME} bytes)"
279 -fi
280 -
281 -if [ "$HAS_GIT" = true ]; then
282 - if ! git checkout -b "$BRANCH_NAME" 2>/dev/null; then
283 - # Check if branch already exists
284 - if git branch --list "$BRANCH_NAME" | grep -q .; then
285 - >&2 echo "Error: Branch '$BRANCH_NAME' already exists. Please use a different feature name or specify a different number with --number."
286 - exit 1
287 - else
288 - >&2 echo "Error: Failed to create git branch '$BRANCH_NAME'. Please check your git configuration and try again."
289 - exit 1
290 - fi
291 - fi
292 -else
293 - >&2 echo "[specify] Warning: Git repository not detected; skipped branch creation for $BRANCH_NAME"
294 -fi
295 -
296 -FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME"
297 -mkdir -p "$FEATURE_DIR"
298 -
299 -TEMPLATE="$REPO_ROOT/.specify/templates/spec-template.md"
300 -SPEC_FILE="$FEATURE_DIR/spec.md"
301 -if [ -f "$TEMPLATE" ]; then cp "$TEMPLATE" "$SPEC_FILE"; else touch "$SPEC_FILE"; fi
302 -
303 -# Set the SPECIFY_FEATURE environment variable for the current session
304 -export SPECIFY_FEATURE="$BRANCH_NAME"
305 -
306 -if $JSON_MODE; then
307 - printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' "$BRANCH_NAME" "$SPEC_FILE" "$FEATURE_NUM"
308 -else
309 - echo "BRANCH_NAME: $BRANCH_NAME"
310 - echo "SPEC_FILE: $SPEC_FILE"
311 - echo "FEATURE_NUM: $FEATURE_NUM"
312 - echo "SPECIFY_FEATURE environment variable set to: $BRANCH_NAME"
313 -fi
D .specify/scripts/bash/setup-plan.sh
+0 -61
@@ -1,61 +0,0 @@
1 -#!/usr/bin/env bash
2 -
3 -set -e
4 -
5 -# Parse command line arguments
6 -JSON_MODE=false
7 -ARGS=()
8 -
9 -for arg in "$@"; do
10 - case "$arg" in
11 - --json)
12 - JSON_MODE=true
13 - ;;
14 - --help|-h)
15 - echo "Usage: $0 [--json]"
16 - echo " --json Output results in JSON format"
17 - echo " --help Show this help message"
18 - exit 0
19 - ;;
20 - *)
21 - ARGS+=("$arg")
22 - ;;
23 - esac
24 -done
25 -
26 -# Get script directory and load common functions
27 -SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
28 -source "$SCRIPT_DIR/common.sh"
29 -
30 -# Get all paths and variables from common functions
31 -eval $(get_feature_paths)
32 -
33 -# Check if we're on a proper feature branch (only for git repos)
34 -check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1
35 -
36 -# Ensure the feature directory exists
37 -mkdir -p "$FEATURE_DIR"
38 -
39 -# Copy plan template if it exists
40 -TEMPLATE="$REPO_ROOT/.specify/templates/plan-template.md"
41 -if [[ -f "$TEMPLATE" ]]; then
42 - cp "$TEMPLATE" "$IMPL_PLAN"
43 - echo "Copied plan template to $IMPL_PLAN"
44 -else
45 - echo "Warning: Plan template not found at $TEMPLATE"
46 - # Create a basic plan file if template doesn't exist
47 - touch "$IMPL_PLAN"
48 -fi
49 -
50 -# Output results
51 -if $JSON_MODE; then
52 - printf '{"FEATURE_SPEC":"%s","IMPL_PLAN":"%s","SPECS_DIR":"%s","BRANCH":"%s","HAS_GIT":"%s"}\n' \
53 - "$FEATURE_SPEC" "$IMPL_PLAN" "$FEATURE_DIR" "$CURRENT_BRANCH" "$HAS_GIT"
54 -else
55 - echo "FEATURE_SPEC: $FEATURE_SPEC"
56 - echo "IMPL_PLAN: $IMPL_PLAN"
57 - echo "SPECS_DIR: $FEATURE_DIR"
58 - echo "BRANCH: $CURRENT_BRANCH"
59 - echo "HAS_GIT: $HAS_GIT"
60 -fi
61 -
D .specify/scripts/bash/update-agent-context.sh
+0 -829
@@ -1,829 +0,0 @@
1 -#!/usr/bin/env bash
2 -
3 -# Update agent context files with information from plan.md
4 -#
5 -# This script maintains AI agent context files by parsing feature specifications
6 -# and updating agent-specific configuration files with project information.
7 -#
8 -# MAIN FUNCTIONS:
9 -# 1. Environment Validation
10 -# - Verifies git repository structure and branch information
11 -# - Checks for required plan.md files and templates
12 -# - Validates file permissions and accessibility
13 -#
14 -# 2. Plan Data Extraction
15 -# - Parses plan.md files to extract project metadata
16 -# - Identifies language/version, frameworks, databases, and project types
17 -# - Handles missing or incomplete specification data gracefully
18 -#
19 -# 3. Agent File Management
20 -# - Creates new agent context files from templates when needed
21 -# - Updates existing agent files with new project information
22 -# - Preserves manual additions and custom configurations
23 -# - Supports multiple AI agent formats and directory structures
24 -#
25 -# 4. Content Generation
26 -# - Generates language-specific build/test commands
27 -# - Creates appropriate project directory structures
28 -# - Updates technology stacks and recent changes sections
29 -# - Maintains consistent formatting and timestamps
30 -#
31 -# 5. Multi-Agent Support
32 -# - Handles agent-specific file paths and naming conventions
33 -# - Supports: Claude, Gemini, Copilot, Cursor, Qwen, opencode, Codex, Windsurf, Kilo Code, Auggie CLI, Roo Code, CodeBuddy CLI, Qoder CLI, Amp, SHAI, Kiro CLI, or Antigravity
34 -# - Can update single agents or all existing agent files
35 -# - Creates default Claude file if no agent files exist
36 -#
37 -# Usage: ./update-agent-context.sh [agent_type]
38 -# Agent types: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|kiro-cli|agy|bob|qodercli
39 -# Leave empty to update all existing agent files
40 -
41 -set -e
42 -
43 -# Enable strict error handling
44 -set -u
45 -set -o pipefail
46 -
47 -#==============================================================================
48 -# Configuration and Global Variables
49 -#==============================================================================
50 -
51 -# Get script directory and load common functions
52 -SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
53 -source "$SCRIPT_DIR/common.sh"
54 -
55 -# Get all paths and variables from common functions
56 -eval $(get_feature_paths)
57 -
58 -NEW_PLAN="$IMPL_PLAN" # Alias for compatibility with existing code
59 -AGENT_TYPE="${1:-}"
60 -
61 -# Agent-specific file paths
62 -CLAUDE_FILE="$REPO_ROOT/CLAUDE.md"
63 -GEMINI_FILE="$REPO_ROOT/GEMINI.md"
64 -COPILOT_FILE="$REPO_ROOT/.github/agents/copilot-instructions.md"
65 -CURSOR_FILE="$REPO_ROOT/.cursor/rules/specify-rules.mdc"
66 -QWEN_FILE="$REPO_ROOT/QWEN.md"
67 -AGENTS_FILE="$REPO_ROOT/AGENTS.md"
68 -WINDSURF_FILE="$REPO_ROOT/.windsurf/rules/specify-rules.md"
69 -KILOCODE_FILE="$REPO_ROOT/.kilocode/rules/specify-rules.md"
70 -AUGGIE_FILE="$REPO_ROOT/.augment/rules/specify-rules.md"
71 -ROO_FILE="$REPO_ROOT/.roo/rules/specify-rules.md"
72 -CODEBUDDY_FILE="$REPO_ROOT/CODEBUDDY.md"
73 -QODER_FILE="$REPO_ROOT/QODER.md"
74 -AMP_FILE="$REPO_ROOT/AGENTS.md"
75 -SHAI_FILE="$REPO_ROOT/SHAI.md"
76 -KIRO_FILE="$REPO_ROOT/AGENTS.md"
77 -AGY_FILE="$REPO_ROOT/.agent/rules/specify-rules.md"
78 -BOB_FILE="$REPO_ROOT/AGENTS.md"
79 -
80 -# Template file
81 -TEMPLATE_FILE="$REPO_ROOT/.specify/templates/agent-file-template.md"
82 -
83 -# Global variables for parsed plan data
84 -NEW_LANG=""
85 -NEW_FRAMEWORK=""
86 -NEW_DB=""
87 -NEW_PROJECT_TYPE=""
88 -
89 -#==============================================================================
90 -# Utility Functions
91 -#==============================================================================
92 -
93 -log_info() {
94 - echo "INFO: $1"
95 -}
96 -
97 -log_success() {
98 - echo "✓ $1"
99 -}
100 -
101 -log_error() {
102 - echo "ERROR: $1" >&2
103 -}
104 -
105 -log_warning() {
106 - echo "WARNING: $1" >&2
107 -}
108 -
109 -# Cleanup function for temporary files
110 -cleanup() {
111 - local exit_code=$?
112 - rm -f /tmp/agent_update_*_$$
113 - rm -f /tmp/manual_additions_$$
114 - exit $exit_code
115 -}
116 -
117 -# Set up cleanup trap
118 -trap cleanup EXIT INT TERM
119 -
120 -#==============================================================================
121 -# Validation Functions
122 -#==============================================================================
123 -
124 -validate_environment() {
125 - # Check if we have a current branch/feature (git or non-git)
126 - if [[ -z "$CURRENT_BRANCH" ]]; then
127 - log_error "Unable to determine current feature"
128 - if [[ "$HAS_GIT" == "true" ]]; then
129 - log_info "Make sure you're on a feature branch"
130 - else
131 - log_info "Set SPECIFY_FEATURE environment variable or create a feature first"
132 - fi
133 - exit 1
134 - fi
135 -
136 - # Check if plan.md exists
137 - if [[ ! -f "$NEW_PLAN" ]]; then
138 - log_error "No plan.md found at $NEW_PLAN"
139 - log_info "Make sure you're working on a feature with a corresponding spec directory"
140 - if [[ "$HAS_GIT" != "true" ]]; then
141 - log_info "Use: export SPECIFY_FEATURE=your-feature-name or create a new feature first"
142 - fi
143 - exit 1
144 - fi
145 -
146 - # Check if template exists (needed for new files)
147 - if [[ ! -f "$TEMPLATE_FILE" ]]; then
148 - log_warning "Template file not found at $TEMPLATE_FILE"
149 - log_warning "Creating new agent files will fail"
150 - fi
151 -}
152 -
153 -#==============================================================================
154 -# Plan Parsing Functions
155 -#==============================================================================
156 -
157 -extract_plan_field() {
158 - local field_pattern="$1"
159 - local plan_file="$2"
160 -
161 - grep "^\*\*${field_pattern}\*\*: " "$plan_file" 2>/dev/null | \
162 - head -1 | \
163 - sed "s|^\*\*${field_pattern}\*\*: ||" | \
164 - sed 's/^[ \t]*//;s/[ \t]*$//' | \
165 - grep -v "NEEDS CLARIFICATION" | \
166 - grep -v "^N/A$" || echo ""
167 -}
168 -
169 -parse_plan_data() {
170 - local plan_file="$1"
171 -
172 - if [[ ! -f "$plan_file" ]]; then
173 - log_error "Plan file not found: $plan_file"
174 - return 1
175 - fi
176 -
177 - if [[ ! -r "$plan_file" ]]; then
178 - log_error "Plan file is not readable: $plan_file"
179 - return 1
180 - fi
181 -
182 - log_info "Parsing plan data from $plan_file"
183 -
184 - NEW_LANG=$(extract_plan_field "Language/Version" "$plan_file")
185 - NEW_FRAMEWORK=$(extract_plan_field "Primary Dependencies" "$plan_file")
186 - NEW_DB=$(extract_plan_field "Storage" "$plan_file")
187 - NEW_PROJECT_TYPE=$(extract_plan_field "Project Type" "$plan_file")
188 -
189 - # Log what we found
190 - if [[ -n "$NEW_LANG" ]]; then
191 - log_info "Found language: $NEW_LANG"
192 - else
193 - log_warning "No language information found in plan"
194 - fi
195 -
196 - if [[ -n "$NEW_FRAMEWORK" ]]; then
197 - log_info "Found framework: $NEW_FRAMEWORK"
198 - fi
199 -
200 - if [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]]; then
201 - log_info "Found database: $NEW_DB"
202 - fi
203 -
204 - if [[ -n "$NEW_PROJECT_TYPE" ]]; then
205 - log_info "Found project type: $NEW_PROJECT_TYPE"
206 - fi
207 -}
208 -
209 -format_technology_stack() {
210 - local lang="$1"
211 - local framework="$2"
212 - local parts=()
213 -
214 - # Add non-empty parts
215 - [[ -n "$lang" && "$lang" != "NEEDS CLARIFICATION" ]] && parts+=("$lang")
216 - [[ -n "$framework" && "$framework" != "NEEDS CLARIFICATION" && "$framework" != "N/A" ]] && parts+=("$framework")
217 -
218 - # Join with proper formatting
219 - if [[ ${#parts[@]} -eq 0 ]]; then
220 - echo ""
221 - elif [[ ${#parts[@]} -eq 1 ]]; then
222 - echo "${parts[0]}"
223 - else
224 - # Join multiple parts with " + "
225 - local result="${parts[0]}"
226 - for ((i=1; i<${#parts[@]}; i++)); do
227 - result="$result + ${parts[i]}"
228 - done
229 - echo "$result"
230 - fi
231 -}
232 -
233 -#==============================================================================
234 -# Template and Content Generation Functions
235 -#==============================================================================
236 -
237 -get_project_structure() {
238 - local project_type="$1"
239 -
240 - if [[ "$project_type" == *"web"* ]]; then
241 - echo "backend/\\nfrontend/\\ntests/"
242 - else
243 - echo "src/\\ntests/"
244 - fi
245 -}
246 -
247 -get_commands_for_language() {
248 - local lang="$1"
249 -
250 - case "$lang" in
251 - *"Python"*)
252 - echo "cd src && pytest && ruff check ."
253 - ;;
254 - *"Rust"*)
255 - echo "cargo test && cargo clippy"
256 - ;;
257 - *"JavaScript"*|*"TypeScript"*)
258 - echo "npm test \\&\\& npm run lint"
259 - ;;
260 - *)
261 - echo "# Add commands for $lang"
262 - ;;
263 - esac
264 -}
265 -
266 -get_language_conventions() {
267 - local lang="$1"
268 - echo "$lang: Follow standard conventions"
269 -}
270 -
271 -create_new_agent_file() {
272 - local target_file="$1"
273 - local temp_file="$2"
274 - local project_name="$3"
275 - local current_date="$4"
276 -
277 - if [[ ! -f "$TEMPLATE_FILE" ]]; then
278 - log_error "Template not found at $TEMPLATE_FILE"
279 - return 1
280 - fi
281 -
282 - if [[ ! -r "$TEMPLATE_FILE" ]]; then
283 - log_error "Template file is not readable: $TEMPLATE_FILE"
284 - return 1
285 - fi
286 -
287 - log_info "Creating new agent context file from template..."
288 -
289 - if ! cp "$TEMPLATE_FILE" "$temp_file"; then
290 - log_error "Failed to copy template file"
291 - return 1
292 - fi
293 -
294 - # Replace template placeholders
295 - local project_structure
296 - project_structure=$(get_project_structure "$NEW_PROJECT_TYPE")
297 -
298 - local commands
299 - commands=$(get_commands_for_language "$NEW_LANG")
300 -
301 - local language_conventions
302 - language_conventions=$(get_language_conventions "$NEW_LANG")
303 -
304 - # Perform substitutions with error checking using safer approach
305 - # Escape special characters for sed by using a different delimiter or escaping
306 - local escaped_lang=$(printf '%s\n' "$NEW_LANG" | sed 's/[\[\.*^$()+{}|]/\\&/g')
307 - local escaped_framework=$(printf '%s\n' "$NEW_FRAMEWORK" | sed 's/[\[\.*^$()+{}|]/\\&/g')
308 - local escaped_branch=$(printf '%s\n' "$CURRENT_BRANCH" | sed 's/[\[\.*^$()+{}|]/\\&/g')
309 -
310 - # Build technology stack and recent change strings conditionally
311 - local tech_stack
312 - if [[ -n "$escaped_lang" && -n "$escaped_framework" ]]; then
313 - tech_stack="- $escaped_lang + $escaped_framework ($escaped_branch)"
314 - elif [[ -n "$escaped_lang" ]]; then
315 - tech_stack="- $escaped_lang ($escaped_branch)"
316 - elif [[ -n "$escaped_framework" ]]; then
317 - tech_stack="- $escaped_framework ($escaped_branch)"
318 - else
319 - tech_stack="- ($escaped_branch)"
320 - fi
321 -
322 - local recent_change
323 - if [[ -n "$escaped_lang" && -n "$escaped_framework" ]]; then
324 - recent_change="- $escaped_branch: Added $escaped_lang + $escaped_framework"
325 - elif [[ -n "$escaped_lang" ]]; then
326 - recent_change="- $escaped_branch: Added $escaped_lang"
327 - elif [[ -n "$escaped_framework" ]]; then
328 - recent_change="- $escaped_branch: Added $escaped_framework"
329 - else
330 - recent_change="- $escaped_branch: Added"
331 - fi
332 -
333 - local substitutions=(
334 - "s|\[PROJECT NAME\]|$project_name|"
335 - "s|\[DATE\]|$current_date|"
336 - "s|\[EXTRACTED FROM ALL PLAN.MD FILES\]|$tech_stack|"
337 - "s|\[ACTUAL STRUCTURE FROM PLANS\]|$project_structure|g"
338 - "s|\[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES\]|$commands|"
339 - "s|\[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE\]|$language_conventions|"
340 - "s|\[LAST 3 FEATURES AND WHAT THEY ADDED\]|$recent_change|"
341 - )
342 -
343 - for substitution in "${substitutions[@]}"; do
344 - if ! sed -i.bak -e "$substitution" "$temp_file"; then
345 - log_error "Failed to perform substitution: $substitution"
346 - rm -f "$temp_file" "$temp_file.bak"
347 - return 1
348 - fi
349 - done
350 -
351 - # Convert \n sequences to actual newlines
352 - newline=$(printf '\n')
353 - sed -i.bak2 "s/\\\\n/${newline}/g" "$temp_file"
354 -
355 - # Clean up backup files
356 - rm -f "$temp_file.bak" "$temp_file.bak2"
357 -
358 - # Prepend Cursor frontmatter for .mdc files so rules are auto-included
359 - if [[ "$target_file" == *.mdc ]]; then
360 - local frontmatter_file
361 - frontmatter_file=$(mktemp) || return 1
362 - printf '%s\n' "---" "description: Project Development Guidelines" "globs: [\"**/*\"]" "alwaysApply: true" "---" "" > "$frontmatter_file"
363 - cat "$temp_file" >> "$frontmatter_file"
364 - mv "$frontmatter_file" "$temp_file"
365 - fi
366 -
367 - return 0
368 -}
369 -
370 -
371 -
372 -
373 -update_existing_agent_file() {
374 - local target_file="$1"
375 - local current_date="$2"
376 -
377 - log_info "Updating existing agent context file..."
378 -
379 - # Use a single temporary file for atomic update
380 - local temp_file
381 - temp_file=$(mktemp) || {
382 - log_error "Failed to create temporary file"
383 - return 1
384 - }
385 -
386 - # Process the file in one pass
387 - local tech_stack=$(format_technology_stack "$NEW_LANG" "$NEW_FRAMEWORK")
388 - local new_tech_entries=()
389 - local new_change_entry=""
390 -
391 - # Prepare new technology entries
392 - if [[ -n "$tech_stack" ]] && ! grep -q "$tech_stack" "$target_file"; then
393 - new_tech_entries+=("- $tech_stack ($CURRENT_BRANCH)")
394 - fi
395 -
396 - if [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]] && [[ "$NEW_DB" != "NEEDS CLARIFICATION" ]] && ! grep -q "$NEW_DB" "$target_file"; then
397 - new_tech_entries+=("- $NEW_DB ($CURRENT_BRANCH)")
398 - fi
399 -
400 - # Prepare new change entry
401 - if [[ -n "$tech_stack" ]]; then
402 - new_change_entry="- $CURRENT_BRANCH: Added $tech_stack"
403 - elif [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]] && [[ "$NEW_DB" != "NEEDS CLARIFICATION" ]]; then
404 - new_change_entry="- $CURRENT_BRANCH: Added $NEW_DB"
405 - fi
406 -
407 - # Check if sections exist in the file
408 - local has_active_technologies=0
409 - local has_recent_changes=0
410 -
411 - if grep -q "^## Active Technologies" "$target_file" 2>/dev/null; then
412 - has_active_technologies=1
413 - fi
414 -
415 - if grep -q "^## Recent Changes" "$target_file" 2>/dev/null; then
416 - has_recent_changes=1
417 - fi
418 -
419 - # Process file line by line
420 - local in_tech_section=false
421 - local in_changes_section=false
422 - local tech_entries_added=false
423 - local changes_entries_added=false
424 - local existing_changes_count=0
425 - local file_ended=false
426 -
427 - while IFS= read -r line || [[ -n "$line" ]]; do
428 - # Handle Active Technologies section
429 - if [[ "$line" == "## Active Technologies" ]]; then
430 - echo "$line" >> "$temp_file"
431 - in_tech_section=true
432 - continue
433 - elif [[ $in_tech_section == true ]] && [[ "$line" =~ ^##[[:space:]] ]]; then
434 - # Add new tech entries before closing the section
435 - if [[ $tech_entries_added == false ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then
436 - printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file"
437 - tech_entries_added=true
438 - fi
439 - echo "$line" >> "$temp_file"
440 - in_tech_section=false
441 - continue
442 - elif [[ $in_tech_section == true ]] && [[ -z "$line" ]]; then
443 - # Add new tech entries before empty line in tech section
444 - if [[ $tech_entries_added == false ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then
445 - printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file"
446 - tech_entries_added=true
447 - fi
448 - echo "$line" >> "$temp_file"
449 - continue
450 - fi
451 -
452 - # Handle Recent Changes section
453 - if [[ "$line" == "## Recent Changes" ]]; then
454 - echo "$line" >> "$temp_file"
455 - # Add new change entry right after the heading
456 - if [[ -n "$new_change_entry" ]]; then
457 - echo "$new_change_entry" >> "$temp_file"
458 - fi
459 - in_changes_section=true
460 - changes_entries_added=true
461 - continue
462 - elif [[ $in_changes_section == true ]] && [[ "$line" =~ ^##[[:space:]] ]]; then
463 - echo "$line" >> "$temp_file"
464 - in_changes_section=false
465 - continue
466 - elif [[ $in_changes_section == true ]] && [[ "$line" == "- "* ]]; then
467 - # Keep only first 2 existing changes
468 - if [[ $existing_changes_count -lt 2 ]]; then
469 - echo "$line" >> "$temp_file"
470 - ((existing_changes_count++))
471 - fi
472 - continue
473 - fi
474 -
475 - # Update timestamp
476 - if [[ "$line" =~ \*\*Last\ updated\*\*:.*[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] ]]; then
477 - echo "$line" | sed "s/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/$current_date/" >> "$temp_file"
478 - else
479 - echo "$line" >> "$temp_file"
480 - fi
481 - done < "$target_file"
482 -
483 - # Post-loop check: if we're still in the Active Technologies section and haven't added new entries
484 - if [[ $in_tech_section == true ]] && [[ $tech_entries_added == false ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then
485 - printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file"
486 - tech_entries_added=true
487 - fi
488 -
489 - # If sections don't exist, add them at the end of the file
490 - if [[ $has_active_technologies -eq 0 ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then
491 - echo "" >> "$temp_file"
492 - echo "## Active Technologies" >> "$temp_file"
493 - printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file"
494 - tech_entries_added=true
495 - fi
496 -
497 - if [[ $has_recent_changes -eq 0 ]] && [[ -n "$new_change_entry" ]]; then
498 - echo "" >> "$temp_file"
499 - echo "## Recent Changes" >> "$temp_file"
500 - echo "$new_change_entry" >> "$temp_file"
501 - changes_entries_added=true
502 - fi
503 -
504 - # Ensure Cursor .mdc files have YAML frontmatter for auto-inclusion
505 - if [[ "$target_file" == *.mdc ]]; then
506 - if ! head -1 "$temp_file" | grep -q '^---'; then
507 - local frontmatter_file
508 - frontmatter_file=$(mktemp) || { rm -f "$temp_file"; return 1; }
509 - printf '%s\n' "---" "description: Project Development Guidelines" "globs: [\"**/*\"]" "alwaysApply: true" "---" "" > "$frontmatter_file"
510 - cat "$temp_file" >> "$frontmatter_file"
511 - mv "$frontmatter_file" "$temp_file"
512 - fi
513 - fi
514 -
515 - # Move temp file to target atomically
516 - if ! mv "$temp_file" "$target_file"; then
517 - log_error "Failed to update target file"
518 - rm -f "$temp_file"
519 - return 1
520 - fi
521 -
522 - return 0
523 -}
524 -#==============================================================================
525 -# Main Agent File Update Function
526 -#==============================================================================
527 -
528 -update_agent_file() {
529 - local target_file="$1"
530 - local agent_name="$2"
531 -
532 - if [[ -z "$target_file" ]] || [[ -z "$agent_name" ]]; then
533 - log_error "update_agent_file requires target_file and agent_name parameters"
534 - return 1
535 - fi
536 -
537 - log_info "Updating $agent_name context file: $target_file"
538 -
539 - local project_name
540 - project_name=$(basename "$REPO_ROOT")
541 - local current_date
542 - current_date=$(date +%Y-%m-%d)
543 -
544 - # Create directory if it doesn't exist
545 - local target_dir
546 - target_dir=$(dirname "$target_file")
547 - if [[ ! -d "$target_dir" ]]; then
548 - if ! mkdir -p "$target_dir"; then
549 - log_error "Failed to create directory: $target_dir"
550 - return 1
551 - fi
552 - fi
553 -
554 - if [[ ! -f "$target_file" ]]; then
555 - # Create new file from template
556 - local temp_file
557 - temp_file=$(mktemp) || {
558 - log_error "Failed to create temporary file"
559 - return 1
560 - }
561 -
562 - if create_new_agent_file "$target_file" "$temp_file" "$project_name" "$current_date"; then
563 - if mv "$temp_file" "$target_file"; then
564 - log_success "Created new $agent_name context file"
565 - else
566 - log_error "Failed to move temporary file to $target_file"
567 - rm -f "$temp_file"
568 - return 1
569 - fi
570 - else
571 - log_error "Failed to create new agent file"
572 - rm -f "$temp_file"
573 - return 1
574 - fi
575 - else
576 - # Update existing file
577 - if [[ ! -r "$target_file" ]]; then
578 - log_error "Cannot read existing file: $target_file"
579 - return 1
580 - fi
581 -
582 - if [[ ! -w "$target_file" ]]; then
583 - log_error "Cannot write to existing file: $target_file"
584 - return 1
585 - fi
586 -
587 - if update_existing_agent_file "$target_file" "$current_date"; then
588 - log_success "Updated existing $agent_name context file"
589 - else
590 - log_error "Failed to update existing agent file"
591 - return 1
592 - fi
593 - fi
594 -
595 - return 0
596 -}
597 -
598 -#==============================================================================
599 -# Agent Selection and Processing
600 -#==============================================================================
601 -
602 -update_specific_agent() {
603 - local agent_type="$1"
604 -
605 - case "$agent_type" in
606 - claude)
607 - update_agent_file "$CLAUDE_FILE" "Claude Code"
608 - ;;
609 - gemini)
610 - update_agent_file "$GEMINI_FILE" "Gemini CLI"
611 - ;;
612 - copilot)
613 - update_agent_file "$COPILOT_FILE" "GitHub Copilot"
614 - ;;
615 - cursor-agent)
616 - update_agent_file "$CURSOR_FILE" "Cursor IDE"
617 - ;;
618 - qwen)
619 - update_agent_file "$QWEN_FILE" "Qwen Code"
620 - ;;
621 - opencode)
622 - update_agent_file "$AGENTS_FILE" "opencode"
623 - ;;
624 - codex)
625 - update_agent_file "$AGENTS_FILE" "Codex CLI"
626 - ;;
627 - windsurf)
628 - update_agent_file "$WINDSURF_FILE" "Windsurf"
629 - ;;
630 - kilocode)
631 - update_agent_file "$KILOCODE_FILE" "Kilo Code"
632 - ;;
633 - auggie)
634 - update_agent_file "$AUGGIE_FILE" "Auggie CLI"
635 - ;;
636 - roo)
637 - update_agent_file "$ROO_FILE" "Roo Code"
638 - ;;
639 - codebuddy)
640 - update_agent_file "$CODEBUDDY_FILE" "CodeBuddy CLI"
641 - ;;
642 - qodercli)
643 - update_agent_file "$QODER_FILE" "Qoder CLI"
644 - ;;
645 - amp)
646 - update_agent_file "$AMP_FILE" "Amp"
647 - ;;
648 - shai)
649 - update_agent_file "$SHAI_FILE" "SHAI"
650 - ;;
651 - kiro-cli)
652 - update_agent_file "$KIRO_FILE" "Kiro CLI"
653 - ;;
654 - agy)
655 - update_agent_file "$AGY_FILE" "Antigravity"
656 - ;;
657 - bob)
658 - update_agent_file "$BOB_FILE" "IBM Bob"
659 - ;;
660 - generic)
661 - log_info "Generic agent: no predefined context file. Use the agent-specific update script for your agent."
662 - ;;
663 - *)
664 - log_error "Unknown agent type '$agent_type'"
665 - log_error "Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|kiro-cli|agy|bob|qodercli|generic"
666 - exit 1
667 - ;;
668 - esac
669 -}
670 -
671 -update_all_existing_agents() {
672 - local found_agent=false
673 -
674 - # Check each possible agent file and update if it exists
675 - if [[ -f "$CLAUDE_FILE" ]]; then
676 - update_agent_file "$CLAUDE_FILE" "Claude Code"
677 - found_agent=true
678 - fi
679 -
680 - if [[ -f "$GEMINI_FILE" ]]; then
681 - update_agent_file "$GEMINI_FILE" "Gemini CLI"
682 - found_agent=true
683 - fi
684 -
685 - if [[ -f "$COPILOT_FILE" ]]; then
686 - update_agent_file "$COPILOT_FILE" "GitHub Copilot"
687 - found_agent=true
688 - fi
689 -
690 - if [[ -f "$CURSOR_FILE" ]]; then
691 - update_agent_file "$CURSOR_FILE" "Cursor IDE"
692 - found_agent=true
693 - fi
694 -
695 - if [[ -f "$QWEN_FILE" ]]; then
696 - update_agent_file "$QWEN_FILE" "Qwen Code"
697 - found_agent=true
698 - fi
699 -
700 - if [[ -f "$AGENTS_FILE" ]]; then
701 - update_agent_file "$AGENTS_FILE" "Codex/opencode"
702 - found_agent=true
703 - fi
704 -
705 - if [[ -f "$WINDSURF_FILE" ]]; then
706 - update_agent_file "$WINDSURF_FILE" "Windsurf"
707 - found_agent=true
708 - fi
709 -
710 - if [[ -f "$KILOCODE_FILE" ]]; then
711 - update_agent_file "$KILOCODE_FILE" "Kilo Code"
712 - found_agent=true
713 - fi
714 -
715 - if [[ -f "$AUGGIE_FILE" ]]; then
716 - update_agent_file "$AUGGIE_FILE" "Auggie CLI"
717 - found_agent=true
718 - fi
719 -
720 - if [[ -f "$ROO_FILE" ]]; then
721 - update_agent_file "$ROO_FILE" "Roo Code"
722 - found_agent=true
723 - fi
724 -
725 - if [[ -f "$CODEBUDDY_FILE" ]]; then
726 - update_agent_file "$CODEBUDDY_FILE" "CodeBuddy CLI"
727 - found_agent=true
728 - fi
729 -
730 - if [[ -f "$SHAI_FILE" ]]; then
731 - update_agent_file "$SHAI_FILE" "SHAI"
732 - found_agent=true
733 - fi
734 -
735 - if [[ -f "$QODER_FILE" ]]; then
736 - update_agent_file "$QODER_FILE" "Qoder CLI"
737 - found_agent=true
738 - fi
739 -
740 - if [[ -f "$KIRO_FILE" ]]; then
741 - update_agent_file "$KIRO_FILE" "Kiro CLI"
742 - found_agent=true
743 - fi
744 -
745 - if [[ -f "$AGY_FILE" ]]; then
746 - update_agent_file "$AGY_FILE" "Antigravity"
747 - found_agent=true
748 - fi
749 - if [[ -f "$BOB_FILE" ]]; then
750 - update_agent_file "$BOB_FILE" "IBM Bob"
751 - found_agent=true
752 - fi
753 -
754 - # If no agent files exist, create a default Claude file
755 - if [[ "$found_agent" == false ]]; then
756 - log_info "No existing agent files found, creating default Claude file..."
757 - update_agent_file "$CLAUDE_FILE" "Claude Code"
758 - fi
759 -}
760 -print_summary() {
761 - echo
762 - log_info "Summary of changes:"
763 -
764 - if [[ -n "$NEW_LANG" ]]; then
765 - echo " - Added language: $NEW_LANG"
766 - fi
767 -
768 - if [[ -n "$NEW_FRAMEWORK" ]]; then
769 - echo " - Added framework: $NEW_FRAMEWORK"
770 - fi
771 -
772 - if [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]]; then
773 - echo " - Added database: $NEW_DB"
774 - fi
775 -
776 - echo
777 -
778 - log_info "Usage: $0 [claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|kiro-cli|agy|bob|qodercli]"
779 -}
780 -
781 -#==============================================================================
782 -# Main Execution
783 -#==============================================================================
784 -
785 -main() {
786 - # Validate environment before proceeding
787 - validate_environment
788 -
789 - log_info "=== Updating agent context files for feature $CURRENT_BRANCH ==="
790 -
791 - # Parse the plan file to extract project information
792 - if ! parse_plan_data "$NEW_PLAN"; then
793 - log_error "Failed to parse plan data"
794 - exit 1
795 - fi
796 -
797 - # Process based on agent type argument
798 - local success=true
799 -
800 - if [[ -z "$AGENT_TYPE" ]]; then
801 - # No specific agent provided - update all existing agent files
802 - log_info "No agent specified, updating all existing agent files..."
803 - if ! update_all_existing_agents; then
804 - success=false
805 - fi
806 - else
807 - # Specific agent provided - update only that agent
808 - log_info "Updating specific agent: $AGENT_TYPE"
809 - if ! update_specific_agent "$AGENT_TYPE"; then
810 - success=false
811 - fi
812 - fi
813 -
814 - # Print summary
815 - print_summary
816 -
817 - if [[ "$success" == true ]]; then
818 - log_success "Agent context update completed successfully"
819 - exit 0
820 - else
821 - log_error "Agent context update completed with errors"
822 - exit 1
823 - fi
824 -}
825 -
826 -# Execute main function if script is run directly
827 -if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
828 - main "$@"
829 -fi
D .specify/templates/agent-file-template.md
+0 -28
@@ -1,28 +0,0 @@
1 -# [PROJECT NAME] Development Guidelines
2 -
3 -Auto-generated from all feature plans. Last updated: [DATE]
4 -
5 -## Active Technologies
6 -
7 -[EXTRACTED FROM ALL PLAN.MD FILES]
8 -
9 -## Project Structure
10 -
11 -```text
12 -[ACTUAL STRUCTURE FROM PLANS]
13 -```
14 -
15 -## Commands
16 -
17 -[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES]
18 -
19 -## Code Style
20 -
21 -[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE]
22 -
23 -## Recent Changes
24 -
25 -[LAST 3 FEATURES AND WHAT THEY ADDED]
26 -
27 -<!-- MANUAL ADDITIONS START -->
28 -<!-- MANUAL ADDITIONS END -->
D .specify/templates/checklist-template.md
+0 -40
@@ -1,40 +0,0 @@
1 -# [CHECKLIST TYPE] Checklist: [FEATURE NAME]
2 -
3 -**Purpose**: [Brief description of what this checklist covers]
4 -**Created**: [DATE]
5 -**Feature**: [Link to spec.md or relevant documentation]
6 -
7 -**Note**: This checklist is generated by the `/speckit.checklist` command based on feature context and requirements.
8 -
9 -<!--
10 - ============================================================================
11 - IMPORTANT: The checklist items below are SAMPLE ITEMS for illustration only.
12 -
13 - The /speckit.checklist command MUST replace these with actual items based on:
14 - - User's specific checklist request
15 - - Feature requirements from spec.md
16 - - Technical context from plan.md
17 - - Implementation details from tasks.md
18 -
19 - DO NOT keep these sample items in the generated checklist file.
20 - ============================================================================
21 --->
22 -
23 -## [Category 1]
24 -
25 -- [ ] CHK001 First checklist item with clear action
26 -- [ ] CHK002 Second checklist item
27 -- [ ] CHK003 Third checklist item
28 -
29 -## [Category 2]
30 -
31 -- [ ] CHK004 Another category item
32 -- [ ] CHK005 Item with specific criteria
33 -- [ ] CHK006 Final item in this category
34 -
35 -## Notes
36 -
37 -- Check items off as completed: `[x]`
38 -- Add comments or findings inline
39 -- Link to relevant resources or documentation
40 -- Items are numbered sequentially for easy reference
D .specify/templates/constitution-template.md
+0 -50
@@ -1,50 +0,0 @@
1 -# [PROJECT_NAME] Constitution
2 -<!-- Example: Spec Constitution, TaskFlow Constitution, etc. -->
3 -
4 -## Core Principles
5 -
6 -### [PRINCIPLE_1_NAME]
7 -<!-- Example: I. Library-First -->
8 -[PRINCIPLE_1_DESCRIPTION]
9 -<!-- Example: Every feature starts as a standalone library; Libraries must be self-contained, independently testable, documented; Clear purpose required - no organizational-only libraries -->
10 -
11 -### [PRINCIPLE_2_NAME]
12 -<!-- Example: II. CLI Interface -->
13 -[PRINCIPLE_2_DESCRIPTION]
14 -<!-- Example: Every library exposes functionality via CLI; Text in/out protocol: stdin/args → stdout, errors → stderr; Support JSON + human-readable formats -->
15 -
16 -### [PRINCIPLE_3_NAME]
17 -<!-- Example: III. Test-First (NON-NEGOTIABLE) -->
18 -[PRINCIPLE_3_DESCRIPTION]
19 -<!-- Example: TDD mandatory: Tests written → User approved → Tests fail → Then implement; Red-Green-Refactor cycle strictly enforced -->
20 -
21 -### [PRINCIPLE_4_NAME]
22 -<!-- Example: IV. Integration Testing -->
23 -[PRINCIPLE_4_DESCRIPTION]
24 -<!-- Example: Focus areas requiring integration tests: New library contract tests, Contract changes, Inter-service communication, Shared schemas -->
25 -
26 -### [PRINCIPLE_5_NAME]
27 -<!-- Example: V. Observability, VI. Versioning & Breaking Changes, VII. Simplicity -->
28 -[PRINCIPLE_5_DESCRIPTION]
29 -<!-- Example: Text I/O ensures debuggability; Structured logging required; Or: MAJOR.MINOR.BUILD format; Or: Start simple, YAGNI principles -->
30 -
31 -## [SECTION_2_NAME]
32 -<!-- Example: Additional Constraints, Security Requirements, Performance Standards, etc. -->
33 -
34 -[SECTION_2_CONTENT]
35 -<!-- Example: Technology stack requirements, compliance standards, deployment policies, etc. -->
36 -
37 -## [SECTION_3_NAME]
38 -<!-- Example: Development Workflow, Review Process, Quality Gates, etc. -->
39 -
40 -[SECTION_3_CONTENT]
41 -<!-- Example: Code review requirements, testing gates, deployment approval process, etc. -->
42 -
43 -## Governance
44 -<!-- Example: Constitution supersedes all other practices; Amendments require documentation, approval, migration plan -->
45 -
46 -[GOVERNANCE_RULES]
47 -<!-- Example: All PRs/reviews must verify compliance; Complexity must be justified; Use [GUIDANCE_FILE] for runtime development guidance -->
48 -
49 -**Version**: [CONSTITUTION_VERSION] | **Ratified**: [RATIFICATION_DATE] | **Last Amended**: [LAST_AMENDED_DATE]
50 -<!-- Example: Version: 2.1.1 | Ratified: 2025-06-13 | Last Amended: 2025-07-16 -->
D .specify/templates/plan-template.md
+0 -104
@@ -1,104 +0,0 @@
1 -# Implementation Plan: [FEATURE]
2 -
3 -**Branch**: `[###-feature-name]` | **Date**: [DATE] | **Spec**: [link]
4 -**Input**: Feature specification from `/specs/[###-feature-name]/spec.md`
5 -
6 -**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/plan-template.md` for the execution workflow.
7 -
8 -## Summary
9 -
10 -[Extract from feature spec: primary requirement + technical approach from research]
11 -
12 -## Technical Context
13 -
14 -<!--
15 - ACTION REQUIRED: Replace the content in this section with the technical details
16 - for the project. The structure here is presented in advisory capacity to guide
17 - the iteration process.
18 --->
19 -
20 -**Language/Version**: [e.g., Python 3.11, Swift 5.9, Rust 1.75 or NEEDS CLARIFICATION]
21 -**Primary Dependencies**: [e.g., FastAPI, UIKit, LLVM or NEEDS CLARIFICATION]
22 -**Storage**: [if applicable, e.g., PostgreSQL, CoreData, files or N/A]
23 -**Testing**: [e.g., pytest, XCTest, cargo test or NEEDS CLARIFICATION]
24 -**Target Platform**: [e.g., Linux server, iOS 15+, WASM or NEEDS CLARIFICATION]
25 -**Project Type**: [e.g., library/cli/web-service/mobile-app/compiler/desktop-app or NEEDS CLARIFICATION]
26 -**Performance Goals**: [domain-specific, e.g., 1000 req/s, 10k lines/sec, 60 fps or NEEDS CLARIFICATION]
27 -**Constraints**: [domain-specific, e.g., <200ms p95, <100MB memory, offline-capable or NEEDS CLARIFICATION]
28 -**Scale/Scope**: [domain-specific, e.g., 10k users, 1M LOC, 50 screens or NEEDS CLARIFICATION]
29 -
30 -## Constitution Check
31 -
32 -*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
33 -
34 -[Gates determined based on constitution file]
35 -
36 -## Project Structure
37 -
38 -### Documentation (this feature)
39 -
40 -```text
41 -specs/[###-feature]/
42 -├── plan.md # This file (/speckit.plan command output)
43 -├── research.md # Phase 0 output (/speckit.plan command)
44 -├── data-model.md # Phase 1 output (/speckit.plan command)
45 -├── quickstart.md # Phase 1 output (/speckit.plan command)
46 -├── contracts/ # Phase 1 output (/speckit.plan command)
47 -└── tasks.md # Phase 2 output (/speckit.tasks command - NOT created by /speckit.plan)
48 -```
49 -
50 -### Source Code (repository root)
51 -<!--
52 - ACTION REQUIRED: Replace the placeholder tree below with the concrete layout
53 - for this feature. Delete unused options and expand the chosen structure with
54 - real paths (e.g., apps/admin, packages/something). The delivered plan must
55 - not include Option labels.
56 --->
57 -
58 -```text
59 -# [REMOVE IF UNUSED] Option 1: Single project (DEFAULT)
60 -src/
61 -├── models/
62 -├── services/
63 -├── cli/
64 -└── lib/
65 -
66 -tests/
67 -├── contract/
68 -├── integration/
69 -└── unit/
70 -
71 -# [REMOVE IF UNUSED] Option 2: Web application (when "frontend" + "backend" detected)
72 -backend/
73 -├── src/
74 -│ ├── models/
75 -│ ├── services/
76 -│ └── api/
77 -└── tests/
78 -
79 -frontend/
80 -├── src/
81 -│ ├── components/
82 -│ ├── pages/
83 -│ └── services/
84 -└── tests/
85 -
86 -# [REMOVE IF UNUSED] Option 3: Mobile + API (when "iOS/Android" detected)
87 -api/
88 -└── [same as backend above]
89 -
90 -ios/ or android/
91 -└── [platform-specific structure: feature modules, UI flows, platform tests]
92 -```
93 -
94 -**Structure Decision**: [Document the selected structure and reference the real
95 -directories captured above]
96 -
97 -## Complexity Tracking
98 -
99 -> **Fill ONLY if Constitution Check has violations that must be justified**
100 -
101 -| Violation | Why Needed | Simpler Alternative Rejected Because |
102 -|-----------|------------|-------------------------------------|
103 -| [e.g., 4th project] | [current need] | [why 3 projects insufficient] |
104 -| [e.g., Repository pattern] | [specific problem] | [why direct DB access insufficient] |
D .specify/templates/spec-template.md
+0 -115
@@ -1,115 +0,0 @@
1 -# Feature Specification: [FEATURE NAME]
2 -
3 -**Feature Branch**: `[###-feature-name]`
4 -**Created**: [DATE]
5 -**Status**: Draft
6 -**Input**: User description: "$ARGUMENTS"
7 -
8 -## User Scenarios & Testing *(mandatory)*
9 -
10 -<!--
11 - IMPORTANT: User stories should be PRIORITIZED as user journeys ordered by importance.
12 - Each user story/journey must be INDEPENDENTLY TESTABLE - meaning if you implement just ONE of them,
13 - you should still have a viable MVP (Minimum Viable Product) that delivers value.
14 -
15 - Assign priorities (P1, P2, P3, etc.) to each story, where P1 is the most critical.
16 - Think of each story as a standalone slice of functionality that can be:
17 - - Developed independently
18 - - Tested independently
19 - - Deployed independently
20 - - Demonstrated to users independently
21 --->
22 -
23 -### User Story 1 - [Brief Title] (Priority: P1)
24 -
25 -[Describe this user journey in plain language]
26 -
27 -**Why this priority**: [Explain the value and why it has this priority level]
28 -
29 -**Independent Test**: [Describe how this can be tested independently - e.g., "Can be fully tested by [specific action] and delivers [specific value]"]
30 -
31 -**Acceptance Scenarios**:
32 -
33 -1. **Given** [initial state], **When** [action], **Then** [expected outcome]
34 -2. **Given** [initial state], **When** [action], **Then** [expected outcome]
35 -
36 ----
37 -
38 -### User Story 2 - [Brief Title] (Priority: P2)
39 -
40 -[Describe this user journey in plain language]
41 -
42 -**Why this priority**: [Explain the value and why it has this priority level]
43 -
44 -**Independent Test**: [Describe how this can be tested independently]
45 -
46 -**Acceptance Scenarios**:
47 -
48 -1. **Given** [initial state], **When** [action], **Then** [expected outcome]
49 -
50 ----
51 -
52 -### User Story 3 - [Brief Title] (Priority: P3)
53 -
54 -[Describe this user journey in plain language]
55 -
56 -**Why this priority**: [Explain the value and why it has this priority level]
57 -
58 -**Independent Test**: [Describe how this can be tested independently]
59 -
60 -**Acceptance Scenarios**:
61 -
62 -1. **Given** [initial state], **When** [action], **Then** [expected outcome]
63 -
64 ----
65 -
66 -[Add more user stories as needed, each with an assigned priority]
67 -
68 -### Edge Cases
69 -
70 -<!--
71 - ACTION REQUIRED: The content in this section represents placeholders.
72 - Fill them out with the right edge cases.
73 --->
74 -
75 -- What happens when [boundary condition]?
76 -- How does system handle [error scenario]?
77 -
78 -## Requirements *(mandatory)*
79 -
80 -<!--
81 - ACTION REQUIRED: The content in this section represents placeholders.
82 - Fill them out with the right functional requirements.
83 --->
84 -
85 -### Functional Requirements
86 -
87 -- **FR-001**: System MUST [specific capability, e.g., "allow users to create accounts"]
88 -- **FR-002**: System MUST [specific capability, e.g., "validate email addresses"]
89 -- **FR-003**: Users MUST be able to [key interaction, e.g., "reset their password"]
90 -- **FR-004**: System MUST [data requirement, e.g., "persist user preferences"]
91 -- **FR-005**: System MUST [behavior, e.g., "log all security events"]
92 -
93 -*Example of marking unclear requirements:*
94 -
95 -- **FR-006**: System MUST authenticate users via [NEEDS CLARIFICATION: auth method not specified - email/password, SSO, OAuth?]
96 -- **FR-007**: System MUST retain user data for [NEEDS CLARIFICATION: retention period not specified]
97 -
98 -### Key Entities *(include if feature involves data)*
99 -
100 -- **[Entity 1]**: [What it represents, key attributes without implementation]
101 -- **[Entity 2]**: [What it represents, relationships to other entities]
102 -
103 -## Success Criteria *(mandatory)*
104 -
105 -<!--
106 - ACTION REQUIRED: Define measurable success criteria.
107 - These must be technology-agnostic and measurable.
108 --->
109 -
110 -### Measurable Outcomes
111 -
112 -- **SC-001**: [Measurable metric, e.g., "Users can complete account creation in under 2 minutes"]
113 -- **SC-002**: [Measurable metric, e.g., "System handles 1000 concurrent users without degradation"]
114 -- **SC-003**: [User satisfaction metric, e.g., "90% of users successfully complete primary task on first attempt"]
115 -- **SC-004**: [Business metric, e.g., "Reduce support tickets related to [X] by 50%"]
D .specify/templates/tasks-template.md
+0 -251
@@ -1,251 +0,0 @@
1 ----
2 -
3 -description: "Task list template for feature implementation"
4 ----
5 -
6 -# Tasks: [FEATURE NAME]
7 -
8 -**Input**: Design documents from `/specs/[###-feature-name]/`
9 -**Prerequisites**: plan.md (required), spec.md (required for user stories), research.md, data-model.md, contracts/
10 -
11 -**Tests**: The examples below include test tasks. Tests are OPTIONAL - only include them if explicitly requested in the feature specification.
12 -
13 -**Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story.
14 -
15 -## Format: `[ID] [P?] [Story] Description`
16 -
17 -- **[P]**: Can run in parallel (different files, no dependencies)
18 -- **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3)
19 -- Include exact file paths in descriptions
20 -
21 -## Path Conventions
22 -
23 -- **Single project**: `src/`, `tests/` at repository root
24 -- **Web app**: `backend/src/`, `frontend/src/`
25 -- **Mobile**: `api/src/`, `ios/src/` or `android/src/`
26 -- Paths shown below assume single project - adjust based on plan.md structure
27 -
28 -<!--
29 - ============================================================================
30 - IMPORTANT: The tasks below are SAMPLE TASKS for illustration purposes only.
31 -
32 - The /speckit.tasks command MUST replace these with actual tasks based on:
33 - - User stories from spec.md (with their priorities P1, P2, P3...)
34 - - Feature requirements from plan.md
35 - - Entities from data-model.md
36 - - Endpoints from contracts/
37 -
38 - Tasks MUST be organized by user story so each story can be:
39 - - Implemented independently
40 - - Tested independently
41 - - Delivered as an MVP increment
42 -
43 - DO NOT keep these sample tasks in the generated tasks.md file.
44 - ============================================================================
45 --->
46 -
47 -## Phase 1: Setup (Shared Infrastructure)
48 -
49 -**Purpose**: Project initialization and basic structure
50 -
51 -- [ ] T001 Create project structure per implementation plan
52 -- [ ] T002 Initialize [language] project with [framework] dependencies
53 -- [ ] T003 [P] Configure linting and formatting tools
54 -
55 ----
56 -
57 -## Phase 2: Foundational (Blocking Prerequisites)
58 -
59 -**Purpose**: Core infrastructure that MUST be complete before ANY user story can be implemented
60 -
61 -**⚠️ CRITICAL**: No user story work can begin until this phase is complete
62 -
63 -Examples of foundational tasks (adjust based on your project):
64 -
65 -- [ ] T004 Setup database schema and migrations framework
66 -- [ ] T005 [P] Implement authentication/authorization framework
67 -- [ ] T006 [P] Setup API routing and middleware structure
68 -- [ ] T007 Create base models/entities that all stories depend on
69 -- [ ] T008 Configure error handling and logging infrastructure
70 -- [ ] T009 Setup environment configuration management
71 -
72 -**Checkpoint**: Foundation ready - user story implementation can now begin in parallel
73 -
74 ----
75 -
76 -## Phase 3: User Story 1 - [Title] (Priority: P1) 🎯 MVP
77 -
78 -**Goal**: [Brief description of what this story delivers]
79 -
80 -**Independent Test**: [How to verify this story works on its own]
81 -
82 -### Tests for User Story 1 (OPTIONAL - only if tests requested) ⚠️
83 -
84 -> **NOTE: Write these tests FIRST, ensure they FAIL before implementation**
85 -
86 -- [ ] T010 [P] [US1] Contract test for [endpoint] in tests/contract/test_[name].py
87 -- [ ] T011 [P] [US1] Integration test for [user journey] in tests/integration/test_[name].py
88 -
89 -### Implementation for User Story 1
90 -
91 -- [ ] T012 [P] [US1] Create [Entity1] model in src/models/[entity1].py
92 -- [ ] T013 [P] [US1] Create [Entity2] model in src/models/[entity2].py
93 -- [ ] T014 [US1] Implement [Service] in src/services/[service].py (depends on T012, T013)
94 -- [ ] T015 [US1] Implement [endpoint/feature] in src/[location]/[file].py
95 -- [ ] T016 [US1] Add validation and error handling
96 -- [ ] T017 [US1] Add logging for user story 1 operations
97 -
98 -**Checkpoint**: At this point, User Story 1 should be fully functional and testable independently
99 -
100 ----
101 -
102 -## Phase 4: User Story 2 - [Title] (Priority: P2)
103 -
104 -**Goal**: [Brief description of what this story delivers]
105 -
106 -**Independent Test**: [How to verify this story works on its own]
107 -
108 -### Tests for User Story 2 (OPTIONAL - only if tests requested) ⚠️
109 -
110 -- [ ] T018 [P] [US2] Contract test for [endpoint] in tests/contract/test_[name].py
111 -- [ ] T019 [P] [US2] Integration test for [user journey] in tests/integration/test_[name].py
112 -
113 -### Implementation for User Story 2
114 -
115 -- [ ] T020 [P] [US2] Create [Entity] model in src/models/[entity].py
116 -- [ ] T021 [US2] Implement [Service] in src/services/[service].py
117 -- [ ] T022 [US2] Implement [endpoint/feature] in src/[location]/[file].py
118 -- [ ] T023 [US2] Integrate with User Story 1 components (if needed)
119 -
120 -**Checkpoint**: At this point, User Stories 1 AND 2 should both work independently
121 -
122 ----
123 -
124 -## Phase 5: User Story 3 - [Title] (Priority: P3)
125 -
126 -**Goal**: [Brief description of what this story delivers]
127 -
128 -**Independent Test**: [How to verify this story works on its own]
129 -
130 -### Tests for User Story 3 (OPTIONAL - only if tests requested) ⚠️
131 -
132 -- [ ] T024 [P] [US3] Contract test for [endpoint] in tests/contract/test_[name].py
133 -- [ ] T025 [P] [US3] Integration test for [user journey] in tests/integration/test_[name].py
134 -
135 -### Implementation for User Story 3
136 -
137 -- [ ] T026 [P] [US3] Create [Entity] model in src/models/[entity].py
138 -- [ ] T027 [US3] Implement [Service] in src/services/[service].py
139 -- [ ] T028 [US3] Implement [endpoint/feature] in src/[location]/[file].py
140 -
141 -**Checkpoint**: All user stories should now be independently functional
142 -
143 ----
144 -
145 -[Add more user story phases as needed, following the same pattern]
146 -
147 ----
148 -
149 -## Phase N: Polish & Cross-Cutting Concerns
150 -
151 -**Purpose**: Improvements that affect multiple user stories
152 -
153 -- [ ] TXXX [P] Documentation updates in docs/
154 -- [ ] TXXX Code cleanup and refactoring
155 -- [ ] TXXX Performance optimization across all stories
156 -- [ ] TXXX [P] Additional unit tests (if requested) in tests/unit/
157 -- [ ] TXXX Security hardening
158 -- [ ] TXXX Run quickstart.md validation
159 -
160 ----
161 -
162 -## Dependencies & Execution Order
163 -
164 -### Phase Dependencies
165 -
166 -- **Setup (Phase 1)**: No dependencies - can start immediately
167 -- **Foundational (Phase 2)**: Depends on Setup completion - BLOCKS all user stories
168 -- **User Stories (Phase 3+)**: All depend on Foundational phase completion
169 - - User stories can then proceed in parallel (if staffed)
170 - - Or sequentially in priority order (P1 → P2 → P3)
171 -- **Polish (Final Phase)**: Depends on all desired user stories being complete
172 -
173 -### User Story Dependencies
174 -
175 -- **User Story 1 (P1)**: Can start after Foundational (Phase 2) - No dependencies on other stories
176 -- **User Story 2 (P2)**: Can start after Foundational (Phase 2) - May integrate with US1 but should be independently testable
177 -- **User Story 3 (P3)**: Can start after Foundational (Phase 2) - May integrate with US1/US2 but should be independently testable
178 -
179 -### Within Each User Story
180 -
181 -- Tests (if included) MUST be written and FAIL before implementation
182 -- Models before services
183 -- Services before endpoints
184 -- Core implementation before integration
185 -- Story complete before moving to next priority
186 -
187 -### Parallel Opportunities
188 -
189 -- All Setup tasks marked [P] can run in parallel
190 -- All Foundational tasks marked [P] can run in parallel (within Phase 2)
191 -- Once Foundational phase completes, all user stories can start in parallel (if team capacity allows)
192 -- All tests for a user story marked [P] can run in parallel
193 -- Models within a story marked [P] can run in parallel
194 -- Different user stories can be worked on in parallel by different team members
195 -
196 ----
197 -
198 -## Parallel Example: User Story 1
199 -
200 -```bash
201 -# Launch all tests for User Story 1 together (if tests requested):
202 -Task: "Contract test for [endpoint] in tests/contract/test_[name].py"
203 -Task: "Integration test for [user journey] in tests/integration/test_[name].py"
204 -
205 -# Launch all models for User Story 1 together:
206 -Task: "Create [Entity1] model in src/models/[entity1].py"
207 -Task: "Create [Entity2] model in src/models/[entity2].py"
208 -```
209 -
210 ----
211 -
212 -## Implementation Strategy
213 -
214 -### MVP First (User Story 1 Only)
215 -
216 -1. Complete Phase 1: Setup
217 -2. Complete Phase 2: Foundational (CRITICAL - blocks all stories)
218 -3. Complete Phase 3: User Story 1
219 -4. **STOP and VALIDATE**: Test User Story 1 independently
220 -5. Deploy/demo if ready
221 -
222 -### Incremental Delivery
223 -
224 -1. Complete Setup + Foundational → Foundation ready
225 -2. Add User Story 1 → Test independently → Deploy/Demo (MVP!)
226 -3. Add User Story 2 → Test independently → Deploy/Demo
227 -4. Add User Story 3 → Test independently → Deploy/Demo
228 -5. Each story adds value without breaking previous stories
229 -
230 -### Parallel Team Strategy
231 -
232 -With multiple developers:
233 -
234 -1. Team completes Setup + Foundational together
235 -2. Once Foundational is done:
236 - - Developer A: User Story 1
237 - - Developer B: User Story 2
238 - - Developer C: User Story 3
239 -3. Stories complete and integrate independently
240 -
241 ----
242 -
243 -## Notes
244 -
245 -- [P] tasks = different files, no dependencies
246 -- [Story] label maps task to specific user story for traceability
247 -- Each user story should be independently completable and testable
248 -- Verify tests fail before implementing
249 -- Commit after each task or logical group
250 -- Stop at any checkpoint to validate story independently
251 -- Avoid: vague tasks, same file conflicts, cross-story dependencies that break independence
M Cargo.lock
+3 -2
@@ -3648,9 +3648,9 @@ dependencies = [
3648 3648
3649 3649 [[package]]
3650 3650 name = "rustls-webpki"
3651 -version = "0.103.9"
3651 +version = "0.103.13"
3652 3652 source = "registry+https://github.com/rust-lang/crates.io-index"
3653 -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53"
3653 +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
3654 3654 dependencies = [
3655 3655 "ring",
3656 3656 "rustls-pki-types",
@@ -3731,6 +3731,7 @@ dependencies = [
3731 3731 "tokio-stream",
3732 3732 "totp-rs",
3733 3733 "tower",
3734 + "tower-http",
3734 3735 "tower-sessions",
3735 3736 "tower-sessions-redis-store",
3736 3737 "tracing",
M crates/rustwarden-core/Cargo.toml
+1 -0
@@ -17,6 +17,7 @@ serde = { version = "1.0", features = ["derive"] }
17 17 serde_json = "1.0"
18 18 # Middleware
19 19 tower = { version = "0.5", features = ["limit", "util"] }
20 +tower-http = { version = "0.6", features = ["cors"] }
20 21 # Logging
21 22 tracing = "0.1"
22 23 tracing-subscriber = { version = "0.3", features = ["env-filter"] }
M crates/rustwarden-core/src/app.rs
+57 -1
@@ -20,7 +20,7 @@
20 20
21 21 use axum::{
22 22 extract::Request,
23 - http::{header::HeaderName, HeaderValue},
23 + http::{header::HeaderName, HeaderValue, Method},
24 24 middleware::Next,
25 25 response::Response,
26 26 Router,
@@ -29,6 +29,7 @@ use sea_orm::DatabaseConnection;
29 29 use std::path::PathBuf;
30 30 use std::sync::Arc;
31 31 use tower::ServiceBuilder;
32 +use tower_http::cors::{AllowOrigin, Any, CorsLayer};
32 33
33 34 use crate::auth::session::SessionConfig;
34 35 use crate::config::auth::AuthConfig;
@@ -424,8 +425,63 @@ impl RustwardenBuilder {
424 425 let db_for_ext = db.clone();
425 426 let i18n_for_app = i18n_service.clone(); // Retained for getter access after build
426 427 let auth_config_for_app = auth_config.clone(); // Retained for getter access after build
428 +
429 + let origins_str = std::env::var("CORS_ALLOWED_ORIGINS").unwrap_or_default();
430 + let cors_layer = if origins_str == "*" {
431 + tracing::warn!("CORS_ALLOWED_ORIGINS=* allows any origin. Use only in development.");
432 + CorsLayer::new()
433 + .allow_origin(Any)
434 + .allow_methods([
435 + Method::GET,
436 + Method::POST,
437 + Method::PUT,
438 + Method::DELETE,
439 + Method::PATCH,
440 + ])
441 + .allow_headers([
442 + axum::http::header::CONTENT_TYPE,
443 + axum::http::header::AUTHORIZATION,
444 + HeaderName::from_static("x-csrf-token"),
445 + ])
446 + .max_age(std::time::Duration::from_secs(3600))
447 + } else if !origins_str.is_empty() {
448 + let origins: Vec<axum::http::HeaderValue> = origins_str
449 + .split(',')
450 + .filter_map(|s| s.trim().parse().ok())
451 + .collect();
452 + if origins.is_empty() {
453 + tracing::warn!("CORS_ALLOWED_ORIGINS set but no valid origins parsed");
454 + CorsLayer::new().allow_origin(AllowOrigin::list(
455 + std::iter::empty::<axum::http::HeaderValue>(),
456 + ))
457 + } else {
458 + tracing::info!("CORS configured for {} origins", origins.len());
459 + CorsLayer::new()
460 + .allow_origin(AllowOrigin::list(origins))
461 + .allow_methods([
462 + Method::GET,
463 + Method::POST,
464 + Method::PUT,
465 + Method::DELETE,
466 + Method::PATCH,
467 + ])
468 + .allow_headers([
469 + axum::http::header::CONTENT_TYPE,
470 + axum::http::header::AUTHORIZATION,
471 + HeaderName::from_static("x-csrf-token"),
472 + ])
473 + .allow_credentials(true)
474 + .max_age(std::time::Duration::from_secs(3600))
475 + }
476 + } else {
477 + CorsLayer::new().allow_origin(AllowOrigin::list(
478 + std::iter::empty::<axum::http::HeaderValue>(),
479 + ))
480 + };
481 +
427 482 let app = router.with_state(db).layer(
428 483 ServiceBuilder::new()
484 + .layer(cors_layer)
429 485 .layer(axum::middleware::from_fn(security_headers_middleware))
430 486 .layer_fn(move |service| AdminService::new(service, db_for_admin.clone()))
431 487 .layer_fn(move |service| AuthService::new(service, db_for_auth.clone()))
M crates/rustwarden-core/src/auth/handlers.rs
+4 -21
@@ -286,6 +286,7 @@ pub async fn login(
286 286 State(db): State<DatabaseConnection>,
287 287 headers: HeaderMap,
288 288 _user_session: Session,
289 + Extension(auth_config): Extension<Arc<AuthConfig>>,
289 290 hooks: Option<Extension<Arc<dyn RustwardenHooks>>>,
290 291 Json(payload): Json<LoginRequest>,
291 292 ) -> Result<Response, AppError> {
@@ -383,12 +384,6 @@ pub async fn login(
383 384 permissions
384 385 );
385 386
386 - // Load auth configuration
387 - let auth_config = AuthConfig::from_env().map_err(|e| {
388 - tracing::error!("Failed to load auth config: {}", e);
389 - AppError::Config(format!("Auth configuration error: {}", e))
390 - })?;
391 -
392 387 // Generate JWT token
393 388 let access_token = JwtService::generate_token(
394 389 &auth_config,
@@ -537,6 +532,7 @@ pub struct Verify2faRequest {
537 532 pub async fn verify_2fa(
538 533 State(db): State<DatabaseConnection>,
539 534 _user_session: Session,
535 + Extension(auth_config): Extension<Arc<AuthConfig>>,
540 536 hooks: Option<Extension<Arc<dyn RustwardenHooks>>>,
541 537 Json(payload): Json<Verify2faRequest>,
542 538 ) -> Result<Response, AppError> {
@@ -578,10 +574,6 @@ pub async fn verify_2fa(
578 574 )
579 575 .await?;
580 576
581 - // Load auth config and generate tokens
582 - let auth_config = AuthConfig::from_env()
583 - .map_err(|e| AppError::Config(format!("Auth configuration error: {}", e)))?;
584 -
585 577 let access_token = JwtService::generate_token(
586 578 &auth_config,
587 579 user.id,
@@ -798,6 +790,7 @@ pub async fn logout(
798 790 State(db): State<DatabaseConnection>,
799 791 headers: HeaderMap,
800 792 _user_session: Session,
793 + Extension(auth_config): Extension<Arc<AuthConfig>>,
801 794 hooks: Option<Extension<Arc<dyn RustwardenHooks>>>,
802 795 ) -> Result<Response, AppError> {
803 796 // Extract JWT from request (cookie or header) - same logic as jwt_extractor
@@ -824,11 +817,6 @@ pub async fn logout(
824 817 })?;
825 818
826 819 // Validate token to get user info
827 - let auth_config = AuthConfig::from_env().map_err(|e| {
828 - tracing::error!("Failed to load auth config: {}", e);
829 - AppError::Config(format!("Auth configuration error: {}", e))
830 - })?;
831 -
832 820 let claims = JwtService::validate_token(&auth_config, &jwt_token)?;
833 821 let user_id = claims.sub;
834 822
@@ -979,6 +967,7 @@ pub async fn logout(
979 967 pub async fn refresh_token(
980 968 State(db): State<DatabaseConnection>,
981 969 headers: HeaderMap,
970 + Extension(auth_config): Extension<Arc<AuthConfig>>,
982 971 Json(payload): Json<RefreshRequest>,
983 972 ) -> Result<Response, AppError> {
984 973 // SEC-C2: Per-IP rate limiting
@@ -1034,12 +1023,6 @@ pub async fn refresh_token(
1034 1023 )
1035 1024 .await?;
1036 1025
1037 - // Load auth configuration
1038 - let auth_config = AuthConfig::from_env().map_err(|e| {
1039 - tracing::error!("Failed to load auth config: {}", e);
1040 - AppError::Config(format!("Auth configuration error: {}", e))
1041 - })?;
1042 -
1043 1026 // Generate new JWT access token
1044 1027 let access_token = JwtService::generate_token(
1045 1028 &auth_config,
M crates/rustwarden-core/src/auth/oauth_handlers.rs
+1 -3
@@ -101,6 +101,7 @@ pub async fn oauth_callback(
101 101 headers: HeaderMap,
102 102 session: Session,
103 103 Extension(registry): Extension<Arc<ProviderRegistry>>,
104 + Extension(auth_config): Extension<Arc<AuthConfig>>,
104 105 hooks: Option<Extension<Arc<dyn RustwardenHooks>>>,
105 106 Path(provider_name): Path<String>,
106 107 Query(params): Query<OAuthCallbackQuery>,
@@ -200,9 +201,6 @@ pub async fn oauth_callback(
200 201 )
201 202 .await?;
202 203
203 - let auth_config = AuthConfig::from_env()
204 - .map_err(|e| AppError::Config(format!("Auth config error: {}", e)))?;
205 -
206 204 let access_token = JwtService::generate_token(
207 205 &auth_config,
208 206 user.id,
M crates/rustwarden-core/src/middleware/admin_helper.rs
+7 -6
@@ -46,13 +46,14 @@ pub async fn extract_admin_claims_from_parts(
46 46 return Err(AppError::Unauthorized);
47 47 }
48 48
49 - // Validate JWT token
50 - let auth_config = AuthConfig::from_env().map_err(|e| {
51 - tracing::error!("Failed to load auth config: {}", e);
52 - AppError::Config(format!("Auth configuration error: {}", e))
53 - })?;
49 + // Validate JWT token — use the shared AuthConfig from request extensions
50 + let auth_config = parts.extensions.get::<std::sync::Arc<AuthConfig>>().cloned()
51 + .ok_or_else(|| {
52 + tracing::error!("AuthConfig extension missing from request parts");
53 + AppError::Config("Auth configuration not available".to_string())
54 + })?;
54 55
55 - let claims = JwtService::validate_token(&auth_config, &jwt_token).map_err(|_| {
56 + let claims = JwtService::validate_token(&*auth_config, &jwt_token).map_err(|_| {
56 57 tracing::warn!("Invalid JWT token for admin route");
57 58 AppError::Unauthorized
58 59 })?;
M crates/rustwarden-core/src/middleware/admin_required.rs
+4 -2
@@ -72,7 +72,8 @@ pub async fn admin_required(
72 72 Some(claims) => claims,
73 73 None => {
74 74 tracing::warn!("Unauthenticated access attempt to admin route: {}", uri);
75 - let redirect_url = format!("/login?redirect={}", uri);
75 + let redirect_url =
76 + crate::middleware::auth_service::sanitize_redirect(&uri.to_string());
76 77 return Redirect::to(&redirect_url).into_response();
77 78 }
78 79 };
@@ -111,7 +112,8 @@ pub async fn admin_required_impl(
111 112 Some(claims) => claims,
112 113 None => {
113 114 tracing::warn!("Unauthenticated access attempt to admin route: {}", uri);
114 - let redirect_url = format!("/login?redirect={}", uri);
115 + let redirect_url =
116 + crate::middleware::auth_service::sanitize_redirect(&uri.to_string());
115 117 return Ok(Redirect::to(&redirect_url).into_response());
116 118 }
117 119 };
M crates/rustwarden-core/src/middleware/admin_service.rs
+5 -5
@@ -89,10 +89,10 @@ where
89 89 return Ok(axum::response::Redirect::to(&redirect_url).into_response());
90 90 }
91 91
92 - // Load auth config and validate token
93 - let auth_config = match AuthConfig::from_env() {
94 - Ok(config) => config,
95 - Err(_) => {
92 + // Load auth config from request extensions (injected at startup)
93 + let auth_config = match parts.extensions.get::<std::sync::Arc<AuthConfig>>().cloned() {
94 + Some(config) => config,
95 + None => {
96 96 return Ok((
97 97 axum::http::StatusCode::INTERNAL_SERVER_ERROR,
98 98 "Internal server error",
@@ -101,7 +101,7 @@ where
101 101 }
102 102 };
103 103
104 - let claims = match JwtService::validate_token(&auth_config, &jwt_token) {
104 + let claims = match JwtService::validate_token(&*auth_config, &jwt_token) {
105 105 Ok(claims) => claims,
106 106 Err(_) => {
107 107 let redirect_url = auth_service::sanitize_redirect(uri.path());
M crates/rustwarden-core/src/middleware/auth_required.rs
+29 -18
@@ -11,6 +11,8 @@ use axum::{
11 11 };
12 12 use sea_orm::DatabaseConnection;
13 13
14 +use std::sync::Arc;
15 +
14 16 use crate::auth::jwt::{Claims, JwtService};
15 17 use crate::auth::jwt_extractor;
16 18 use crate::config::auth::AuthConfig;
@@ -37,12 +39,18 @@ pub async fn extract_user_from_jwt(db: &DatabaseConnection, request: &Request) -
37 39
38 40 /// Internal implementation of extract_user_from_jwt
39 41 async fn extract_user_from_jwt_impl(db: &DatabaseConnection, request: &Request) -> Option<Claims> {
40 - // Load auth configuration
41 - let auth_config = match AuthConfig::from_env() {
42 - Ok(config) => config,
43 - Err(e) => {
44 - tracing::warn!("Failed to load auth config: {}", e);
45 - return None;
42 + // Load auth configuration from request extensions (injected at startup)
43 + let auth_config = match request.extensions().get::<Arc<AuthConfig>>() {
44 + Some(config) => config.clone(),
45 + None => {
46 + tracing::warn!("AuthConfig extension missing, falling back to from_env");
47 + match AuthConfig::from_env() {
48 + Ok(config) => Arc::new(config),
49 + Err(e) => {
50 + tracing::warn!("Failed to load auth config: {}", e);
51 + return None;
52 + }
53 + }
46 54 }
47 55 };
48 56
@@ -62,7 +70,7 @@ async fn extract_user_from_jwt_impl(db: &DatabaseConnection, request: &Request)
62 70 }
63 71
64 72 // Validate JWT token
65 - match JwtService::validate_token(&auth_config, &jwt_token) {
73 + match JwtService::validate_token(&*auth_config, &jwt_token) {
66 74 Ok(claims) => {
67 75 tracing::debug!("User extracted from JWT: {}", claims.sub);
68 76 Some(claims)
@@ -116,18 +124,20 @@ pub async fn auth_required_impl(
116 124 uri
117 125 );
118 126
119 - // Load auth configuration
120 - let auth_config = AuthConfig::from_env().map_err(|e| {
121 - tracing::error!("Failed to load auth config: {}", e);
122 - AppError::Config(format!("Auth configuration error: {}", e))
123 - })?;
127 + // Load auth configuration from request extensions (injected at startup)
128 + let auth_config = request.extensions().get::<Arc<AuthConfig>>().cloned()
129 + .ok_or_else(|| {
130 + tracing::error!("AuthConfig extension missing from request");
131 + AppError::Config("Auth configuration not available".to_string())
132 + })?;
124 133
125 134 // Extract JWT from request (cookie or header)
126 135 let jwt_token = match jwt_extractor::extract_jwt_from_request(&request) {
127 136 Ok(token) => token,
128 137 Err(_) => {
129 138 tracing::warn!("Unauthenticated access attempt to: {} (no token)", uri);
130 - let redirect_url = format!("/login?redirect={}", uri);
139 + let redirect_url =
140 + crate::middleware::auth_service::sanitize_redirect(&uri.to_string());
131 141 return Ok(Redirect::to(&redirect_url).into_response());
132 142 }
133 143 };
@@ -135,16 +145,17 @@ pub async fn auth_required_impl(
135 145 // Check if token is blacklisted
136 146 if JwtBlacklistService::is_blacklisted(db, &jwt_token).await? {
137 147 tracing::warn!("Blacklisted JWT token used for: {}", uri);
138 - let redirect_url = format!("/login?redirect={}", uri);
148 + let redirect_url = crate::middleware::auth_service::sanitize_redirect(&uri.to_string());
139 149 return Ok(Redirect::to(&redirect_url).into_response());
140 150 }
141 151
142 152 // Validate JWT token
143 - let claims = match JwtService::validate_token(&auth_config, &jwt_token) {
153 + let claims = match JwtService::validate_token(&*auth_config, &jwt_token) {
144 154 Ok(claims) => claims,
145 155 Err(e) => {
146 156 tracing::warn!("Invalid JWT token for {}: {}", uri, e);
147 - let redirect_url = format!("/login?redirect={}", uri);
157 + let redirect_url =
158 + crate::middleware::auth_service::sanitize_redirect(&uri.to_string());
148 159 return Ok(Redirect::to(&redirect_url).into_response());
149 160 }
150 161 };
@@ -208,12 +219,12 @@ pub async fn optional_auth(
208 219
209 220 // Try to extract and validate JWT (but don't fail if missing)
210 221 if let Ok(jwt_token) = jwt_extractor::extract_jwt_from_request(&request) {
211 - if let Ok(auth_config) = AuthConfig::from_env() {
222 + if let Some(auth_config) = request.extensions().get::<Arc<AuthConfig>>().cloned() {
212 223 if JwtBlacklistService::is_blacklisted(&db, &jwt_token)
213 224 .await
214 225 .is_ok_and(|b| !b)
215 226 {
216 - if let Ok(claims) = JwtService::validate_token(&auth_config, &jwt_token) {
227 + if let Ok(claims) = JwtService::validate_token(&*auth_config, &jwt_token) {
217 228 tracing::debug!("User authenticated: {} (from JWT)", claims.sub);
218 229 }
219 230 }
M crates/rustwarden-core/src/middleware/auth_service.rs
+6 -6
@@ -96,9 +96,9 @@ where
96 96 return Ok(axum::response::Redirect::to(&redirect_url).into_response());
97 97 }
98 98
99 - let auth_config = match AuthConfig::from_env() {
100 - Ok(config) => config,
101 - Err(_) => {
99 + let auth_config = match parts.extensions.get::<std::sync::Arc<AuthConfig>>().cloned() {
100 + Some(config) => config,
101 + None => {
102 102 return Ok((
103 103 axum::http::StatusCode::INTERNAL_SERVER_ERROR,
104 104 "Internal server error",
@@ -107,7 +107,7 @@ where
107 107 }
108 108 };
109 109
110 - let claims = match JwtService::validate_token(&auth_config, &jwt_token) {
110 + let claims = match JwtService::validate_token(&*auth_config, &jwt_token) {
111 111 Ok(claims) => claims,
112 112 Err(_) => {
113 113 tracing::warn!("AuthService: Invalid JWT token, redirecting to login");
@@ -231,8 +231,8 @@ where
231 231 .unwrap_or(true);
232 232
233 233 if !is_blacklisted {
234 - if let Ok(auth_config) = AuthConfig::from_env() {
235 - if let Ok(claims) = JwtService::validate_token(&auth_config, &jwt_token)
234 + if let Some(auth_config) = parts.extensions.get::<std::sync::Arc<AuthConfig>>().cloned() {
235 + if let Ok(claims) = JwtService::validate_token(&*auth_config, &jwt_token)
236 236 {
237 237 parts.extensions.insert(claims.clone());
238 238 if let Ok(json) = serde_json::to_string(&claims) {
A docs/SECURITY_AUDIT_2026_05.md
+192 -0
@@ -0,0 +1,192 @@
1 +# Audit de sécurité Rustwarden — Mai 2026
2 +
3 +**Date :** 4 mai 2026
4 +**Périmètre :** Ensemble du code source (`rustwarden-core`, `rustwarden-ui`, `app`, configuration)
5 +**Contexte :** Rustwarden est un framework backend Rust de gestion des utilisateurs et d'authentification (JWT, RBAC, OAuth2/OIDC, 2FA/TOTP, équipes, sessions, emails). Ce n'est pas un gestionnaire de mots de passe.
6 +
7 +**Niveau de risque global : HAUT**
8 +
9 +---
10 +
11 +## Synthèse
12 +
13 +| Sévérité | Nombre | Corrigés | Risque accepté | À planifier |
14 +|----------|--------|----------|----------------|-------------|
15 +| CRITIQUE | 2 | 1 | 1 | 0 |
16 +| HAUT | 5 | 4 | 1 | 0 |
17 +| MOYEN | 7 | 0 | 0 | 1 (CSP) |
18 +| BAS / INFO | 8 | 0 | 0 | 0 |
19 +
20 +---
21 +
22 +## Findings détaillés
23 +
24 +### CRITIQUE
25 +
26 +#### C1. Blocklist JWT_SECRET incomplète
27 +- **Fichier :** `crates/rustwarden-core/src/config/auth.rs:73-78`
28 +- **OWASP :** A02 — Cryptographic Failures
29 +- **Description :** Les valeurs placeholder de `.env` et `.env.sample` ne figurent pas dans `KNOWN_EXAMPLES`. Un déploiement copiant ces fichiers sans modifier le secret ne serait pas bloqué au démarrage.
30 +- **Décision : RISQUE ACCEPTÉ** — Le `.env` est dans `.gitignore`, seul `.env.sample` est commité. Environnement de développement uniquement.
31 +
32 +#### C2. Open redirect dans `auth_required` et `admin_required`
33 +- **Fichiers :**
34 + - `crates/rustwarden-core/src/middleware/auth_required.rs:130,138,147`
35 + - `crates/rustwarden-core/src/middleware/admin_required.rs:75,114`
36 +- **OWASP :** A01 — Broken Access Control
37 +- **Description :** Le paramètre `redirect` utilise le raw URI sans sanitisation (`format!("/login?redirect={}", uri)`), contrairement à `AuthService` qui dispose de `sanitize_redirect()`. Un URI type `//evil.com` est interprété comme protocol-relative par le navigateur.
38 +- **Décision : CORRIGÉ** — Réutilisation de `sanitize_redirect()` sur tous les points de redirection.
39 +
40 +---
41 +
42 +### HAUT
43 +
44 +#### H1. Aucune configuration CORS appliquée
45 +- **Fichier :** `crates/rustwarden-core/src/app.rs` (stack middleware)
46 +- **OWASP :** A05 — Security Misconfiguration
47 +- **Description :** `tower-http` est déclaré avec la feature `cors` dans `rustwarden-ui` mais aucun `CorsLayer` n'est instancié. Pour un framework destiné à des SPA sur des origines différentes, c'est un trou fonctionnel et sécuritaire.
48 +- **Décision : CORRIGÉ** — `CorsLayer` configurable ajouté dans `app.rs` via variables d'environnement.
49 +
50 +#### H2. CSP neutralisée par `unsafe-inline` + `unsafe-eval`
51 +- **Fichier :** `crates/rustwarden-core/src/app.rs:589-591`
52 +- **OWASP :** A03 — Injection (XSS)
53 +- **Description :** `script-src 'self' 'unsafe-inline' 'unsafe-eval'` permet l'exécution de scripts injectés. Alpine.js requiert `eval()` sauf avec le build CSP-compatible `@alpinejs/csp`. HTMX n'utilise pas `eval()` mais peut injecter du HTML via `innerHTML`.
54 +- **Décision : PLAN SPÉCIFIQUE** — Voir `docs/plans/PLAN_CSP_HTMX.md`.
55 +
56 +#### H3. Dépendances vulnérables
57 +- **Fichier :** `Cargo.lock`
58 +- **OWASP :** A06 — Vulnerable and Outdated Components
59 +- **Advisories :**
60 +
61 +| Crate | Advisory | Impact |
62 +|-------|----------|--------|
63 +| `rsa 0.9.10` | RUSTSEC-2023-0071 | Marvin Attack (timing). Pas de fix upstream. Risque limité : JWT utilise HS256. |
64 +| `rustls-webpki 0.103.9` | RUSTSEC-2026-0104 | Panic DoS dans CRL parsing |
65 +| `rustls-webpki 0.103.9` | RUSTSEC-2026-0099 | Bypass wildcard name constraint |
66 +| `rand 0.9.2` | RUSTSEC-2026-0097 | Unsoundness avec custom loggers |
67 +
68 +- **Décision : CORRIGÉ** — `cargo update -p rustls-webpki` appliqué.
69 +
70 +#### H4. `AuthConfig::from_env()` appelé à chaque requête
71 +- **Fichiers :**
72 + - `crates/rustwarden-core/src/middleware/auth_required.rs:40-41,119-123`
73 + - `crates/rustwarden-core/src/auth/extractors.rs:200`
74 + - `crates/rustwarden-core/src/auth/handlers.rs:386,581,826,1037`
75 +- **OWASP :** A04 — Insecure Design
76 +- **Description :** Au lieu de lire le `Extension<Arc<AuthConfig>>` partagé (injecté en `app.rs:440`), les handlers et middlewares rechargent la config depuis les variables d'env à chaque requête, créant une fenêtre TOCTOU.
77 +- **Décision : CORRIGÉ** — Tous les appels remplacés par lecture de l'extension Axum.
78 +
79 +#### H5. `accept_invalid_certs` sans garde production
80 +- **Fichiers :** `config/email.rs:48,76`, `services/email_service.rs:97-98`
81 +- **OWASP :** A02 — Cryptographic Failures
82 +- **Description :** `SMTP_ACCEPT_INVALID_CERTS=true` n'est pas bloqué en production contrairement à `COOKIE_SECURE`.
83 +- **Décision : RISQUE ACCEPTÉ** — Cas d'usage légitime avec certificats TLS auto-signés. Le warning `tracing::warn!` est conservé.
84 +
85 +---
86 +
87 +### MOYEN
88 +
89 +#### M1. HS256 JWT avec secret symétrique partagé
90 +- **Fichier :** `auth/jwt.rs:163-167`
91 +- **OWASP :** A02
92 +- **Description :** Tout service validant les JWT peut aussi en forger. Acceptable pour monolithe, problématique pour multi-services.
93 +- **Recommandation :** Documenter comme contrainte architecturale. Considérer RS256/ES256 pour déploiements multi-services.
94 +
95 +#### M2. Rate limiter in-memory non partagé entre instances
96 +- **Fichier :** `services/rate_limit_service.rs`
97 +- **OWASP :** A04
98 +- **Description :** Rate limiting contournable derrière un load balancer (N × limite par instance).
99 +- **Recommandation :** Documenter. Intégrer Redis quand `SESSION_BACKEND=redis`.
100 +
101 +#### M3. `innerHTML` dans les templates admin
102 +- **Fichiers :** `templates/admin/users_list.html:434,459`, `templates/settings/password.html:321,333,349`
103 +- **OWASP :** A03
104 +- **Description :** Malgré un commentaire `// SEC-FIX: Use textContent`, le code utilise toujours `innerHTML`.
105 +- **Recommandation :** Inclus dans le plan CSP/HTMX (H2).
106 +
107 +#### M4. TOTP SHA-1
108 +- **Fichier :** `services/totp_service.rs:62`
109 +- **OWASP :** A02
110 +- **Description :** Standard RFC 6238 pour compatibilité authenticator apps. Acceptable.
111 +- **Recommandation :** Documenter le choix. SHA-256 en option future.
112 +
113 +#### M5. `.env.sample` JWT_SECRET pas dans la blocklist
114 +- **Fichier :** `.env.sample:15`
115 +- **OWASP :** A05
116 +- **Description :** Copie directe sans modification → déploiement vulnérable.
117 +- **Note :** Couvert par la décision C1 (risque accepté en dev).
118 +
119 +#### M6. Session backend SeaORM fallback silencieux vers MemoryStore
120 +- **Fichier :** `auth/session.rs:95`
121 +- **OWASP :** A07
122 +- **Description :** Devrait être une erreur, pas un simple warning.
123 +- **Recommandation :** Logger en `error` ou refuser le démarrage.
124 +
125 +#### M7. Pas de rate limit IP sur le endpoint 2FA
126 +- **Fichier :** `auth/handlers.rs:536-541`
127 +- **OWASP :** A07
128 +- **Description :** 5 tentatives/challenge mais challenges recréables sans limite IP.
129 +- **Recommandation :** Ajouter `check_auth_rate_limit` sur `verify_2fa`.
130 +
131 +---
132 +
133 +### BAS / INFO
134 +
135 +| # | Description | Fichier |
136 +|---|-------------|---------|
137 +| L1 | `certificat_imap.pem` en racine — vérifier qu'il s'agit d'un cert public | `./certificat_imap.pem` |
138 +| L2 | `AppError::Validation` passe les détails au client | `error.rs:135-140` |
139 +| L3 | Mot de passe admin par défaut loggé — limiter au niveau DEBUG | `services/default_admin_service.rs` |
140 +| L4 | Chemin relatif `certificat_imap.pem` dans `.env` — fragile | `.env:41,56` |
141 +| L5 | HSTS sans `preload` — acceptable, rendre configurable | `app.rs:599-601` |
142 +| I1 | `\|safe` filter sur variables `_json` dans Askama — vérifier échappement `</script>` | `templates/settings/preferences.html:312-316` |
143 +| I2 | `X-XSS-Protection: 0` correctement implémenté (recommandation moderne) | `app.rs:575-577` |
144 +| I3 | SeaORM paramétré partout, zéro raw SQL — conforme à la constitution | Ensemble du code |
145 +
146 +---
147 +
148 +## Points positifs
149 +
150 +Le framework démontre une maturité sécurité significative :
151 +
152 +- **Anti-timing attack** : dummy bcrypt hash avec coût identique pour utilisateurs inexistants
153 +- **Refresh token rotation** avec détection de rejeu et révocation totale
154 +- **Tokens stockés en SHA-256**, jamais en clair (refresh tokens, personal access tokens)
155 +- **Session ID recyclé** au login (anti-fixation)
156 +- **CSRF constant-time** via `subtle::ConstantTimeEq`, exemption Bearer
157 +- **OAuth** : state CSRF constant-time + PKCE + nonce
158 +- **AES-256-GCM** pour secrets at-rest (TOTP, OAuth)
159 +- **Cookie Secure forcé** en production (`APP_DEBUG=false`)
160 +- **TOTP brute-force** : max 5 tentatives/challenge, lockout 15 min
161 +- **Validation systématique** via `validator` sur tous les DTOs
162 +- **Zéro raw SQL** — règle constitution respectée
163 +- **Password complexity** configurable via middleware dédié
164 +- **Audit logging** via hooks extensibles (`on_audit_event`)
165 +
166 +---
167 +
168 +## Conformité OWASP Top 10
169 +
170 +| Catégorie | Statut | Notes |
171 +|-----------|--------|-------|
172 +| A01: Broken Access Control | **CORRIGÉ** | Open redirect corrigé, CORS ajouté. RBAC solide. |
173 +| A02: Cryptographic Failures | **PARTIEL** | HS256 documenté. AES-256-GCM, bcrypt bien implémentés. accept_invalid_certs accepté (auto-signé). |
174 +| A03: Injection | **PLAN** | CSP à renforcer (plan HTMX). Askama auto-escape. Zéro SQL injection. |
175 +| A04: Insecure Design | **CORRIGÉ** | AuthConfig centralisé. Rate limiter in-memory documenté. |
176 +| A05: Security Misconfiguration | **CORRIGÉ** | CORS configuré. Secrets en env vars. |
177 +| A06: Vulnerable Components | **CORRIGÉ** | rustls-webpki mis à jour. rsa en surveillance. |
178 +| A07: Auth Failures | **PARTIEL** | 2FA rate limit IP à ajouter. Session backend à clarifier. |
179 +| A08: Integrity Failures | PASS | Pas de mécanisme de mise à jour non signé. |
180 +| A09: Logging Failures | MINEUR | Détails de validation exposés. Auth events bien loggés. |
181 +| A10: SSRF | PASS | URLs OAuth construites depuis config, pas input utilisateur. |
182 +
183 +---
184 +
185 +## Historique des corrections
186 +
187 +| Date | Finding | Action |
188 +|------|---------|--------|
189 +| 2026-05-04 | C2 | Open redirect corrigé via `sanitize_redirect()` |
190 +| 2026-05-04 | H1 | CorsLayer configurable ajouté |
191 +| 2026-05-04 | H3 | `cargo update -p rustls-webpki` |
192 +| 2026-05-04 | H4 | AuthConfig centralisé via Extension |
D docs/admin_oauth_settings_ui_7ca3f1fd.plan.html
+0 -40
@@ -1,40 +0,0 @@
1 -<!DOCTYPE html><html><head>
2 - <title>admin_oauth_settings_ui_7ca3f1fd.plan</title>
3 - <meta charset="utf-8">
4 - <meta name="viewport" content="width=device-width, initial-scale=1.0">
5 -
6 - <link rel="stylesheet" href="file:////home/monsieurb/.cursor/extensions/shd101wyy.markdown-preview-enhanced-0.8.20-universal/crossnote/dependencies/katex/katex.min.css">
7 -
8 -
9 -
10 -
11 -
12 - <style>
13 - code[class*=language-],pre[class*=language-]{color:#333;background:0 0;font-family:Consolas,"Liberation Mono",Menlo,Courier,monospace;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.4;-moz-tab-size:8;-o-tab-size:8;tab-size:8;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:.8em;overflow:auto;border-radius:3px;background:#f5f5f5}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal;background:#f5f5f5}.token.blockquote,.token.comment{color:#969896}.token.cdata{color:#183691}.token.doctype,.token.macro.property,.token.punctuation,.token.variable{color:#333}.token.builtin,.token.important,.token.keyword,.token.operator,.token.rule{color:#a71d5d}.token.attr-value,.token.regex,.token.string,.token.url{color:#183691}.token.atrule,.token.boolean,.token.code,.token.command,.token.constant,.token.entity,.token.number,.token.property,.token.symbol{color:#0086b3}.token.prolog,.token.selector,.token.tag{color:#63a35c}.token.attr-name,.token.class,.token.class-name,.token.function,.token.id,.token.namespace,.token.pseudo-class,.token.pseudo-element,.token.url-reference .token.variable{color:#795da3}.token.entity{cursor:help}.token.title,.token.title .token.punctuation{font-weight:700;color:#1d3e81}.token.list{color:#ed6a43}.token.inserted{background-color:#eaffea;color:#55a532}.token.deleted{background-color:#ffecec;color:#bd2c00}.token.bold{font-weight:700}.token.italic{font-style:italic}.language-json .token.property{color:#183691}.language-markup .token.tag .token.punctuation{color:#333}.language-css .token.function,code.language-css{color:#0086b3}.language-yaml .token.atrule{color:#63a35c}code.language-yaml{color:#183691}.language-ruby .token.function{color:#333}.language-markdown .token.url{color:#795da3}.language-makefile .token.symbol{color:#795da3}.language-makefile .token.variable{color:#183691}.language-makefile .token.builtin{color:#0086b3}.language-bash .token.keyword{color:#0086b3}pre[data-line]{position:relative;padding:1em 0 1em 3em}pre[data-line] .line-highlight-wrapper{position:absolute;top:0;left:0;background-color:transparent;display:block;width:100%}pre[data-line] .line-highlight{position:absolute;left:0;right:0;padding:inherit 0;margin-top:1em;background:hsla(24,20%,50%,.08);background:linear-gradient(to right,hsla(24,20%,50%,.1) 70%,hsla(24,20%,50%,0));pointer-events:none;line-height:inherit;white-space:pre}pre[data-line] .line-highlight:before,pre[data-line] .line-highlight[data-end]:after{content:attr(data-start);position:absolute;top:.4em;left:.6em;min-width:1em;padding:0 .5em;background-color:hsla(24,20%,50%,.4);color:#f4f1ef;font:bold 65%/1.5 sans-serif;text-align:center;vertical-align:.3em;border-radius:999px;text-shadow:none;box-shadow:0 1px #fff}pre[data-line] .line-highlight[data-end]:after{content:attr(data-end);top:auto;bottom:.4em}html body{font-family:'Helvetica Neue',Helvetica,'Segoe UI',Arial,freesans,sans-serif;font-size:16px;line-height:1.6;color:#333;background-color:#fff;overflow:initial;box-sizing:border-box;word-wrap:break-word}html body>:first-child{margin-top:0}html body h1,html body h2,html body h3,html body h4,html body h5,html body h6{line-height:1.2;margin-top:1em;margin-bottom:16px;color:#000}html body h1{font-size:2.25em;font-weight:300;padding-bottom:.3em}html body h2{font-size:1.75em;font-weight:400;padding-bottom:.3em}html body h3{font-size:1.5em;font-weight:500}html body h4{font-size:1.25em;font-weight:600}html body h5{font-size:1.1em;font-weight:600}html body h6{font-size:1em;font-weight:600}html body h1,html body h2,html body h3,html body h4,html body h5{font-weight:600}html body h5{font-size:1em}html body h6{color:#5c5c5c}html body strong{color:#000}html body del{color:#5c5c5c}html body a:not([href]){color:inherit;text-decoration:none}html body a{color:#08c;text-decoration:none}html body a:hover{color:#00a3f5;text-decoration:none}html body img{max-width:100%}html body>p{margin-top:0;margin-bottom:16px;word-wrap:break-word}html body>ol,html body>ul{margin-bottom:16px}html body ol,html body ul{padding-left:2em}html body ol.no-list,html body ul.no-list{padding:0;list-style-type:none}html body ol ol,html body ol ul,html body ul ol,html body ul ul{margin-top:0;margin-bottom:0}html body li{margin-bottom:0}html body li.task-list-item{list-style:none}html body li>p{margin-top:0;margin-bottom:0}html body .task-list-item-checkbox{margin:0 .2em .25em -1.8em;vertical-align:middle}html body .task-list-item-checkbox:hover{cursor:pointer}html body blockquote{margin:16px 0;font-size:inherit;padding:0 15px;color:#5c5c5c;background-color:#f0f0f0;border-left:4px solid #d6d6d6}html body blockquote>:first-child{margin-top:0}html body blockquote>:last-child{margin-bottom:0}html body hr{height:4px;margin:32px 0;background-color:#d6d6d6;border:0 none}html body table{margin:10px 0 15px 0;border-collapse:collapse;border-spacing:0;display:block;width:100%;overflow:auto;word-break:normal;word-break:keep-all}html body table th{font-weight:700;color:#000}html body table td,html body table th{border:1px solid #d6d6d6;padding:6px 13px}html body dl{padding:0}html body dl dt{padding:0;margin-top:16px;font-size:1em;font-style:italic;font-weight:700}html body dl dd{padding:0 16px;margin-bottom:16px}html body code{font-family:Menlo,Monaco,Consolas,'Courier New',monospace;font-size:.85em;color:#000;background-color:#f0f0f0;border-radius:3px;padding:.2em 0}html body code::after,html body code::before{letter-spacing:-.2em;content:'\00a0'}html body pre>code{padding:0;margin:0;word-break:normal;white-space:pre;background:0 0;border:0}html body .highlight{margin-bottom:16px}html body .highlight pre,html body pre{padding:1em;overflow:auto;line-height:1.45;border:#d6d6d6;border-radius:3px}html body .highlight pre{margin-bottom:0;word-break:normal}html body pre code,html body pre tt{display:inline;max-width:initial;padding:0;margin:0;overflow:initial;line-height:inherit;word-wrap:normal;background-color:transparent;border:0}html body pre code:after,html body pre code:before,html body pre tt:after,html body pre tt:before{content:normal}html body blockquote,html body dl,html body ol,html body p,html body pre,html body ul{margin-top:0;margin-bottom:16px}html body kbd{color:#000;border:1px solid #d6d6d6;border-bottom:2px solid #c7c7c7;padding:2px 4px;background-color:#f0f0f0;border-radius:3px}@media print{html body{background-color:#fff}html body h1,html body h2,html body h3,html body h4,html body h5,html body h6{color:#000;page-break-after:avoid}html body blockquote{color:#5c5c5c}html body pre{page-break-inside:avoid}html body table{display:table}html body img{display:block;max-width:100%;max-height:100%}html body code,html body pre{word-wrap:break-word;white-space:pre}}.markdown-preview{width:100%;height:100%;box-sizing:border-box}.markdown-preview ul{list-style:disc}.markdown-preview ul ul{list-style:circle}.markdown-preview ul ul ul{list-style:square}.markdown-preview ol{list-style:decimal}.markdown-preview ol ol,.markdown-preview ul ol{list-style-type:lower-roman}.markdown-preview ol ol ol,.markdown-preview ol ul ol,.markdown-preview ul ol ol,.markdown-preview ul ul ol{list-style-type:lower-alpha}.markdown-preview .newpage,.markdown-preview .pagebreak{page-break-before:always}.markdown-preview pre.line-numbers{position:relative;padding-left:3.8em;counter-reset:linenumber}.markdown-preview pre.line-numbers>code{position:relative}.markdown-preview pre.line-numbers .line-numbers-rows{position:absolute;pointer-events:none;top:1em;font-size:100%;left:0;width:3em;letter-spacing:-1px;border-right:1px solid #999;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.markdown-preview pre.line-numbers .line-numbers-rows>span{pointer-events:none;display:block;counter-increment:linenumber}.markdown-preview pre.line-numbers .line-numbers-rows>span:before{content:counter(linenumber);color:#999;display:block;padding-right:.8em;text-align:right}.markdown-preview .mathjax-exps .MathJax_Display{text-align:center!important}.markdown-preview:not([data-for=preview]) .code-chunk .code-chunk-btn-group{display:none}.markdown-preview:not([data-for=preview]) .code-chunk .status{display:none}.markdown-preview:not([data-for=preview]) .code-chunk .output-div{margin-bottom:16px}.markdown-preview .md-toc{padding:0}.markdown-preview .md-toc .md-toc-link-wrapper .md-toc-link{display:inline;padding:.25rem 0}.markdown-preview .md-toc .md-toc-link-wrapper .md-toc-link div,.markdown-preview .md-toc .md-toc-link-wrapper .md-toc-link p{display:inline}.markdown-preview .md-toc .md-toc-link-wrapper.highlighted .md-toc-link{font-weight:800}.scrollbar-style::-webkit-scrollbar{width:8px}.scrollbar-style::-webkit-scrollbar-track{border-radius:10px;background-color:transparent}.scrollbar-style::-webkit-scrollbar-thumb{border-radius:5px;background-color:rgba(150,150,150,.66);border:4px solid rgba(150,150,150,.66);background-clip:content-box}html body[for=html-export]:not([data-presentation-mode]){position:relative;width:100%;height:100%;top:0;left:0;margin:0;padding:0;overflow:auto}html body[for=html-export]:not([data-presentation-mode]) .markdown-preview{position:relative;top:0;min-height:100vh}@media screen and (min-width:914px){html body[for=html-export]:not([data-presentation-mode]) .markdown-preview{padding:2em calc(50% - 457px + 2em)}}@media screen and (max-width:914px){html body[for=html-export]:not([data-presentation-mode]) .markdown-preview{padding:2em}}@media screen and (max-width:450px){html body[for=html-export]:not([data-presentation-mode]) .markdown-preview{font-size:14px!important;padding:1em}}@media print{html body[for=html-export]:not([data-presentation-mode]) #sidebar-toc-btn{display:none}}html body[for=html-export]:not([data-presentation-mode]) #sidebar-toc-btn{position:fixed;bottom:8px;left:8px;font-size:28px;cursor:pointer;color:inherit;z-index:99;width:32px;text-align:center;opacity:.4}html body[for=html-export]:not([data-presentation-mode])[html-show-sidebar-toc] #sidebar-toc-btn{opacity:1}html body[for=html-export]:not([data-presentation-mode])[html-show-sidebar-toc] .md-sidebar-toc{position:fixed;top:0;left:0;width:300px;height:100%;padding:32px 0 48px 0;font-size:14px;box-shadow:0 0 4px rgba(150,150,150,.33);box-sizing:border-box;overflow:auto;background-color:inherit}html body[for=html-export]:not([data-presentation-mode])[html-show-sidebar-toc] .md-sidebar-toc::-webkit-scrollbar{width:8px}html body[for=html-export]:not([data-presentation-mode])[html-show-sidebar-toc] .md-sidebar-toc::-webkit-scrollbar-track{border-radius:10px;background-color:transparent}html body[for=html-export]:not([data-presentation-mode])[html-show-sidebar-toc] .md-sidebar-toc::-webkit-scrollbar-thumb{border-radius:5px;background-color:rgba(150,150,150,.66);border:4px solid rgba(150,150,150,.66);background-clip:content-box}html body[for=html-export]:not([data-presentation-mode])[html-show-sidebar-toc] .md-sidebar-toc a{text-decoration:none}html body[for=html-export]:not([data-presentation-mode])[html-show-sidebar-toc] .md-sidebar-toc .md-toc{padding:0 16px}html body[for=html-export]:not([data-presentation-mode])[html-show-sidebar-toc] .md-sidebar-toc .md-toc .md-toc-link-wrapper .md-toc-link{display:inline;padding:.25rem 0}html body[for=html-export]:not([data-presentation-mode])[html-show-sidebar-toc] .md-sidebar-toc .md-toc .md-toc-link-wrapper .md-toc-link div,html body[for=html-export]:not([data-presentation-mode])[html-show-sidebar-toc] .md-sidebar-toc .md-toc .md-toc-link-wrapper .md-toc-link p{display:inline}html body[for=html-export]:not([data-presentation-mode])[html-show-sidebar-toc] .md-sidebar-toc .md-toc .md-toc-link-wrapper.highlighted .md-toc-link{font-weight:800}html body[for=html-export]:not([data-presentation-mode])[html-show-sidebar-toc] .markdown-preview{left:300px;width:calc(100% - 300px);padding:2em calc(50% - 457px - 300px / 2);margin:0;box-sizing:border-box}@media screen and (max-width:1274px){html body[for=html-export]:not([data-presentation-mode])[html-show-sidebar-toc] .markdown-preview{padding:2em}}@media screen and (max-width:450px){html body[for=html-export]:not([data-presentation-mode])[html-show-sidebar-toc] .markdown-preview{width:100%}}html body[for=html-export]:not([data-presentation-mode]):not([html-show-sidebar-toc]) .markdown-preview{left:50%;transform:translateX(-50%)}html body[for=html-export]:not([data-presentation-mode]):not([html-show-sidebar-toc]) .md-sidebar-toc{display:none}
14 -/* Please visit the URL below for more information: */
15 -/* https://shd101wyy.github.io/markdown-preview-enhanced/#/customize-css */
16 -
17 - </style>
18 - <!-- The content below will be included at the end of the <head> element. --><script type="text/javascript">
19 - document.addEventListener("DOMContentLoaded", function () {
20 - // your code here
21 - });
22 -</script></head><body for="html-export">
23 -
24 -
25 - <div class="crossnote markdown-preview ">
26 -
27 -<h1 id="refonte-admin-oauth-settings---configuration-complete-via-ui">Refonte Admin OAuth Settings -- Configuration complete via UI </h1>
28 -<h2></h2>
29 -
30 - </div>
31 -
32 -
33 -
34 -
35 -
36 -
37 -
38 -
39 -
40 - </body></html>
40 > \ No newline at end of file
A docs/constitution.md
+150 -0
@@ -0,0 +1,150 @@
1 +<!--
2 +Sync Impact Report:
3 +- Version change: (none) → 0.5.2 (initial placement)
4 +- Source: Rustwarden/constitution.md — constitution complète importée pour le nouveau projet
5 +- Modified principles: N/A (placement initial)
6 +- Added sections: Constitution complète (Core Principles I–V, Technical Constraints, Development Workflow, Governance)
7 +- Removed sections: None
8 +- Templates requiring updates:
9 + ✅ plan-template.md (Constitution Check / gates — aligné, pas de modification)
10 + ✅ spec-template.md (scope/requirements — aligné, pas de modification)
11 + ✅ tasks-template.md (catégorisation tâches — aligné, pas de modification)
12 + ✅ .cursor/commands/*.md (référence .specify/memory/constitution.md — valide)
13 +- Follow-up TODOs: None
14 +-->
15 +
16 +# Rustwarden Constitution
17 +
18 +## Core Principles
19 +
20 +### I. Qualité de Code
21 +
22 +Toutes les fonctions publiques DOIVENT être documentées avec `///` (doc comments Rust). Le code DOIT respecter les bonnes pratiques Rust (clippy warnings = 0). Les structures de données DOIVENT utiliser `#[derive(Debug, Clone)]` où approprié. La gestion d'erreurs DOIT utiliser `Result<T, E>` avec des types d'erreur explicites.
23 +
24 +**Rationale**: La documentation et la qualité du code facilitent la maintenance, la collaboration et réduisent les bugs. Rust offre des outils puissants (clippy, rustfmt) qui doivent être utilisés.
25 +
26 +### II. Architecture Axum et SeaORM
27 +
28 +Toutes les routes HTTP DOIVENT être définies avec Axum uniquement (`axum::Router`). Toutes les interactions avec la base de données DOIVENT passer par SeaORM (aucun raw SQL autorisé). Les migrations DOIVENT être gérées via le système de migrations SeaORM (`migrations/src/`). Le serveur DOIT utiliser Tokio comme runtime async (requis par Axum).
29 +
30 +**Documentation SeaORM** (amendement v0.4.1): Toutes les interactions avec la base de données DOIVENT être implémentées en référence à la [documentation officielle SeaORM](https://www.sea-ql.org/SeaORM/docs/index/). Avant d'implémenter une nouvelle fonctionnalité de base de données (requêtes, relations, transactions, etc.), la documentation SeaORM DOIT être consultée pour garantir l'utilisation des bonnes pratiques et des patterns recommandés. Les exemples de code et les patterns documentés dans la documentation SeaORM DOIVENT être privilégiés par rapport aux approches personnalisées.
31 +
32 +**Implémentation obligatoire des handlers serveur** (amendement v0.5.0): TOUJOURS implémenter les handlers serveur pour que l'API soit fonctionnelle. Les handlers stub (retournant `501 Not Implemented`) sont INTERDITS dans le code de production. Chaque route définie DOIT avoir un handler complet qui :
33 +- Injecte l'état nécessaire (`State<DatabaseConnection>`, `Extension<Claims>`, etc.)
34 +- Utilise les services métier appropriés
35 +- Retourne des réponses fonctionnelles avec données réelles
36 +- Gère les erreurs de manière appropriée
37 +
38 +Les handlers stub sont uniquement autorisés temporairement pendant le développement incrémental, mais DOIVENT être remplacés par des implémentations complètes avant que la feature soit considérée comme terminée. Toute documentation d'API (OpenAPI, guides, exemples) qui référence un endpoint DOIT correspondre à un handler serveur fonctionnel.
39 +
40 +**Rationale**: Axum et SeaORM sont les frameworks standard pour Rust, offrant sécurité, performance et maintenabilité. L'utilisation exclusive de ces outils garantit la cohérence de l'architecture. La documentation officielle SeaORM contient les meilleures pratiques, les patterns recommandés et les solutions aux problèmes courants. En s'y référant systématiquement, on évite les erreurs d'implémentation, on améliore la maintenabilité et on garantit la compatibilité avec les futures versions de SeaORM. L'implémentation complète des handlers serveur est essentielle pour que l'API soit réellement utilisable par les clients. Des handlers stub créent une fausse impression de fonctionnalité et empêchent les tests d'intégration réels. Cette règle garantit que toute documentation d'API correspond à une implémentation fonctionnelle, évitant les frustrations des développeurs clients.
41 +
42 +### III. Développement Incrémental
43 +
44 +Chaque fonctionnalité DOIT être complète et testée avant de passer à la suivante. L'architecture DOIT être modulaire (structure de dossiers claire, séparation des responsabilités). Les user stories DOIVENT être indépendantes et testables individuellement.
45 +
46 +**Rationale**: Le développement incrémental permet de livrer de la valeur rapidement, facilite les tests et réduit la complexité. Chaque étape doit être validée avant de continuer.
47 +
48 +### IV. Sécurité par Défaut
49 +
50 +Tous les secrets DOIVENT être stockés dans des variables d'environnement, JAMAIS dans le code. La validation et la sanitization des entrées utilisateur DOIVENT être effectuées systématiquement. L'authentification et l'autorisation RBAC DOIVENT être implémentées pour toutes les routes protégées.
51 +
52 +**JWT Authentication Standard** (amendement v0.3.1): Le système DOIT extraire et vérifier les JWT tokens depuis le header `Authorization` au format `Bearer <token>`. Cette extraction DOIT être la méthode principale pour l'authentification API. Les cookies HttpOnly peuvent être utilisés comme méthode alternative pour les clients web, mais le header Authorization a priorité pour permettre aux clients API de surcharger l'authentification par cookie.
53 +
54 +**Conformité ANSSI Rust** (amendement v0.4.0): Le code DOIT respecter les recommandations du [Guide ANSSI pour le développement sécurisé en Rust](https://anssi-fr.github.io/rust-guide/). Spécifiquement :
55 +- Le code DOIT compiler avec `#![deny(warnings)]` activé (ou au minimum `#![warn(warnings)]`)
56 +- Toute utilisation de `unsafe` DOIT être justifiée et documentée avec un commentaire `// SAFETY:` expliquant pourquoi `unsafe` est nécessaire et quels invariants doivent être respectés
57 +- Les `unwrap()` et `expect()` sont INTERDITS dans le code de production (sauf dans les tests unitaires). Toutes les erreurs DOIVENT être gérées explicitement avec `Result<T, E>` et l'opérateur `?`
58 +- L'utilisation de `std::mem::uninitialized` est INTERDITE (dépréciée). L'utilisation de `std::mem::MaybeUninit` DOIT être justifiée pour chaque cas d'usage
59 +- Les outils `cargo-audit` et `cargo-outdated` DOIVENT être exécutés régulièrement. Les vulnérabilités détectées DOIVENT être corrigées avant chaque release
60 +- Toutes les considérations de sécurité, invariants et garanties DOIVENT être documentées
61 +
62 +Voir `docs/ANSSI_COMPLIANCE_REPORT.md` pour le rapport de conformité détaillé et les actions prioritaires.
63 +
64 +**Rationale**: La sécurité est critique pour un système cloud. Les secrets dans le code sont une vulnérabilité majeure. La validation des entrées prévient les attaques par injection. L'authentification JWT via header Authorization est le standard de l'industrie pour les APIs REST et permet une authentification stateless sécurisée. Les guidelines ANSSI représentent les meilleures pratiques pour le développement sécurisé en Rust. Leur respect garantit que les garanties de sécurité offertes par Rust ne sont pas invalidées par une utilisation incorrecte du langage.
65 +
66 +### V. Tests et Validation Continue
67 +
68 +Tous les composants DOIVENT avoir des tests unitaires et d'intégration. La couverture de tests DOIT être > 80% (objectif > 90% pour les composants critiques). Les tests DOIVENT être exécutés avant chaque commit (`cargo test`). Les tests DOIVENT suivre les bonnes pratiques Rust (structure `#[cfg(test)]`, tests d'intégration dans `tests/integration/`).
69 +
70 +**Rationale**: Les tests garantissent la qualité et la fiabilité du code. Une couverture élevée réduit les régressions et facilite les refactorings. L'exécution automatique des tests avant commit maintient la qualité du codebase.
71 +
72 +## Technical Constraints
73 +
74 +### Framework Requirements
75 +
76 +- **Rust Version**: Rust 1.91.1+ (Edition 2024) - **IMPORTANT**: Version minimale requise pour Axum/SeaORM
77 +- **Web Framework**: Axum 0.8.7+ (dernière version stable)
78 +- **ORM**: SeaORM 2.0.x+ (requis pour toutes les interactions DB)
79 +- **Runtime**: Tokio 1.x (requis par Axum)
80 +- **Database**: PostgreSQL 18+ (recommandé)
81 +
82 +### Development Environment
83 +
84 +**Docker pour Base de Données** (amendement v0.5.2): En développement, la base de données PostgreSQL DOIT être lancée via Docker depuis le dossier `database/` à la racine du projet. Le développeur DOIT utiliser le fichier `database/docker-compose.yml` pour démarrer la base de données. La base de données est lancée et gérée par le développeur (pas automatiquement par le système). Cette configuration garantit un environnement de développement cohérent et isolé, facilite le partage de configuration entre développeurs, et permet un reset rapide de la base de données pour les tests.
85 +
86 +**Rationale**: L'utilisation de Docker pour PostgreSQL en développement standardise l'environnement de développement, évite les conflits de versions entre développeurs, et simplifie la configuration initiale. Le développeur garde le contrôle sur le démarrage/arrêt de la base de données, permettant une meilleure gestion des ressources et une flexibilité dans le workflow de développement.
87 +
88 +### Code Quality Requirements
89 +
90 +- Toutes les fonctions publiques DOIVENT avoir une documentation `///`
91 +- `cargo clippy -- -D warnings` DOIT passer sans erreur
92 +- `cargo fmt` DOIT être appliqué avant chaque commit
93 +- Les erreurs DOIVENT utiliser des types explicites (`Result<T, AppError>`)
94 +- `#![deny(warnings)]` DOIT être activé dans `src/lib.rs` ou `src/main.rs` (conformité ANSSI)
95 +- Les `unwrap()` et `expect()` sont INTERDITS dans le code de production (conformité ANSSI)
96 +- Toute utilisation de `unsafe` DOIT être documentée avec `// SAFETY:` (conformité ANSSI)
97 +
98 +### Testing Requirements
99 +
100 +- Tests unitaires dans le même fichier avec `#[cfg(test)]` ou dans `tests/unit/`
101 +- Tests d'intégration dans `tests/integration/`
102 +- Tests de contrat dans `tests/contract/`
103 +- Couverture > 80% (objectif > 90% pour auth, security, core services)
104 +
105 +### Security Requirements
106 +
107 +- Pas de secrets hardcodés (variables d'environnement uniquement)
108 +- Validation de toutes les entrées utilisateur
109 +- Hashing des mots de passe (bcrypt, jamais en clair)
110 +- JWT tokens: extraction depuis header `Authorization: Bearer <token>` (priorité 1)
111 +- Cookies HttpOnly pour les clients web (fallback, priorité 2)
112 +- Rate limiting sur les endpoints d'authentification
113 +- Logging de tous les événements d'authentification (succès et échecs)
114 +- Conformité aux guidelines ANSSI Rust (voir `docs/ANSSI_COMPLIANCE_REPORT.md`)
115 +- Code `unsafe` minimisé et documenté avec `// SAFETY:`
116 +- Gestion d'erreurs explicite (pas de panics en production)
117 +
118 +## Development Workflow
119 +
120 +### Code Review Process
121 +
122 +Toutes les pull requests DOIVENT être revues avant merge. Les reviewers DOIVENT vérifier la conformité avec cette constitution. Les violations constitutionnelles DOIVENT être corrigées avant merge.
123 +
124 +### Quality Gates
125 +
126 +- ✅ `cargo build` passe sans erreur
127 +- ✅ `cargo test` passe (tous les tests)
128 +- ✅ `cargo clippy -- -D warnings` passe
129 +- ✅ `cargo fmt --check` passe
130 +- ✅ Couverture de tests > 80%
131 +- ✅ Documentation complète (`cargo doc` génère sans warnings)
132 +- ✅ `cargo audit` ne signale pas de vulnérabilités (avant release, conformité ANSSI)
133 +- ✅ `cargo outdated` a été vérifié (avant release, conformité ANSSI)
134 +- ✅ Aucun `unwrap()`/`expect()` dans le code de production (sauf tests, conformité ANSSI)
135 +- ✅ Tous les blocs `unsafe` sont documentés avec `// SAFETY:` (conformité ANSSI)
136 +
137 +### Testing Strategy
138 +
139 +1. **TDD recommandé**: Écrire les tests avant l'implémentation pour les nouvelles fonctionnalités
140 +2. **Tests unitaires**: Pour chaque fonction publique, tester les cas normaux et d'erreur
141 +3. **Tests d'intégration**: Pour chaque endpoint API, tester les scénarios complets
142 +4. **Tests de contrat**: Pour chaque endpoint public, valider le format de réponse
143 +
144 +## Governance
145 +
146 +Cette constitution prévaut sur toutes les autres pratiques du projet. Les amendements nécessitent une documentation, une approbation et un plan de migration si nécessaire.
147 +
148 +**Version**: 0.5.2 | **Ratified**: 2025-12-09 | **Last Amended**: 2025-03-05
149 +
150 +**Compliance**: Toutes les PRs et reviews DOIVENT vérifier la conformité avec cette constitution. La complexité DOIT être justifiée. Utiliser les templates dans `.specify/templates/` pour la documentation des features.
A docs/plans/PLAN_CSP_HTMX.md
+246 -0
@@ -0,0 +1,246 @@
1 +# Plan : Renforcement CSP pour la stack HTMX + Alpine.js + DaisyUI
2 +
3 +**Date :** 4 mai 2026
4 +**Finding :** H2 — CSP neutralisée par `unsafe-inline` et `unsafe-eval`
5 +**Fichier principal :** `crates/rustwarden-core/src/app.rs:588-592`
6 +**Objectif :** Supprimer `'unsafe-inline'` et `'unsafe-eval'` de la CSP sans casser le fonctionnement de l'UI.
7 +
8 +---
9 +
10 +## État actuel
11 +
12 +```
13 +script-src 'self' 'unsafe-inline' 'unsafe-eval';
14 +style-src 'self' 'unsafe-inline';
15 +```
16 +
17 +### Pourquoi c'est nécessaire aujourd'hui
18 +
19 +| Directive | Raison |
20 +|-----------|--------|
21 +| `script-src 'unsafe-eval'` | Alpine.js standard utilise `new Function()` pour évaluer les expressions `x-data`, `x-bind`, etc. |
22 +| `script-src 'unsafe-inline'` | Scripts `<script>` inline dans les templates Askama (init Alpine, handlers HTMX). |
23 +| `style-src 'unsafe-inline'` | Attributs `style=""` sur les éléments, styles dynamiques Alpine (`x-bind:style`), classes Tailwind générées à la volée par DaisyUI. |
24 +
25 +---
26 +
27 +## Plan en 4 phases
28 +
29 +### Phase 1 : Migrer vers Alpine.js CSP build (supprime `unsafe-eval`)
30 +
31 +**Effort :** Faible
32 +**Impact :** Supprime `'unsafe-eval'` de `script-src`
33 +
34 +Alpine.js fournit un build CSP-compatible (`@alpinejs/csp`) qui pré-compile les expressions au lieu d'utiliser `eval()`.
35 +
36 +#### Actions
37 +
38 +1. **Remplacer le CDN/import Alpine.js** par `@alpinejs/csp` dans les assets statiques :
39 + ```html
40 + <!-- AVANT -->
41 + <script src="/static/js/alpine.min.js" defer></script>
42 + <!-- APRÈS -->
43 + <script src="/static/js/alpine-csp.min.js" defer></script>
44 + ```
45 +
46 +2. **Adapter les expressions Alpine** si nécessaire. Le build CSP ne supporte pas :
47 + - `x-data="{ open: false }"` avec des expressions complexes inline
48 + - `x-on:click="count++"` avec des expressions JavaScript arbitraires
49 +
50 + **Alternative :** Déplacer la logique dans des composants Alpine enregistrés :
51 + ```javascript
52 + // static/js/components.js
53 + Alpine.data('dropdown', () => ({
54 + open: false,
55 + toggle() { this.open = !this.open }
56 + }));
57 + ```
58 + ```html
59 + <div x-data="dropdown">
60 + <button x-on:click="toggle">Menu</button>
61 + </div>
62 + ```
63 +
64 +3. **Inventaire des templates** utilisant Alpine inline :
65 + - `templates/admin/*.html` — modals, dropdowns, toggles
66 + - `templates/settings/*.html` — préférences, sécurité TOTP
67 + - `templates/login.html` — password toggle, 2FA form
68 +
69 + Chaque expression `x-data="{ ... }"` et `x-on:click="..."` doit être auditée.
70 +
71 +4. **Mettre à jour la CSP** :
72 + ```
73 + script-src 'self' 'unsafe-inline'; // unsafe-eval supprimé
74 + ```
75 +
76 +#### Vérification
77 +- Toutes les pages avec Alpine fonctionnent (login, admin, settings, profil)
78 +- Console navigateur : aucune erreur CSP
79 +
80 +---
81 +
82 +### Phase 2 : Externaliser les scripts inline (supprime `unsafe-inline` pour scripts)
83 +
84 +**Effort :** Moyen
85 +**Impact :** Supprime `'unsafe-inline'` de `script-src`
86 +
87 +#### Actions
88 +
89 +1. **Identifier tous les `<script>` inline** dans les templates Askama :
90 + ```bash
91 + grep -rn '<script>' crates/rustwarden-ui/templates/
92 + ```
93 +
94 +2. **Extraire chaque bloc `<script>` dans un fichier `.js` externe** :
95 + - `static/js/login.js` — logique login, 2FA, redirect
96 + - `static/js/admin.js` — modals, CRUD utilisateurs, gestion rôles
97 + - `static/js/settings.js` — password change, TOTP setup, preferences
98 + - `static/js/common.js` — CSRF token injection, notifications
99 +
100 +3. **Gérer les variables serveur** injectées dans les scripts inline.
101 +
102 + Pattern actuel (Askama → script inline) :
103 + ```html
104 + <script>
105 + const csrfToken = "{{ csrf_token }}";
106 + const userId = {{ user_id }};
107 + </script>
108 + ```
109 +
110 + Pattern cible (data attributes → script externe) :
111 + ```html
112 + <div id="app-data"
113 + data-csrf-token="{{ csrf_token }}"
114 + data-user-id="{{ user_id }}">
115 + </div>
116 + <script src="/static/js/settings.js"></script>
117 + ```
118 + ```javascript
119 + // static/js/settings.js
120 + const appData = document.getElementById('app-data').dataset;
121 + const csrfToken = appData.csrfToken;
122 + ```
123 +
124 +4. **Gérer les `|safe` filters Askama** qui injectent du JSON dans `<script>` :
125 +
126 + Pattern actuel (`preferences.html:312-316`) :
127 + ```html
128 + <script>
129 + const themes = {{ themes_json|safe }};
130 + </script>
131 + ```
132 +
133 + Pattern cible :
134 + ```html
135 + <script id="themes-data" type="application/json">{{ themes_json|safe }}</script>
136 + <script src="/static/js/preferences.js"></script>
137 + ```
138 + ```javascript
139 + const themes = JSON.parse(document.getElementById('themes-data').textContent);
140 + ```
141 +
142 +5. **Configurer HTMX pour fonctionner sans inline scripts** :
143 +
144 + HTMX n'utilise pas `eval()` par défaut. Cependant, certains attributs HTMX peuvent exécuter du JS :
145 + - `hx-on:*` (exécute du JS inline) → Migrer vers des event listeners externes
146 + - `hx-vals='js:...'` → Migrer vers `hx-vals` avec JSON statique ou `hx-include`
147 +
148 + Inventorier les usages :
149 + ```bash
150 + grep -rn 'hx-on:' crates/rustwarden-ui/templates/
151 + grep -rn "hx-vals.*js:" crates/rustwarden-ui/templates/
152 + ```
153 +
154 +6. **Mettre à jour la CSP** :
155 + ```
156 + script-src 'self'; // Plus de unsafe-inline ni unsafe-eval
157 + ```
158 +
159 +#### Vérification
160 +- Toutes les fonctionnalités JS opérationnelles
161 +- Console navigateur : aucune violation CSP pour scripts
162 +- Tests manuels : login, 2FA, admin CRUD, settings
163 +
164 +---
165 +
166 +### Phase 3 : Traiter `innerHTML` (corrige finding M3)
167 +
168 +**Effort :** Faible
169 +**Impact :** Élimine les vecteurs XSS dans le DOM
170 +
171 +#### Actions
172 +
173 +1. **Remplacer `innerHTML` par `textContent`** partout où du texte brut est injecté :
174 + - `templates/admin/users_list.html:434,459`
175 + - `templates/settings/password.html:321,333,349`
176 +
177 +2. **Pour les cas nécessitant du HTML** (modals avec formatage), utiliser la construction DOM :
178 + ```javascript
179 + // AVANT
180 + element.innerHTML = `<strong>${username}</strong>`;
181 +
182 + // APRÈS
183 + const strong = document.createElement('strong');
184 + strong.textContent = username;
185 + element.replaceChildren(strong);
186 + ```
187 +
188 +3. **Auditer les réponses HTMX** : HTMX injecte les réponses serveur via `innerHTML` par design. C'est sûr tant que :
189 + - Les réponses HTML sont générées par Askama (auto-escaped)
190 + - Aucune donnée utilisateur non-échappée n'est dans les fragments HTML
191 +
192 + Vérifier qu'aucun handler ne renvoie du HTML construit par string concatenation.
193 +
194 +#### Vérification
195 +- `grep -rn 'innerHTML' templates/` → zéro résultat (hors HTMX internals)
196 +- Tests fonctionnels des modals et formulaires
197 +
198 +---
199 +
200 +### Phase 4 : Réduire `unsafe-inline` pour les styles (optionnel)
201 +
202 +**Effort :** Élevé
203 +**Impact :** Sécurité marginale (XSS via CSS est rare et peu exploitable)
204 +
205 +`'unsafe-inline'` pour les styles est beaucoup moins dangereux que pour les scripts. Les attaques CSS-only sont limitées (exfiltration de données via `background-image: url(...)`, pas d'exécution de code).
206 +
207 +#### Contraintes
208 +
209 +- **Tailwind/DaisyUI** : Les classes utilitaires sont dans le CSS compilé (pas inline). OK.
210 +- **Alpine.js `x-bind:style`** : Génère des attributs `style=""` dynamiquement. Nécessiterait des nonces.
211 +- **HTMX `htmx-settling`, `htmx-swapping`** : Classes CSS ajoutées dynamiquement. OK (pas de styles inline).
212 +
213 +#### Si souhaité
214 +
215 +1. Implémenter un middleware de nonce CSP :
216 + - Générer un nonce par requête (`rand::random::<[u8; 16]>()`)
217 + - L'injecter dans le header CSP : `style-src 'self' 'nonce-{nonce}'`
218 + - Le passer au contexte Askama pour les templates
219 + - Ajouter `nonce="{nonce}"` sur chaque `<style>` tag
220 +
221 +2. Alternative : utiliser des hashes CSP pour les blocs `<style>` statiques.
222 +
223 +#### Recommandation
224 +
225 +**Reporter cette phase.** Le gain sécurité est marginal et le coût d'implémentation est élevé avec Alpine.js. Prioriser les phases 1-3 qui éliminent les vrais vecteurs XSS.
226 +
227 +---
228 +
229 +## Résumé des changements CSP par phase
230 +
231 +| Phase | CSP résultante | `unsafe-eval` | `unsafe-inline` (scripts) | `unsafe-inline` (styles) |
232 +|-------|---------------|---------------|--------------------------|--------------------------|
233 +| Actuel | `script-src 'self' 'unsafe-inline' 'unsafe-eval'` | Oui | Oui | Oui |
234 +| Phase 1 | `script-src 'self' 'unsafe-inline'` | **Non** | Oui | Oui |
235 +| Phase 2 | `script-src 'self'` | **Non** | **Non** | Oui |
236 +| Phase 3 | (pas de changement CSP, correction DOM) | — | — | — |
237 +| Phase 4 | `style-src 'self' 'nonce-...'` | **Non** | **Non** | **Non** |
238 +
239 +---
240 +
241 +## Priorité recommandée
242 +
243 +1. **Phase 1** (immédiat) — Quick win, faible effort
244 +2. **Phase 3** (immédiat) — Quick win, corrections `innerHTML`
245 +3. **Phase 2** (1-2 semaines) — Chantier principal
246 +4. **Phase 4** (backlog) — Optionnel, gain marginal
D docs/spec-remember-me.md
+0 -282
@@ -1,282 +0,0 @@
1 -# Spécification : "Se souvenir de moi" (Remember Me)
2 -
3 -**Version** : 1.0
4 -**Date** : 2026-03-23
5 -**Composant** : `rustwarden-core` (crate framework)
6 -**Statut** : Proposition
7 -
8 ----
9 -
10 -## 1. Contexte et problème
11 -
12 -Les utilisateurs perdent leur session fréquemment à cause de la combinaison de trois timers :
13 -
14 -| Timer | Valeur par défaut | Effet |
15 -|-------|-------------------|-------|
16 -| `JWT_EXPIRATION_MINUTES` | 15 min | Cookie `jwt_token` expiré, requêtes rejetées |
17 -| `SESSION_TIMEOUT_MINUTES` | 30 min | Session serveur invalidée après inactivité |
18 -| `REFRESH_TOKEN_EXPIRATION_DAYS` | 7 jours | Impossibilité de renouveler le JWT |
19 -
20 -Sans mécanisme de renouvellement automatique côté client, dès que le JWT expire (15 min), l'utilisateur est déconnecté même si son refresh token est encore valide.
21 -
22 -## 2. Objectif
23 -
24 -Ajouter un champ optionnel `remember_me` au flux d'authentification pour permettre aux utilisateurs de maintenir une session longue durée tout en préservant la sécurité du JWT court.
25 -
26 -**Principe fondamental** : le JWT (access token) reste toujours court (5-15 min). Seule la durée du refresh token et du cookie de refresh changent selon le choix de l'utilisateur.
27 -
28 -## 3. Changements requis
29 -
30 -### 3.1 `LoginRequest` — Nouveau champ
31 -
32 -**Fichier** : `crates/rustwarden-core/src/auth/handlers.rs` (ligne 43)
33 -
34 -```rust
35 -#[derive(Debug, Deserialize, Validate)]
36 -pub struct LoginRequest {
37 - #[validate(length(min = 1))]
38 - pub username: String,
39 - #[validate(length(min = 1))]
40 - pub password: String,
41 - /// Si true, le refresh token a une durée de vie étendue.
42 - /// Défaut : false (rétrocompatibilité).
43 - #[serde(default)]
44 - pub remember_me: bool,
45 -}
46 -```
47 -
48 -`#[serde(default)]` garantit la **rétrocompatibilité** : les clients existants qui n'envoient pas ce champ obtiennent `false`.
49 -
50 -### 3.2 `AuthConfig` — Nouvelle variable d'environnement
51 -
52 -**Fichier** : `crates/rustwarden-core/src/config/auth.rs`
53 -
54 -```rust
55 -pub struct AuthConfig {
56 - // ... champs existants ...
57 -
58 - /// Durée du refresh token quand "remember me" est activé.
59 - /// Variable : REMEMBER_ME_EXPIRATION_DAYS (défaut : 30)
60 - pub remember_me_expiration_days: i64,
61 -}
62 -```
63 -
64 -Chargement :
65 -
66 -```rust
67 -let remember_me_expiration_days = env::var("REMEMBER_ME_EXPIRATION_DAYS")
68 - .ok()
69 - .and_then(|v| v.parse::<i64>().ok())
70 - .unwrap_or(30);
71 -```
72 -
73 -**Contrainte de validation** : `remember_me_expiration_days` doit être >= `refresh_token_expiration_days` et <= 90 jours. Log un warning si > 60 jours.
74 -
75 -### 3.3 Handler `login()` — Logique conditionnelle
76 -
77 -**Fichier** : `crates/rustwarden-core/src/auth/handlers.rs`, fonction `login()`
78 -
79 -Modifier la création du refresh token (actuellement ligne 300-305) :
80 -
81 -```rust
82 -// Avant (actuel) :
83 -let created_rt = RefreshTokenService::create_refresh_token(
84 - &db,
85 - user.id,
86 - auth_config.refresh_token_expiration_days,
87 -).await?;
88 -
89 -// Après :
90 -let rt_expiration_days = if payload.remember_me {
91 - auth_config.remember_me_expiration_days
92 -} else {
93 - auth_config.refresh_token_expiration_days
94 -};
95 -
96 -let created_rt = RefreshTokenService::create_refresh_token(
97 - &db,
98 - user.id,
99 - rt_expiration_days,
100 -).await?;
101 -```
102 -
103 -### 3.4 `LoginResponse` — Information de durée
104 -
105 -**Fichier** : `crates/rustwarden-core/src/auth/handlers.rs`
106 -
107 -Ajouter un champ pour que le client sache quand renouveler :
108 -
109 -```rust
110 -#[derive(Debug, Serialize)]
111 -pub struct LoginResponse {
112 - pub access_token: String,
113 - pub refresh_token: Option<String>,
114 - pub token_type: String,
115 - pub expires_in: i64,
116 - /// Durée de vie du refresh token en secondes.
117 - /// Permet au client de planifier le renouvellement ou d'afficher
118 - /// la durée restante de la session.
119 - pub refresh_expires_in: i64,
120 - pub user: UserInfo,
121 -}
122 -```
123 -
124 -Valeur : `rt_expiration_days * 86400`.
125 -
126 -### 3.5 Endpoint `refresh_token()` — Propagation de la durée
127 -
128 -**Fichier** : `crates/rustwarden-core/src/auth/handlers.rs`, fonction `refresh_token()`
129 -
130 -Problème : lors du renouvellement, la durée du nouveau refresh token utilise `auth_config.refresh_token_expiration_days` (7 jours), ce qui **raccourcit** un token "remember me" de 30 jours à chaque rotation.
131 -
132 -**Solution** : conserver la durée restante originale ou la durée "remember me" lors de la rotation.
133 -
134 -Option recommandée — **conserver la durée configurée du token d'origine** :
135 -
136 -```rust
137 -// Dans refresh_token(), après validation du old_token :
138 -let original_duration_days = (old_token.expires_at - old_token.created_at).num_days();
139 -
140 -// Utiliser cette durée pour le nouveau token (plafonné à remember_me_expiration_days)
141 -let rt_expiration_days = original_duration_days
142 - .min(auth_config.remember_me_expiration_days)
143 - .max(auth_config.refresh_token_expiration_days);
144 -
145 -let new_created_rt = RefreshTokenService::create_refresh_token(
146 - &db,
147 - user.id,
148 - rt_expiration_days,
149 -).await?;
150 -```
151 -
152 -Ainsi, un token "remember me" de 30 jours génère un nouveau token de 30 jours à chaque rotation, sans jamais dépasser la limite configurée.
153 -
154 -### 3.6 `RefreshResponse` — Même ajout
155 -
156 -```rust
157 -#[derive(Debug, Serialize)]
158 -pub struct RefreshResponse {
159 - pub access_token: String,
160 - pub refresh_token: Option<String>,
161 - pub token_type: String,
162 - pub expires_in: i64,
163 - pub refresh_expires_in: i64, // Nouveau
164 -}
165 -```
166 -
167 -## 4. Variables d'environnement
168 -
169 -| Variable | Défaut | Description |
170 -|----------|--------|-------------|
171 -| `REMEMBER_ME_EXPIRATION_DAYS` | 30 | Durée du refresh token avec "remember me" |
172 -| `REFRESH_TOKEN_EXPIRATION_DAYS` | 7 | Durée du refresh token sans "remember me" (inchangé) |
173 -| `JWT_EXPIRATION_MINUTES` | 15 | Durée du JWT access token (inchangé) |
174 -
175 -## 5. Matrice de sécurité
176 -
177 -| Vecteur d'attaque | Mitigation | Statut |
178 -|-------------------|------------|--------|
179 -| Vol du JWT | Durée courte (15 min), inchangée | Existant |
180 -| Vol du refresh token (localStorage) | Rotation + détection de réutilisation + hash SHA-256 en base | Existant |
181 -| Brute-force refresh | Rate limiting (5/min/IP) | Existant |
182 -| Accumulation de tokens | Limite 10 tokens actifs/utilisateur | Existant |
183 -| Session longue après vol de credentials | Le refresh token étendu ne bypass pas la révocation au logout ni au changement de mot de passe | Existant |
184 -| Refresh token "remember me" volé | Fenêtre d'exploitation plus longue (30j vs 7j) — **risque accepté** car mitigé par rotation et détection de réutilisation | Nouveau — acceptable |
185 -| Appareil partagé / oubli de déconnexion | L'utilisateur choisit explicitement "Se souvenir de moi" — pas activé par défaut | Par design |
186 -
187 -### Recommandation complémentaire (optionnelle, hors scope v1)
188 -
189 -Pour les applications sensibles, envisager à terme :
190 -- **Révocation par appareil** : endpoint `DELETE /api/v1/auth/sessions/{token_id}` pour révoquer un refresh token spécifique depuis un écran "Appareils connectés"
191 -- **Notification de nouvelle connexion** : email à l'utilisateur lors d'un login depuis un nouveau user-agent/IP
192 -
193 -## 6. Rétrocompatibilité
194 -
195 -| Composant | Impact |
196 -|-----------|--------|
197 -| API `POST /api/v1/auth/login` | Aucun — `remember_me` est optionnel (`#[serde(default)]`) |
198 -| API `POST /api/v1/auth/refresh` | Aucun — le comportement par défaut reste 7 jours |
199 -| `LoginResponse` / `RefreshResponse` | Nouveau champ `refresh_expires_in` — les clients qui ne le lisent pas ne sont pas affectés |
200 -| `.env` | Aucune variable existante modifiée. `REMEMBER_ME_EXPIRATION_DAYS` est optionnelle |
201 -| Applications consommatrices | Aucune modification requise. Le comportement est identique si `remember_me` n'est pas envoyé |
202 -
203 -## 7. Côté client (hors scope rustwarden-core, pour l'application consommatrice)
204 -
205 -Pour que la fonctionnalité soit complète, l'application consommatrice (ex : PasterMan) devra :
206 -
207 -1. **Checkbox dans le formulaire de login** : envoyer `"remember_me": true` dans le body JSON
208 -2. **Stocker le flag** : `localStorage.setItem("remember_me", "true")` si coché
209 -3. **Auto-refresh JS** : si `remember_me === "true"`, mettre en place un `setInterval` qui appelle `POST /api/v1/auth/refresh` toutes les `(JWT_EXPIRATION - 2 min)` (soit ~13 min par défaut) avec le refresh token stocké
210 -4. **Mise à jour du refresh token** : à chaque réponse de refresh, remplacer l'ancien refresh token en localStorage par le nouveau (rotation)
211 -5. **Nettoyage au logout** : supprimer `remember_me` et `refresh_token` du localStorage
212 -
213 -Exemple de timer auto-refresh :
214 -
215 -```javascript
216 -function setupAutoRefresh(expiresInSeconds) {
217 - const refreshInterval = (expiresInSeconds - 120) * 1000; // 2 min avant expiration
218 - setInterval(async () => {
219 - const refreshToken = localStorage.getItem("refresh_token");
220 - if (!refreshToken || localStorage.getItem("remember_me") !== "true") return;
221 -
222 - const response = await fetch("/api/v1/auth/refresh", {
223 - method: "POST",
224 - headers: { "Content-Type": "application/json" },
225 - credentials: "include",
226 - body: JSON.stringify({ refresh_token: refreshToken }),
227 - });
228 -
229 - if (response.ok) {
230 - const data = await response.json();
231 - if (data.refresh_token) {
232 - localStorage.setItem("refresh_token", data.refresh_token);
233 - }
234 - // Le nouveau JWT est automatiquement set via le cookie HttpOnly par le serveur
235 - } else {
236 - // Token expiré ou révoqué — rediriger vers login
237 - localStorage.removeItem("refresh_token");
238 - localStorage.removeItem("remember_me");
239 - window.location.href = "/login";
240 - }
241 - }, refreshInterval);
242 -}
243 -```
244 -
245 -> **Note** : l'endpoint `POST /api/v1/auth/refresh` ne set pas actuellement de cookie `jwt_token` dans la réponse (contrairement à `login()`). Il faudra soit ajouter ce set-cookie dans le handler `refresh_token()`, soit que le client utilise le `access_token` de la réponse JSON pour un header `Authorization: Bearer`. La première option (set-cookie) est recommandée pour la cohérence avec le flux web.
246 -
247 -## 8. Tests à ajouter
248 -
249 -### Tests unitaires (`crates/rustwarden-core`)
250 -
251 -1. **`test_login_without_remember_me`** — `remember_me` absent ou `false` → refresh token avec durée standard (7j)
252 -2. **`test_login_with_remember_me`** — `remember_me: true` → refresh token avec durée étendue (30j)
253 -3. **`test_remember_me_default_false`** — Désérialisation d'un body sans champ `remember_me` → `false`
254 -4. **`test_refresh_preserves_duration`** — Rotation d'un token "remember me" → le nouveau token conserve la durée étendue
255 -5. **`test_refresh_caps_duration`** — La durée du nouveau token ne dépasse jamais `remember_me_expiration_days`
256 -6. **`test_remember_me_config_validation`** — `REMEMBER_ME_EXPIRATION_DAYS` < `REFRESH_TOKEN_EXPIRATION_DAYS` → erreur de configuration
257 -7. **`test_login_response_includes_refresh_expires_in`** — Le champ `refresh_expires_in` est présent et correct
258 -
259 -### Tests d'intégration
260 -
261 -8. **`test_full_remember_me_flow`** — Login avec `remember_me: true` → attendre expiration JWT → refresh → vérifier nouveau JWT valide
262 -9. **`test_logout_revokes_remember_me_tokens`** — Login "remember me" → logout → refresh échoue
263 -
264 -## 9. Fichiers impactés (résumé)
265 -
266 -| Fichier | Modification |
267 -|---------|-------------|
268 -| `crates/rustwarden-core/src/config/auth.rs` | Ajouter `remember_me_expiration_days` à `AuthConfig` |
269 -| `crates/rustwarden-core/src/auth/handlers.rs` | `LoginRequest` + champ, `LoginResponse` + champ, `RefreshResponse` + champ, logique dans `login()` et `refresh_token()` |
270 -| `.env.example` | Documenter `REMEMBER_ME_EXPIRATION_DAYS` |
271 -
272 -Aucune migration de base de données nécessaire — la table `refresh_token` stocke déjà `expires_at` par token.
273 -
274 -## 10. Critères d'acceptation
275 -
276 -- [ ] Un client envoyant `{"username":"x","password":"y"}` (sans `remember_me`) a un comportement identique à l'actuel
277 -- [ ] Un client envoyant `{"username":"x","password":"y","remember_me":true}` reçoit un refresh token avec `expires_at` = now + `REMEMBER_ME_EXPIRATION_DAYS`
278 -- [ ] La rotation du refresh token conserve la durée d'origine (pas de raccourcissement à 7j)
279 -- [ ] `LoginResponse` et `RefreshResponse` incluent `refresh_expires_in` en secondes
280 -- [ ] La durée maximale est plafonnée par configuration (défaut 30j, max recommandé 90j)
281 -- [ ] Tous les mécanismes de sécurité existants (rotation, détection de réutilisation, révocation au logout, limite par utilisateur) s'appliquent sans modification
282 -- [ ] Les tests existants passent sans modification