# CP Creation ALTCHA Bundle

Bundle Symfony générique pour protéger **uniquement les formulaires choisis** avec le widget ALTCHA `3.2.2` et un service privé ALTCHA v2. La soumission est vérifiée directement par Symfony : aucun endpoint public `/verify` n'est ajouté.

Prérequis : PHP `8.2+` et Symfony `7.4` ou `8.x`.

Le bundle est préparé localement pour le futur dépôt `git@bitbucket.org:cpcreation/altcha-bundle.git`. Il n'est pas encore publié.

## Contrats publics

- Bundle : `CpCreation\AltchaBundle\CpCreationAltchaBundle`
- FormType : `CpCreation\AltchaBundle\Form\AltchaType`
- Client : `CpCreation\AltchaBundle\Contract\AltchaServiceClientInterface`
- Fournisseur de scopes : `CpCreation\AltchaBundle\Contract\ScopeProviderInterface`
- DTO : `CpCreation\AltchaBundle\Model\ScopePolicy`, `Challenge`, `VerificationRequest`, `VerificationResult`, `ScopeManifest`, `ScopeSyncResult`
- Route same-origin : `cp_creation_altcha_challenge`
- Commande : `cp-creation:altcha:scopes:sync`

`AltchaServiceClientInterface` expose `resolvePolicy()`, `createChallenge()`, `verifyAndConsume()` et `synchronizeScopes()`. Les scopes restent des chaînes validées ; le bundle n'impose aucune enum métier.

## Installation locale avant publication

Déclarer temporairement le dossier comme dépôt Composer `path`, puis enregistrer le bundle :

```php
// config/bundles.php
CpCreation\AltchaBundle\CpCreationAltchaBundle::class => ['all' => true],
```

Importer uniquement la route de challenge :

```yaml
# config/routes/cp_creation_altcha.yaml
cp_creation_altcha:
    resource: '@CpCreationAltchaBundle/Resources/config/routes.php'
```

Installer les assets du bundle avec ALTCHA épinglé :

```bash
yarn add --dev @cpcreation/altcha-bundle@file:vendor/cpcreation/altcha-bundle/assets altcha@3.2.2
```

Déclarer ensuite le contrôleur dans le bridge Stimulus du projet ; sans cette
entrée, le bundle échoue volontairement fermé car le miroir du honeypot ne peut
pas être initialisé :

```json
// assets/controllers.json
{
    "controllers": {
        "@cpcreation/altcha-bundle": {
            "altcha": {
                "enabled": true,
                "fetch": "eager",
                "autoimport": {
                    "@cpcreation/altcha-bundle/css/altcha.css": true
                }
            }
        }
    }
}
```

Puis reconstruire les assets avec la commande habituelle du projet. La future
publication devra fournir une recette Symfony Flex ou un configurateur
équivalent et prouver cette installation sur un projet vierge ; l’enregistrement
manuel ci-dessus reste obligatoire tant que cette recette n’existe pas.

## Configuration

```yaml
# config/packages/cp_creation_altcha.yaml
cp_creation_altcha:
    service_url: '%env(ALTCHA_SERVICE_URL)%'
    runtime_token: '%env(ALTCHA_RUNTIME_TOKEN)%'
    scope_sync_token: '%env(ALTCHA_SCOPE_SYNC_TOKEN)%'
    envelope_secret: '%env(ALTCHA_ENVELOPE_SECRET)%'
    subject_secret: '%env(ALTCHA_SUBJECT_SECRET)%'
    envelope_ttl_seconds: 900
    challenge_limit_per_minute: 30
    submission_limit_per_minute: 10
    scope_source: 'my-symfony-project'
    scopes:
        site.contact: 'Contact'
        custom.project_form: 'Project form'
```

`ALTCHA_ENVELOPE_SECRET` doit être un secret local aléatoire d'au moins 32 octets. Le token runtime ne permet ni de déclarer ni d'activer un scope ; la synchronisation utilise une identité `ScopeSync` distincte.

Symfony doit configurer ses véritables `trusted_proxies` et `trusted_headers`, car les quotas IP reposent sur `Request::getClientIp()`. Le service Docker reste joignable seulement par PHP via `127.0.0.1`.

## Protéger un formulaire

### Formulaire Symfony classique

```php
use CpCreation\AltchaBundle\Form\AltchaType;

$builder->add('antibot', AltchaType::class, [
    'scope' => 'site.contact',
]);
```

La soumission HTTP classique vérifie et consomme toujours la preuve directement
pendant le traitement du formulaire.

### Formulaire LiveComponent

```php
use CpCreation\AltchaBundle\Form\AltchaType;

$builder->add('antibot', AltchaType::class, [
    'scope' => 'security.registration_confirm',
    'partial_live_actions' => ['validatePasswords'],
    'final_live_actions' => ['submitRegisterConfirm'],
]);
```

Options publiques :

- `scope` : obligatoire, déclaré dans le registre ;
- `final_live_actions` : liste explicite des actions LiveComponent finales qui vérifient et consomment la preuve, défaut `[]`. Quand elle est renseignée, un rendu passif ou une autre action Live conserve la preuve ; la réponse d'une action finale déclenche seule le reset du widget ;
- `partial_live_actions` : liste explicite des actions LiveComponent partielles qui ne soumettent pas le formulaire final, défaut `[]`. Elle assure la compatibilité avec les formulaires qui ne déclarent pas encore `final_live_actions` ;
- `subject_limits_enabled` : défaut `false` ;
- `subject_path` : chemin du sujet seulement si une limite sujet est volontairement activée.

