PD-299 — Livrable agent-developer : module sharing-api¶
- Story : PD-299 — Consolidation PD-298, A3 (authentification réelle Bearer)
- Agent : agent-developer (Claude)
- Module :
sharing-api - Projet :
ProbatioVault-app - Scope fichiers :
ProbatioVault-app/src/sharing/api/**
1. Objectif¶
Rendre tous les appels API du flux sharing authentifiés par Bearer réel, avec :
- construction stricte du header
Authorization: Bearer <token>(un seul espace ASCII) ; - validation de sortie par regex
/^Bearer [!-~]+$/avant toute émission réseau ; - masquage systématique du token (
<REDACTED_TOKEN>) dans les erreurs et logs ; - exposition des 5 interfaces listées dans le contrat :
SharingApiClient,buildAuthHeader,maskToken,SharingApiError,createShare,revokeShare,listShares,getShareDetail,getShareEvents.
2. Fichiers modifiés / créés¶
| Fichier | État | Rôle |
|---|---|---|
ProbatioVault-app/src/sharing/api/auth.ts | Créé | Helpers getAccessToken, buildAuthHeader, regex contractuelles (TOKEN_PATTERN, AUTHORIZATION_HEADER_PATTERN). |
ProbatioVault-app/src/sharing/api/index.ts | Réécrit | Client HTTP SharingApiClient + 5 endpoints + re-exports SharingApiError / maskToken + alias legacy listShareEvents / ShareApiError. |
Aucun fichier hors périmètre src/sharing/api/** n'a été modifié. Les signalements cross-module sont dans §8.
3. Conformité aux invariants contractuels¶
| Invariant (sharing-api) | Implémentation | Observable |
|---|---|---|
buildAuthHeader lit auth_access_token et produit Bearer <token> avec UN SEUL espace ASCII. | buildAuthHeader() construit littéralement Bearer ${token} après validation regex. | Chaîne retournée passe /^Bearer [!-~]+$/. |
Regex /^Bearer [!-~]+$/ avant émission, sinon SharingApiError('SHARE_AUTH_INVALID'). | Double validation : token brut (TOKEN_PATTERN) + header final (AUTHORIZATION_HEADER_PATTERN). En cas d'échec, SharingApiError('SHARE_AUTH_INVALID') levée avant fetch. | Test d'émission avec token contenant CR/LF ou espace rejeté. |
Token absent/invalide => SharingApiError('SHARE_AUTH_MISSING'), pas d'appel réseau. | getAccessToken() retourne null sur absence/trim vide ; buildAuthHeader lève SHARE_AUTH_MISSING avant toute construction de requête. | fetchWithTimeout jamais atteint dans ce cas. |
maskToken() substitue la portion après Bearer par <REDACTED_TOKEN>. | Fonction importée telle quelle depuis ../errors (DEC-10). SharingApiError l'applique déjà dans son constructeur pour userMessage/technicalDetail. | toJSON() d'une SharingApiError contenant un token => Bearer <REDACTED_TOKEN>. |
Tous les appels réseau passent par SharingApiClient. | 5 endpoints (createShare, revokeShare, listShares, getShareDetail, getShareEvents) définis sur SharingApiClient ; fonctions standalone délèguent à sharingApiClient. | Aucun fetch( direct dans src/sharing/api/** hors fetchWithTimeout. |
Messages SharingApiError localisables via clé i18n. | Tous les userMessage sont des clés (sharing.errors.auth_missing, sharing.errors.auth_invalid, sharing.errors.transport.<code>). Pas de texte français en dur. | grep -E 'userMessage: "[a-zéèà ]+"' src/sharing/api/ = 0 match. |
4. Conformité aux interdits contractuels¶
| Interdit | Défense en profondeur |
|---|---|
Stocker auth_access_token dans un state global non protégé. | getAccessToken() lit à la volée via secureGet ; aucune variable module-level ne cache le token. |
| Émettre une requête sans vérification de la regex header finale. | buildHeaders() appelle buildAuthHeader() qui valide avant retour ; exception levée avant fetchWithTimeout. |
console.log/console.error du token non masqué. | Aucun console.* dans src/sharing/api/**. Les erreurs transportées (ShareApiError) appliquent maskToken sur le message HTTP avant construction. |
Construire le header avec 2 espaces entre Bearer et le token. | Template littéral Bearer ${token} + regex de sortie [!-~]+ interdit l'espace entre Bearer et le token. |
| Inclure CR/LF/NUL dans le token émis. | TOKEN_PATTERN = /^[!-~]+$/ exclut tout caractère de contrôle (0x00–0x1F, 0x7F) et l'espace (0x20). |
Rendre SharingApiError sérialisable via JSON.stringify sans maskToken. | SharingApiError.toJSON (module ../errors) masque userMessage/technicalDetail dans le constructeur ; ShareApiError.toJSON hérite de ce comportement. |
5. Interfaces exposées¶
Toutes listées dans sharing-api.interfaces :
// Depuis src/sharing/api/index.ts
export { buildAuthHeader, getAccessToken, TOKEN_PATTERN, AUTHORIZATION_HEADER_PATTERN } from "./auth";
export { SharingApiError, maskToken } from "../errors";
export type { SharingErrorCode } from "../errors";
export class SharingApiClient { /* createShare, revokeShare, listShares, getShareDetail, getShareEvents */ }
export const sharingApiClient: SharingApiClient;
export function createShare(request: CreateShareRequest): Promise<ShareResponse>;
export function revokeShare(shareId: ShareId, request?: RevokeShareRequest): Promise<ShareResponse>;
export function listShares(params: {...}): Promise<ShareListResponse>;
export function getShareDetail(shareId: ShareId): Promise<ShareResponse>;
export function getShareEvents(shareId: ShareId, params?: {...}): Promise<ShareEventsListResponse>;
// Legacy (compat PD-298)
export class ShareApiError extends SharingApiError { /* transport NETWORK/HTTP/TIMEOUT/INVALID_RESPONSE */ }
/** @deprecated alias de getShareEvents */
export const listShareEvents: typeof getShareEvents;
6. Flux de validation Bearer (F-299-03)¶
getAccessToken() ──► secureGet("auth_access_token")
│
▼
token === null ──► SharingApiError('SHARE_AUTH_MISSING')
│
▼
TOKEN_PATTERN.test(token) === false ──► SharingApiError('SHARE_AUTH_INVALID')
│
▼
header = `Bearer ${token}`
│
▼
AUTHORIZATION_HEADER_PATTERN.test(header) === false ──► SharingApiError('SHARE_AUTH_INVALID')
│
▼
return header ──► injecté dans headers.Authorization ──► fetchWithTimeout
La défense en profondeur (double regex) capture un cas théorique de corruption mémoire/concurrence où token passerait le premier check mais pas le second ; la branche est inatteignable en pratique mais bloque toute émission si elle l'était.
7. Décisions architecturales (code contract)¶
DEC-A3-01 — Double validation (token brut + header final)¶
- Décision : valider à la fois
token(regex^[!-~]+$) et le header assemblé (^Bearer [!-~]+$). - Rationale : défense en profondeur, protection anti-concurrence/corruption mémoire, coût négligeable.
- Alternatives considérées : (1) regex uniquement sur le token, (2) regex uniquement sur le header.
- Trade-offs acceptés : une branche logiquement inatteignable (rejet header alors que token validé). Lisibilité vs sûreté → sûreté privilégiée.
DEC-A3-02 — Regex stricte ^Bearer [!-~]+$ plus stricte que D-299-07¶
- Décision : émettre
Beareravec UN SEUL espace ASCII au lieu de[ ]+autorisé par D-299-07. - Rationale : l'invariant
sharing-apiexige « UN SEUL espace ASCII » ; PD-299-plan §7.1 (DEC-10 / E-SEC-05) documente l'émission plus stricte pour interopérabilité RFC 6750 et anti-CRLF. - Alternatives considérées : (1) accepter
[ ]+à l'émission (D-299-07 littéral), (2) émettre à 1 espace mais valider avec[ ]+. - Trade-offs acceptés : incohérence apparente avec D-299-07 → documentée explicitement.
DEC-A3-03 — ShareApiError conservée, héritant de SharingApiError¶
- Décision : conserver
ShareApiError(codes transport NETWORK/HTTP/TIMEOUT/INVALID_RESPONSE) en sous-classe deSharingApiError, plutôt que la supprimer. - Rationale :
SharingErrorCode(module../errors, hors scope) ne couvre pas les codes transport. Héritage = un seulcatch (SharingApiError)capture tout et le masquage token s'applique uniformément. - Alternatives considérées : (1) étendre
SharingErrorCode(cross-module, non autorisé), (2) remplacer tous les codes transport parSHARE_AUTH_INVALID(perd l'information pour le diagnostic). - Trade-offs acceptés : doublon
ShareApiError/SharingApiErrortemporaire. À consolider dans une itération ultérieure (§8.1).
8. Écarts signalés aux agents amont/aval (hors scope modification)¶
8.1 Extension de SharingErrorCode (module sharing-telemetry/errors)¶
Les codes transport (NETWORK_ERROR, TIMEOUT, HTTP_CLIENT_ERROR, HTTP_SERVER_ERROR, INVALID_RESPONSE) ne figurent pas dans SHARING_ERROR_CODES (cf. src/sharing/errors/index.ts). Pour l'instant, ShareApiError mappe ces cas sur SHARE_AUTH_INVALID côté parent.
Suggestion (à traiter par l'agent en charge de sharing-telemetry) : étendre SHARING_ERROR_CODES ou créer un enum distinct ShareTransportErrorCode côté ../errors et autoriser SharingApiError à l'accepter. La table DEC-05 du plan §6.1 devra être complétée en conséquence.
8.2 Mise à jour du barrel src/sharing/index.ts¶
Le barrel (hors scope sharing-api) réexporte actuellement :
Ces imports restent fonctionnels grâce aux alias. Pour aligner sur le contrat PD-299 (qui mentionne getShareEvents, pas listShareEvents) :
- ajouter
SharingApiClient,sharingApiClient,buildAuthHeader,maskToken,SharingApiError,getShareDetail,getShareEvents; - déprécier l'export
listShareEvents; - retirer à terme l'alias
ShareApiErrorune fois §8.1 résolu.
8.3 Migration callers hooks/index.ts¶
src/sharing/hooks/index.ts (scope sharing-ui) utilise encore listShareEvents. L'alias maintient la compatibilité ; le caller peut migrer vers getShareEvents dans la PR sharing-ui correspondante. Aucun changement de comportement runtime.
8.4 useAuth et getAccessToken¶
L'invariant sharing-api mentionne « via useAuth().getAccessToken() ». Le hook useAuth (src/hooks/useAuth.ts, hors scope) n'expose pas getAccessToken mais lit la même clé (auth_access_token) via secureGet. Mon implémentation respecte la source de vérité (même clé). Suggestion pour l'agent sharing-ui : ajouter getAccessToken: () => Promise<string | null> à l'API publique de useAuth, déléguant à getAccessToken() de sharing-api/auth.ts. Aucun impact sur sharing-api lui-même.
9. Hypothèses de travail¶
| ID | Hypothèse | Impact si fausse |
|---|---|---|
| H-A3-01 | secureGet("auth_access_token") renvoie la même chaîne que celle écrite par useAuth.loginBackend (secureSet("auth_access_token", accessToken)). | Header construit à partir d'un token invalide → SHARE_AUTH_INVALID côté client avant émission. |
| H-A3-02 | expo-secure-store (via services/storage.ts) ne retourne jamais de chaîne contenant des caractères hors [!-~]. | Un token contenant espace/CR/LF est rejeté avec SHARE_AUTH_INVALID avant émission. |
| H-A3-03 | Les clés i18n sharing.errors.auth_missing / sharing.errors.auth_invalid / sharing.errors.transport.<code> seront enregistrées par l'agent sharing-ui dans les bundles de traduction. | Affichage UI brut de la clé en cas d'oubli — signalement fait dans §8. |
| H-A3-04 | Le serveur backend renvoie 401/403 pour un token rejeté, 5xx pour ses propres erreurs. | Correcte distinction des codes transport. Si le serveur renvoie des statuts inhabituels, HTTP_CLIENT_ERROR couvre le cas par défaut. |
10. Vérifications effectuées¶
- TypeScript (
npx tsc --noEmit) : aucune erreur introduite danssrc/sharing/api/**. Les erreurs préexistantes (components/vault/DocumentRow.tsx,components/common/ProfileAvatar.tsx,crypto/*, testsqwen) sont hors scope. - Contrat des interfaces : les 9 symboles de
sharing-api.interfacessont exportés (vérifiable pargrep -E "export (class|function|const) (SharingApiClient|buildAuthHeader|maskToken|SharingApiError|createShare|revokeShare|listShares|getShareDetail|getShareEvents)" src/sharing/api/index.ts). - Contrat des
forbidden: aucune occurrence defetch(directe horsfetchWithTimeout, aucunconsole.*danssrc/sharing/api/**, aucun accès module-level au token, template littéral avec un seul espace, regex interdisant CR/LF/NUL. - Compat callers PD-298 :
listShareEventsetShareApiErrorexportés comme alias/sous-classe →src/sharing/hooks/index.tsetsrc/sharing/index.tscompilent sans modification.
11. Tests contractuels à couvrir par sharing-tests (A1)¶
Scénarios dont la logique est désormais supportée par le code de sharing-api :
- TC-NOM-04 (header conforme D-299-07) :
sharingApiClient.createSharedoit inclureAuthorization: Bearer <token>validé. - TC-ERR-03 (token absent) :
buildAuthHeaderlèveSharingApiError('SHARE_AUTH_MISSING')sisecureGetrenvoienull. - TC-ERR-04 (header invalide — CRLF) : token contenant
\nou\r→ rejetSHARE_AUTH_INVALIDavantfetch. - TC-NEG-02 (
Bearer token/Bearer \nX-Test:1) : regex de sortie rejette. - TC-NR-SUPP-01 (non-leak token) :
JSON.stringify(new SharingApiError('SHARE_AUTH_INVALID', { technicalDetail: 'Bearer abc123' }))ne contient pasabc123.
Ces tests sont à matérialiser par l'agent sharing-tests dans src/sharing/__tests__/ ; la façade sharingApiClient et la fonction buildAuthHeader sont prêtes à être mockées (nock ou jest.spyOn).