@eqs/auth : aucun chemin de migration des comptes, sessions, clés API et groupes d'un dépôt rétrofitté #80

Closed
opened 2026-09-21 06:55:07 +00:00 by nicolas-fryder · 2 comments

Ce qui manque. Le cahier de @eqs/auth (§8) abandonne LocalSession, ApiToken, permissions.ts et les deux formats de hachage d'ATLAS, et (§10) affirme que « la migration des comptes scrypt d'ATLAS se fait sans réinitialisation ». C'est la seule phrase sur la migration d'un existant. Or ATLAS est le dépôt rétrofitté (D-001, ROADMAP #15), et il porte aujourd'hui, en production :

  • 22 comptes dans local_users (scrypt N=2^16 au format scrypt:<sel>:<hash>, quelques PBKDF2 hérités), avec role (USER|EDITOR|ADMIN), active, forcePasswordChange, passwordSetBySeed ;
  • 90 sessions dans local_sessions (cookie eq-atlas-session) ;
  • 3 clés API dans api_tokens (SHA-256 de la clé, préfixe eqatlas_, scopes[] = codes de permission, révocation, expiration, lastUsedAt) — dont celle du worker ;
  • 4 groupes × 70 permissions (groups, group_permissions, user_groups), plus external_identities (Entra) et login_throttle (verrou par compte, persisté).

Le fragment schemas/auth.prisma définit user, session, account, verification, api_key. Rien ne dit comment on y arrive depuis un schéma existant.

Questions, dans l'ordre où elles bloquent le rétrofit :

  1. Mots de passe. Quel est le hachage attendu par creerAuth et comment déclarer le vérificateur personnalisé ? Le format d'ATLAS est scrypt:<sel hex>:<dérivé hex> (N=65 536, r=8, p=1, keylen 64) et pbkdf2:<itérations>:<sel>:<dérivé> pour l'hérité. Faut-il les recopier tels quels dans account.password, ou les re-hacher au premier login (ce qu'ATLAS fait déjà pour PBKDF2 → scrypt) ?
  2. Clés API. api_key.hash est un SHA-256 : celui d'ATLAS aussi. Une ligne migrée (hash, préfixe, scopes) est-elle lue telle quelle par principalDepuisCle, ou le préfixe doit-il valoir API_KEY_PREFIX pour que la clé soit reconnue ? Concrètement : peut-on garder eqatlas_ en posant API_KEY_PREFIX=eqatlas_, ou faut-il réémettre les clés (donc reconfigurer le worker et les appelants) ?
  3. Scopes. Les scopes d'ATLAS sont des codes de permission (ingest.monuments) ; ceux du socle sont des operationId (monuments.importer). Une clé migrée doit-elle voir ses scopes traduits, et par qui ?
  4. Sessions. Faut-il tenter de migrer local_sessions vers session (token, expiration), ou assume-t-on une reconnexion de tout le monde ? Si oui, il faut le dire dans le cahier : c'est une décision d'exploitation.
  5. Groupes → rôles. roles.yaml remplace 4 groupes × 70 droits. Existe-t-il, ou est-il prévu, un eqs auth roles --depuis <permissions.ts> ou l'équivalent, ou la traduction est-elle manuelle ? Et LocalUser.role (USER|EDITOR|ADMIN) devient-il user.role tel quel ?
  6. forcePasswordChange / passwordSetBySeed / login_throttle. Ces trois mécanismes sont des décisions d'ATLAS (audit passe 6). Le socle en couvre-t-il l'intention (mot de passe temporaire imposé côté serveur, verrou par compte indépendant des en-têtes), ou restent-ils dans l'application ?

Ce que ça coûte. Bloquant pour l'étape 5 du plan. Sans réponse, chaque choix est une hypothèse qu'un futur rétrofit (photofront, metalfrom, nuage : mêmes questions) refera.

Proposition. Une section « Rétrofit d'un existant » dans le cahier de @eqs/auth, avec le contrat du hachage personnalisé, la règle sur le préfixe des clés, et la position sur les sessions. Et, si les réponses sont mécaniques, une commande eqs migrate auth --plan qui lit le schéma Prisma existant et propose le SQL — le même esprit que eqs migrate plan.

Contexte : rétrofit d'ATLAS, branche epic/retrofit de EQS/ATLAS, docs/retrofit-eqs.md. Je commence par les étapes 0–4 (tooling, config, errors, telemetry), qui n'en dépendent pas.

**Ce qui manque.** Le cahier de `@eqs/auth` (§8) abandonne `LocalSession`, `ApiToken`, `permissions.ts` et les deux formats de hachage d'ATLAS, et (§10) affirme que « la migration des comptes scrypt d'ATLAS se fait sans réinitialisation ». C'est la seule phrase sur la migration d'un existant. Or ATLAS est **le** dépôt rétrofitté (D-001, ROADMAP #15), et il porte aujourd'hui, en production : - 22 comptes dans `local_users` (scrypt N=2^16 au format `scrypt:<sel>:<hash>`, quelques PBKDF2 hérités), avec `role` (`USER|EDITOR|ADMIN`), `active`, `forcePasswordChange`, `passwordSetBySeed` ; - 90 sessions dans `local_sessions` (cookie `eq-atlas-session`) ; - 3 clés API dans `api_tokens` (SHA-256 de la clé, préfixe `eqatlas_`, `scopes[]` = codes de permission, révocation, expiration, `lastUsedAt`) — dont celle du worker ; - 4 groupes × 70 permissions (`groups`, `group_permissions`, `user_groups`), plus `external_identities` (Entra) et `login_throttle` (verrou par compte, persisté). Le fragment `schemas/auth.prisma` définit `user`, `session`, `account`, `verification`, `api_key`. Rien ne dit comment on y arrive depuis un schéma existant. **Questions, dans l'ordre où elles bloquent le rétrofit :** 1. **Mots de passe.** Quel est le hachage attendu par `creerAuth` et comment déclarer le vérificateur personnalisé ? Le format d'ATLAS est `scrypt:<sel hex>:<dérivé hex>` (N=65 536, r=8, p=1, keylen 64) et `pbkdf2:<itérations>:<sel>:<dérivé>` pour l'hérité. Faut-il les recopier tels quels dans `account.password`, ou les re-hacher au premier login (ce qu'ATLAS fait déjà pour PBKDF2 → scrypt) ? 2. **Clés API.** `api_key.hash` est un SHA-256 : celui d'ATLAS aussi. Une ligne migrée (hash, préfixe, scopes) est-elle lue telle quelle par `principalDepuisCle`, ou le préfixe doit-il valoir `API_KEY_PREFIX` pour que la clé soit reconnue ? Concrètement : peut-on garder `eqatlas_` en posant `API_KEY_PREFIX=eqatlas_`, ou faut-il réémettre les clés (donc reconfigurer le worker et les appelants) ? 3. **Scopes.** Les scopes d'ATLAS sont des codes de permission (`ingest.monuments`) ; ceux du socle sont des `operationId` (`monuments.importer`). Une clé migrée doit-elle voir ses scopes traduits, et par qui ? 4. **Sessions.** Faut-il tenter de migrer `local_sessions` vers `session` (token, expiration), ou assume-t-on une reconnexion de tout le monde ? Si oui, il faut le dire dans le cahier : c'est une décision d'exploitation. 5. **Groupes → rôles.** `roles.yaml` remplace 4 groupes × 70 droits. Existe-t-il, ou est-il prévu, un `eqs auth roles --depuis <permissions.ts>` ou l'équivalent, ou la traduction est-elle manuelle ? Et `LocalUser.role` (`USER|EDITOR|ADMIN`) devient-il `user.role` tel quel ? 6. **`forcePasswordChange` / `passwordSetBySeed` / `login_throttle`.** Ces trois mécanismes sont des décisions d'ATLAS (audit passe 6). Le socle en couvre-t-il l'intention (mot de passe temporaire imposé côté serveur, verrou par compte indépendant des en-têtes), ou restent-ils dans l'application ? **Ce que ça coûte.** Bloquant pour l'étape 5 du plan. Sans réponse, chaque choix est une hypothèse qu'un futur rétrofit (photofront, metalfrom, nuage : mêmes questions) refera. **Proposition.** Une section « Rétrofit d'un existant » dans le cahier de `@eqs/auth`, avec le contrat du hachage personnalisé, la règle sur le préfixe des clés, et la position sur les sessions. Et, si les réponses sont mécaniques, une commande `eqs migrate auth --plan` qui lit le schéma Prisma existant et propose le SQL — le même esprit que `eqs migrate plan`. Contexte : rétrofit d'ATLAS, branche `epic/retrofit` de `EQS/ATLAS`, `docs/retrofit-eqs.md`. Je commence par les étapes 0–4 (tooling, config, errors, telemetry), qui n'en dépendent pas.
Author
Owner

Un second rétrofit tombe sur les mêmes questions, et en ajoute une

Le ticket prévoyait le cas : « photofront, metalfrom, nuage : mêmes questions ». C'est arrivé. nuage est en rétrofit depuis aujourd'hui (EQS-Sandbox/NUAGE, branche retrofit-socle-eqs, @eqs/* 0.13.0, profil nest), et ses questions 1 à 6 sont les tiennes, avec d'autres chiffres.

Mais il en pose une que la liste n'a pas, et elle est plus structurelle.

nuage ne tient pas de session : il délègue à HQ

// apps/api/src/auth/auth.service.ts
async validateHqSession(cookieToken: string) {
  const response = await fetch(`${hqApiUrl}/api/auth/get-session`, {
    headers: { Cookie: `better-auth.session_token=${cookieToken}` },
  })
  …
}

Chaque compte porte un hqUserId et un hqRole. L'identité n'est pas locale : elle est empruntée à HQ à chaque requête.

Les quatre fournisseurs entrants de @eqs/auth supposent tous l'inverse — que le satellite tient sa propre session, contre Entra ou en local. Il n'y a pas de cinquième fournisseur entrant, et pas de point d'extension pour en brancher un : EqsAuthModule.forEqs(options) prend DepsPrincipal plus un canAccess, et la résolution du principal est fermée.

Précision, parce que je me suis trompé en première lecture : src/delegue.ts existe bien et s'annonce « Fournisseur 5 », mais il répond à l'autre question — au nom de qui le service sort, pas qui entre. Il ne s'applique pas ici.

Ce que nuage va faire, et pourquoi

Il abandonne la délégation et rejoint le cas général : sa propre session better-auth contre le même locataire Entra que HQ. Le SSO vécu ne change pas (une seule connexion Microsoft couvre les deux, rebond silencieux), HQ reste propriétaire des données métier, appelées par client généré et @eqs/http.

Le raisonnement, pour qu'il soit opposable plus tard : D-007 §2 dit qu'un package contient ce qui est identique dans au moins deux applications. La délégation à HQ n'existe que dans nuage. Elle ne devient donc pas un package, et un if dans @eqs/auth pour un seul appelant serait exactement ce que la doctrine §7 refuse.

Conséquence assumée, et c'est elle qui coûte : hqRole disparaît comme source de droits. Les droits viendront de roles.yaml et des groupes Entra. C'est la question 5 de ce ticket, posée autrement — chez ATLAS c'est groups × group_permissions qu'il faut traduire, chez nuage c'est un rôle hérité d'un autre service.

Ce que ça ajoute à la demande initiale

La section « Rétrofit d'un existant » que tu proposes pour le cahier de @eqs/auth gagnerait une septième entrée :

7. L'application déléguait son identité à un autre service de la flotte. Le socle ne modélise pas ce cas et ne le modélisera pas (D-007 §2). Ce qu'il faut savoir avant de s'engager : ce qui se perd (le rôle hérité, le rattachement d'identité), ce qui se garde (le lien de données par identifiant étranger, qui reste une copie déclarée au sens de l'invariant 3), et le fait que le SSO survit si les deux services partagent le locataire.

Parce que la vraie difficulté n'est pas technique — c'est de savoir à quoi on renonce, et de l'avoir écrit avant de commencer plutôt qu'après.

Conception du rétrofit : docs/superpowers/specs/2026-09-30-retrofit-socle-eqs-design.md dans EQS-Sandbox/NUAGE. L'étage auth est le dernier des dix ; je reviendrai ici avec les chiffres quand j'y serai.

## Un second rétrofit tombe sur les mêmes questions, et en ajoute une Le ticket prévoyait le cas : « photofront, metalfrom, **nuage** : mêmes questions ». C'est arrivé. `nuage` est en rétrofit depuis aujourd'hui (`EQS-Sandbox/NUAGE`, branche `retrofit-socle-eqs`, `@eqs/*` 0.13.0, profil `nest`), et ses questions 1 à 6 sont les tiennes, avec d'autres chiffres. Mais il en pose une que la liste n'a pas, et elle est plus structurelle. ## `nuage` ne tient pas de session : il délègue à HQ ```ts // apps/api/src/auth/auth.service.ts async validateHqSession(cookieToken: string) { const response = await fetch(`${hqApiUrl}/api/auth/get-session`, { headers: { Cookie: `better-auth.session_token=${cookieToken}` }, }) … } ``` Chaque compte porte un `hqUserId` et un `hqRole`. L'identité n'est pas locale : elle est **empruntée à HQ à chaque requête**. Les quatre fournisseurs entrants de `@eqs/auth` supposent tous l'inverse — que le satellite tient sa propre session, contre Entra ou en local. Il n'y a pas de cinquième fournisseur entrant, et pas de point d'extension pour en brancher un : `EqsAuthModule.forEqs(options)` prend `DepsPrincipal` plus un `canAccess`, et la résolution du principal est fermée. Précision, parce que je me suis trompé en première lecture : **`src/delegue.ts` existe bien et s'annonce « Fournisseur 5 »**, mais il répond à l'autre question — au nom de qui le service **sort**, pas qui **entre**. Il ne s'applique pas ici. ## Ce que `nuage` va faire, et pourquoi Il **abandonne la délégation** et rejoint le cas général : sa propre session better-auth contre le même locataire Entra que HQ. Le SSO vécu ne change pas (une seule connexion Microsoft couvre les deux, rebond silencieux), HQ reste propriétaire des **données** métier, appelées par client généré et `@eqs/http`. Le raisonnement, pour qu'il soit opposable plus tard : D-007 §2 dit qu'un package contient ce qui est identique dans **au moins deux** applications. La délégation à HQ n'existe que dans `nuage`. Elle ne devient donc pas un package, et un `if` dans `@eqs/auth` pour un seul appelant serait exactement ce que la doctrine §7 refuse. Conséquence assumée, et c'est elle qui coûte : **`hqRole` disparaît comme source de droits.** Les droits viendront de `roles.yaml` et des groupes Entra. C'est la question 5 de ce ticket, posée autrement — chez ATLAS c'est `groups` × `group_permissions` qu'il faut traduire, chez `nuage` c'est un rôle hérité d'un autre service. ## Ce que ça ajoute à la demande initiale La section « Rétrofit d'un existant » que tu proposes pour le cahier de `@eqs/auth` gagnerait une septième entrée : > **7. L'application déléguait son identité à un autre service de la flotte.** Le socle ne modélise pas ce cas et ne le modélisera pas (D-007 §2). Ce qu'il faut savoir avant de s'engager : ce qui se perd (le rôle hérité, le rattachement d'identité), ce qui se garde (le lien de données par identifiant étranger, qui reste une copie déclarée au sens de l'invariant 3), et le fait que le SSO survit si les deux services partagent le locataire. Parce que la vraie difficulté n'est pas technique — c'est de savoir **à quoi on renonce**, et de l'avoir écrit avant de commencer plutôt qu'après. Conception du rétrofit : `docs/superpowers/specs/2026-09-30-retrofit-socle-eqs-design.md` dans `EQS-Sandbox/NUAGE`. L'étage auth est le dernier des dix ; je reviendrai ici avec les chiffres quand j'y serai.
Author
Owner

Répondu dans 9cd35c2 (§8 bis du cahier de @eqs/auth, D-056), avec une mesure à chaque fois. Les réponses courtes :

  1. Mots de passe : gardés. Recopiés tels quels, ils ne marchaient PAS. better-auth hache au format <sel>:<dérivé> (N=16384, r=16), donc un scrypt:<sel>:<dérivé> d'ATLAS était lu de travers et refusé. La phrase du cahier sur la « migration sans réinitialisation » était fausse : creerAuth ne permettait pas de brancher un vérificateur. C'est maintenant possible avec creerAuth({ verifierMotDePasseHerite }) : tu passes celui de prisma/passwordHash.ts, il rend undefined pour un hash qui n'est pas de tes formats, et le mot de passe est re-haché au format better-auth à la première connexion réussie. C'est prouvé sur ton format exact (N=65536, r=8, sel hex pris comme chaîne). PBKDF2 passe par le même vérificateur.
  2. Clés API : à réémettre. Le socle hache la clé entière, préfixe compris. ATLAS hache raw sans eqatlas_ (api-tokens/route.ts) : les hashes ne correspondent pas et ne se recalculent pas sans les clés en clair. Pour 3 clés, réémettre. Une application qui hache la clé entière, elle, peut garder ses clés avec API_KEY_PREFIX.
  3. Portées : traduites par toi. Une portée est comparée à l'operationId. ingest.monuments n'ouvre aucune route.
  4. Sessions : non migrées. Tout le monde se reconnecte : le jeton est signé par AUTH_SECRET et porte un autre cookie. C'est une décision d'exploitation, à annoncer avant la bascule.
  5. Groupes → roles.yaml à la main. eqs auth roles --init donne le squelette. user.role doit être en minuscules : ADMIN recopié ne donne aucune permission.
  6. forcePasswordChange, passwordSetBySeed et le verrou par compte restent dans l'application. Le socle limite par adresse, pas par compte. Une seconde application qui en a besoin en fera un candidat au socle.
  7. Identité déléguée (nuage) : non modélisée. Ta lecture est écrite telle quelle : le SSO survit, le rôle hérité se perd, le lien de données se garde.
Répondu dans 9cd35c2 (§8 bis du cahier de `@eqs/auth`, D-056), avec une mesure à chaque fois. Les réponses courtes : 1. **Mots de passe : gardés.** Recopiés tels quels, ils ne marchaient PAS. better-auth hache au format `<sel>:<dérivé>` (N=16384, r=16), donc un `scrypt:<sel>:<dérivé>` d'ATLAS était lu de travers et refusé. La phrase du cahier sur la « migration sans réinitialisation » était fausse : `creerAuth` ne permettait pas de brancher un vérificateur. C'est maintenant possible avec `creerAuth({ verifierMotDePasseHerite })` : tu passes celui de `prisma/passwordHash.ts`, il rend `undefined` pour un hash qui n'est pas de tes formats, et le mot de passe est re-haché au format better-auth à la première connexion réussie. C'est prouvé sur ton format exact (N=65536, r=8, sel hex pris comme chaîne). PBKDF2 passe par le même vérificateur. 2. **Clés API : à réémettre.** Le socle hache la clé entière, préfixe compris. ATLAS hache `raw` sans `eqatlas_` (`api-tokens/route.ts`) : les hashes ne correspondent pas et ne se recalculent pas sans les clés en clair. Pour 3 clés, réémettre. Une application qui hache la clé entière, elle, peut garder ses clés avec `API_KEY_PREFIX`. 3. **Portées : traduites par toi.** Une portée est comparée à l'`operationId`. `ingest.monuments` n'ouvre aucune route. 4. **Sessions : non migrées.** Tout le monde se reconnecte : le jeton est signé par `AUTH_SECRET` et porte un autre cookie. C'est une décision d'exploitation, à annoncer avant la bascule. 5. **Groupes → `roles.yaml` à la main.** `eqs auth roles --init` donne le squelette. `user.role` doit être en minuscules : `ADMIN` recopié ne donne aucune permission. 6. **`forcePasswordChange`, `passwordSetBySeed` et le verrou par compte restent dans l'application.** Le socle limite par adresse, pas par compte. Une seconde application qui en a besoin en fera un candidat au socle. 7. **Identité déléguée (`nuage`) : non modélisée.** Ta lecture est écrite telle quelle : le SSO survit, le rôle hérité se perd, le lien de données se garde.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Infrastructure/eqs-platform#80
No description provided.