Le honeypot est intrinsèque à tout scope protégé : son nom reste opaque,
aléatoire et signé, et il n'est jamais choisi ni désactivé par le projet.

Le contrôleur aligne automatiquement le `minDuration` du widget sur le
`minimumFillTimeMs` de la politique distante (avec le minimum ALTCHA de 500 ms) :
la coche verte ne précède pas l'acceptation serveur et le projet hôte n'a
aucune option manuelle à ajouter.

Une action ne peut pas figurer à la fois dans `partial_live_actions` et
`final_live_actions` : cette configuration est rejetée dès la création du
formulaire. Quand `final_live_actions` reste vide, une action Live inconnue est
considérée comme finale et échoue fermée, comme dans les versions précédentes.
Quand l'allowlist finale est renseignée, seules les actions listées consomment
la preuve ; les requêtes HTTP classiques restent toujours protégées.

Sur un formulaire LiveComponent, synchroniser les champs sans rendu implicite
évite qu'un changement de payload ALTCHA provoque un aller-retour intermédiaire.
Les actions explicites comme `validatePasswords` continuent de fonctionner :

```twig
{{ form_start(form, {
    attr: {
        'data-model': 'norender|*',
        'data-action': 'live#action:prevent',
        'data-live-action-param': 'submitRegisterConfirm'
    }
}) }}
```

Toute surcharge de template ou de thème doit préserver
`data-model="norender|*"`, `data-action="live#action:prevent"` et un
`data-live-action-param` identique à l'action déclarée dans
`final_live_actions`. Supprimer ou désynchroniser l'un de ces attributs peut
réintroduire un rendu intermédiaire et la perte visuelle de la preuve ALTCHA.

Le contrôleur lit l'action réellement envoyée dans
`request:started.requestConfig.actions`. Un simple événement DOM `submit`, un
rendu passif ou une action non listée n'arme donc jamais le reset. Pour laisser
un formulaire non protégé — par exemple le checkout — ne pas lui ajouter
`AltchaType`.

Aucun dispatch manuel n'est nécessaire dans le flux LiveComponent normal : la
réponse de l'action déclarée dans `final_live_actions` réinitialise
automatiquement la preuve consommée et le widget.

L'événement suivant est réservé à un reset métier hors de cette réponse finale,
par exemple lors de l'annulation explicite ou du remplacement d'un formulaire.
Il permet de cibler un seul formulaire sans toucher aux autres :

```js
window.dispatchEvent(new CustomEvent('cpcreation-altcha:reset', {
    detail: { form: document.querySelector('#contact-form') },
}));
```

## Scopes fournis par un autre bundle

Un bundle métier peut contribuer ses scopes sans modifier la configuration du projet :

```php
use CpCreation\AltchaBundle\Contract\ScopeProviderInterface;
use CpCreation\AltchaBundle\Model\ScopeDefinition;

final class ProjectAltchaScopes implements ScopeProviderInterface
{
    public function scopes(): iterable
    {
        yield new ScopeDefinition('crm.quote_request', 'Demande de devis');
    }
}
```

Avec l'autoconfiguration Symfony, ce service reçoit automatiquement le tag `cp_creation_altcha.scope_provider`. La configuration YAML et les providers peuvent coexister.

Comparer les déclarations sans mutation (mode par défaut) :

```bash
php bin/console cp-creation:altcha:scopes:sync
```

Appliquer ensuite explicitement :

```bash
php bin/console cp-creation:altcha:scopes:sync --apply
```

## Protocole HTTP interne

- `POST /v1/policies/resolve` : `{host, scopes:[...]}` ;
- `POST /v1/challenges` : `{host, scope, clientIp, formToken, policyVersion}` ;
- `POST /v1/verifications` : `{host, scope, clientIp, formToken, payload, subjectHash?}` ;
- `PUT /v1/scopes` : `{source, dryRun, scopes:[{name,label}]}` avec `Authorization: ScopeSync …`.

Les appels runtime utilisent `Authorization: Bearer …`. Les échecs `replay`, `policy_changed`, `expired`, `invalid_solution`, `rate_limited`, `unavailable` et `forbidden_context` restent typés. Le formulaire conserve les autres champs, renouvelle son enveloppe et peut obtenir un nouveau challenge.

## Ordre des protections

Lors d'une soumission finale, le bundle applique : quota local IP + scope, politique distante versionnée, enveloppe HMAC liée au scope et au domaine (`iat`, `exp`, `nonce`), honeypot opaque, délai minimal, puis seulement la vérification/consommation ALTCHA. Une barrière de groupes de validation empêche les validateurs métier coûteux de s'exécuter après un rejet précoce.

La politique distante distingue `enabled`, `altchaEnabled` et le profil
cryptographique. Le honeypot reste local au bundle Symfony. PBKDF2 est le profil
actif initial ; Argon2id ne doit être activé qu'après les essais téléphone.

Le contrôleur enregistre déjà le worker dédié `altcha/workers/argon2id` sous la
forme d'un asset same-origin séparé. Cela prépare les essais Argon2id sans
l'activer : le flag serveur et la policy restent les deux gardes obligatoires.

## Tests locaux

```bash
php vendor/bin/phpunit -c libs/cpcreation/altcha-bundle/phpunit.xml.dist
node libs/cpcreation/altcha-bundle/tests/Frontend/altcha-controller.mjs
```
