PD-287 — Plan d'implémentation¶
Stack cible : ProbatioVault-backend (NestJS + TypeORM + PostgreSQL). Aucune autre pile n'est visée.
Hypothèse contractuelle retenue : le backend expose déjà vault_secure.proofs (service Proofs), un module Crypto (HSM + KMS envelope), un service Mail transactionnel, un journal d'audit plateforme, un rate-limiter distribué (Redis) et un orchestrateur de workers. Le présent plan s'insère comme module SharingModule + workers dédiés, sans fork de modules existants.
1. Découpage en composants¶
| # | Composant | Responsabilité | Owner (agent step 6b) | Routes/endpoints exposés |
|---|---|---|---|---|
| C1 | SharingModule (API partage) | Create/list/revoke/read liens côté propriétaire, exposition destinataire du lien. Applique guards partage, lock distribué, idempotence. | agent-sharing-api | POST /shares, GET /shares, GET /shares/:id, POST /shares/:id/revoke, GET /sharing/:token, POST /sharing/:token/activate, POST /sharing/:token/reauth/request-otp, POST /sharing/:token/reauth/confirm, GET /sharing/:token/proof, POST /sharing/:token/export |
| C2 | OtpService | Émission OTP email 6 digits, validation, compteurs session/cumulé, cooldown, rate-limit renvois, hook notification propriétaire. | agent-otp | interne (consommé par C1) |
| C3 | RecipientSessionService | Gestion sessions éphémères destinataire (création post-OTP, timeout d'inactivité, réauth, invalidation forcée sur révocation/expiration). | agent-recipient-session | interne (consommé par C1) |
| C4 | PreCryptoService | Génération identité éphémère destinataire, PRE_ReKeyGen, PRE_ReEncrypt, envelope chiffrée de la DEK. Appels HSM via wrapper existant. | agent-crypto-pre | interne (consommé par C1, C10) |
| C5 | AuditJournalService | Append-only synchrone bloquant, hash-chain (prev_hash), séquenceur d'événements, extraction cohérente par lien. | agent-audit-journal | interne (consommé par C1, C3, C6, C7, C10) + GET /shares/:id/events (owner) |
| C6 | MerkleAnchorWorker | Worker périodique : batch N événements, calcul racine Merkle, ancrage blockchain, persistence audit_batch_merkle_root, rattrapage post-crash ordonné. | agent-audit-journal (workers) | cron interne |
| C7 | ReconciliationWorker | Détection orphelins (lien non terminal inactif > seuil), rattrapage ancrage, clearing HEALTHY après N cycles conformes. | agent-reconciliation-worker | cron interne |
| C8 | RetentionWorker | Purge RGPD J+90 post-terminal : suppression email/IP/UA/session/clés éphémères/compteurs ; préservation du journal d'audit. Event technique de purge. | agent-retention-worker | cron interne |
| C9 | TrustStoreService | Chargement du trust-store plateforme (racines X.509 + politique CRL/OCSP), vérification chaîne certificat destinataire éphémère, fail-closed explicite. | agent-trust-store | interne (consommé par C4) |
| C10 | ExportComposerService | Construction ZIP (document chiffré + artefacts + manifeste JCS + signature plateforme + certificat), calcul SHA-256, contrôle taille. Feature-flag export_zip_enabled. | agent-export | interne (consommé par C1) |
| C11 | NotificationDispatcher | Notifications propriétaire (3 blocages OTP, révocation anti-abus) via canaux mail/push existants. Journalisation canal+corrélation. | agent-notifications | interne (consommé par C2, C3) |
| C12 | ShareProofGuard (cross-module) | Guard NestJS enregistré dans le scope ProofsModule pour les routes listées §5.8 de la spec ; vérifie share_state=ACTIVE, session valide, TTL, non-révocation, ownership indirect. | agent-sharing-api (livré avec C1, intégré dans ProofsModule) | injecté sur GET /proofs/:id/document, GET /proofs/:id/probative-metadata, POST /proofs/:id/export-composite |
| C13 | Migrations DDL + repositories | Schéma vault_secure.share_links, share_otp_state, share_sessions, share_audit_events, share_audit_batches, share_idempotency_keys. Contraintes FK, index anti-scan sur share_url_token (hash). | agent-sharing-api | n/a |
| C14 | Suite de tests | Unit + Integration + E2E + Perf (k6/Artillery) + Sécurité (fuzz token, scan logs/dumps). | agent-tests | n/a |
2. Flux techniques¶
2.1 F-287-01 — Création du lien (propriétaire)¶
POST /sharesavecproof_id,recipient_email,ttl_seconds?, headerIdempotency-Key.SharingControllervalide le payload via DTO +class-validator(regex §5.1).- Acquisition lock Redis
share:create:{proof_id}:{recipient_email}TTL 600s → 409SHARE_BUSYsinon. - Vérification idempotence :
share_idempotency_keys(key=sha256(key+canonicalJson(payload)))→ 409IDEMPOTENCY_CONFLICTsi payload différent, 409IDEMPOTENCY_WINDOW_EXPIREDsi hors fenêtre 24h. - Vérification ownership preuve via
ProofsService.assertOwner(proof_id, userId). - Vérification quotas (C7 expose
QuotaCheckServiceinterrogé via repositoryshare_quota_counters) :create_links_per_proof_per_24h,create_links_per_owner_per_24h,active_links_per_owner_max. PreCryptoService.generateEphemeralIdentity()→recipient_ephemeral_pubkey_pem, cert signé par CA plateforme (DER b64). Aucun stockage du secret privé côté backend (la clé privée n'est jamais générée côté serveur ; seuls la pubkey et la capsule PRE transitent).PreCryptoService.reKeyGen(owner_key_ref, recipient_ephemeral_pubkey)→pre_rekey_envelope_b64(chiffré KMS).- Génération token : 32 octets CSPRNG
crypto.randomBytes(32)→ base64url 43 chars. Persistence desha256(token)comme PK indexée ; le token clair n'est jamais stocké. - Transaction SQL unique :
INSERT share_links (id, proof_id, owner_user_id, recipient_email_lower, token_sha256, state='PENDING_ACTIVATION', created_at, expires_at=created_at+ttl, pre_rekey_envelope_b64, recipient_ephemeral_cert_der_b64, encrypted_dek_envelope_b64)AuditJournalService.append(LINK_CREATED, share_link_id, prev_hash)bloquant dans la même transaction (pattern outbox pas utilisé ici — l'audit est synchrone §5.7).- Commit.
- Libération lock. Réponse
201 { share_url_token, share_link_id, state, expires_at_utc }au propriétaire uniquement.
2.2 F-287-02 — Activation destinataire OTP¶
GET /sharing/:token: résolutiontoken_sha256, check étatPENDING_ACTIVATION+ non expiré. Si KO → 404 générique (§INV-287-12).OtpService.emitOtp(share_link_id, recipient_email):- Contrôle
otp_resend_limit_per_hour(Redis counter fenêtre glissante 1h). - Génère code 6 digits via
crypto.randomInt(0, 1_000_000)(pasMath.random()— cf. learningcrypto.randomUUID()2026-02-21, même justification S2245). - Stocke
sha256(otp_code)+emitted_at_utc+valid_until_utc = emitted_at_utc + OTP_CODE_TTL_SECONDS(nouvelle constanteOTP_CODE_TTL_SECONDS=300s, cf. Point de vigilance #10). - Envoi email via
MailService.sendOtp(). - Retour
200(pas de différenciation succès/rate-limit vers destinataire autre que 429 rate-limit). POST /sharing/:token/activate { otp_code }:- Lock Redis
share:otp:{share_link_id}TTL 30s (empêche concurrence session). - Chargement
share_otp_state(compteur sessionotp_attempt_count, cumuléotp_failed_attempts_total). - Comparaison timing-safe
crypto.timingSafeEqual(sha256(input), stored_sha256)+ check non péremption OTP. - Si OK : transition
PENDING_ACTIVATION → ACTIVE, création sessionrecipient_session_id, resetotp_attempt_count=0, append auditLINK_ACTIVATED. - Si KO et
otp_attempt_count < 4: 404 générique, incrément session + cumulé. - Si KO et
otp_attempt_count == 4(5e échec) : transition→ OTP_BLOCKED,otp_blocked_until_utc = now + 900s, incrément blocages → hookNotificationDispatchersi 3e blocage, append audit. Réponse 429 (suit §5.4 F-287-02, cf. Point de vigilance #1). - Si
otp_failed_attempts_total >= 50après incrément : transition→ REVOKED, notification propriétaire, append auditLINK_REVOKED. Réponse 429. - Déverrouillage automatique
OTP_BLOCKED → ACTIVEexécuté parReconciliationWorker(tick 60s) OU à la prochaine tentative OTP valide si cooldown expiré.
2.3 F-287-03 — Consultation de la preuve¶
GET /sharing/:token/proofavec headerX-Recipient-Session: <recipient_session_id>.ShareProofGuard(C12) : valide token, chargeshare_link, vérifiestate=ACTIVE, expiration, session non expirée (last_activity + 1800s).- Si session invalide → 404 générique + indication UX de réauth via
/reauth/request-otp(enveloppe standard{ reauth_required: true }sans différencier existence lien). - Mise à jour session
last_activity = now. PreCryptoService.reEncrypt(encrypted_dek_envelope, pre_rekey_envelope)→recipient_capsule_encrypted_b64.ProofsService.getEncryptedDocument(proof_id)retourne le document chiffré (interprétation plan : le document chiffré est streamé inline en base64 ou via flux binaire dans la réponse — cf. Point de vigilance #2 et ADR-287-01).AuditJournalService.append(LINK_VIEWED, ...)avant émission de la réponse (synchrone bloquant). Si append échoue → 503 fail-closed, réponse vidée.- Réponse
200 { encrypted_document_b64, recipient_capsule_encrypted_b64, proof_hash_sha256_hex, merkle_proof_canonical_json, tsa_token_der_b64, blockchain_anchor_txid_hex }.
2.4 F-287-04 — Export composite (optionnel)¶
- Feature flag
export_zip_enabled=falsepar défaut. Sifalse→ 404 générique surPOST /sharing/:token/export(pas de différenciation de capability). - Si
trueet session valide :ExportComposerServiceconstruit : /document.bin(copie chiffrée, capsule PRE en métadonnée)/proof.json(hash+Merkle+TSA+anchor)/audit-subset.jsonl(événementsLINK_*strictement liés à ce lien)/manifest.json(JCS canonical, clés minimales §D-287-26)/manifest.sig(signature ECDSA P-384 par clé plateforme HSM)/platform-cert.pem(cert plateforme + chaîne racine)- Check
size <= export_zip_max_size_mb→ 413 sinon (avant streaming). AuditJournalService.append(LINK_EXPORTED, export_zip_sha256_hex)synchrone.- Streaming ZIP vers destinataire.
2.5 F-287-05 — Révocation¶
POST /shares/:id/revoke(owner) → lockshare:lifecycle:{id}.- Transaction : UPDATE state→REVOKED,
revoked_at_utc=now, invalidationshare_sessionsactives (table flag), append auditLINK_REVOKEDbloquant. - Publication event Redis
share.revoked→RecipientSessionServicepurge cache session local (coupure au round-trip suivant, cf. CA-287-08). RetentionWorkerrescheduled pour J+90.
2.6 F-287-06 — Expiration TTL¶
- Worker cron 30s scanne
share_links WHERE state IN ('PENDING_ACTIVATION','ACTIVE','OTP_BLOCKED') AND expires_at < now(). - Transition batch →
EXPIRED, appendLINK_EXPIRED+ invalidation sessions + purge artefacts temporaires. - Réponses destinataire post-expiration : 404 générique.
2.7 F-287-07 — Vue propriétaire¶
GET /shares?state=active|allpaginé.GET /shares/:id/eventsrenvoie événements liés au lien (depuisshare_audit_eventsscopéshare_link_id).- Premier affichage UI propriétaire : flag
user_preferences.noDrmWarningAcknowledged(scope compte utilisateur serveur — cf. Point de vigilance #16 et ADR-287-02).
2bis. Diagramme de dépendances agents¶
graph LR
subgraph Wave1[Wave 1 - fondations]
C13[agent-sharing-api/migrations<br/>C13 DDL]
C9[agent-trust-store<br/>C9]
C5[agent-audit-journal<br/>C5 append + hash-chain]
end
subgraph Wave2[Wave 2 - services crypto et session]
C4[agent-crypto-pre<br/>C4]
C2[agent-otp<br/>C2]
C3[agent-recipient-session<br/>C3]
C11[agent-notifications<br/>C11]
end
subgraph Wave3[Wave 3 - API + guard + workers]
C1[agent-sharing-api<br/>C1 + C12 guard]
C6[agent-audit-journal<br/>C6 Merkle worker]
C7[agent-reconciliation-worker<br/>C7]
C8[agent-retention-worker<br/>C8]
C10[agent-export<br/>C10]
end
subgraph Wave4[Wave 4 - tests]
C14[agent-tests<br/>Unit+Int+E2E+Perf+Sec]
end
C13 --> C4
C13 --> C2
C13 --> C3
C13 --> C5
C9 --> C4
C5 --> C1
C5 --> C6
C5 --> C7
C5 --> C8
C5 --> C10
C4 --> C1
C4 --> C10
C2 --> C1
C3 --> C1
C11 --> C2
C11 --> C3
C1 --> C14
C6 --> C14
C7 --> C14
C8 --> C14
C10 --> C14 Lecture : 4 waves. Wave 1 livre les fondations (DDL + trust-store + audit service). Wave 2 livre les services middleware (crypto PRE, OTP, sessions, notifications). Wave 3 livre l'API, le guard cross-module et les workers. Wave 4 produit la suite de tests. Les agents d'une même wave sont parallélisables.
2ter. Mécanismes cross-module (obligatoire)¶
| Élément | Détail |
|---|---|
| Routes protégées | GET /proofs/:id/document, GET /proofs/:id/probative-metadata, POST /proofs/:id/export-composite |
| Controllers impactés | ProofsController (module ProofsModule) |
| Effet du guard | HTTP 404 générique pour TOUT cas invalide côté destinataire : share_link.state ∉ {ACTIVE}, session invalide, TTL dépassé, otp_failed_attempts_total >= 50, token inconnu. Code uniforme 404 (anti-enumeration INV-287-12). Seul le 429 rate-limit est distinct. |
| Mécanisme de jointure cross-schéma | vault_secure.share_links.proof_id → vault_secure.proofs.id via FK + view matérialisée read-only share_access_view(share_link_id, proof_id, state, expires_at, session_id, last_activity) indexée sur share_link_id et session_id. |
| Scope d'enregistrement | { provide: APP_GUARD, useClass: ShareProofGuard } dans ProofsModule uniquement (pas global). Ordre : après JwtAuthGuard si présent, avant RolesGuard. Le guard détecte via metadata décorateur @ShareContext() que la requête est dans un contexte partage (sinon bypass, fallback au guard owner standard). |
| Exceptions d'accès | Aucune. Aucun rôle (y compris admin) ne peut contourner une révocation/expiration sur un accès destinataire. Les opérateurs plateforme accèdent aux métadonnées via leurs propres routes (hors périmètre story). |
3. Mapping invariants → mécanismes¶
| Invariant | Exigence | Mécanisme | Composant | Observable | Risque |
|---|---|---|---|---|---|
| INV-287-01 | Zero-knowledge serveur (document jamais en clair) | Document chiffré au repos (AES-256-GCM envelope KMS) ; backend ne manipule que encrypted_dek_envelope_b64 et pre_rekey_envelope_b64 ; PreCryptoService opère sur capsules, jamais sur la DEK en clair. Logs filtrés (middleware SensitiveFieldScrubber). | C4, C1, C5, infra log | Test TC-INV-11 : capture traffic + grep logs + dump mémoire avant GC forcé. Scan automatisé sur release candidate. | Timing de GC → mémoire transitoire du binaire chiffré. Mitigation : pas de déchiffrement serveur. |
| INV-287-02 | PRE obligatoire, pas de clé en clair | PreCryptoService.reKeyGen/reEncrypt wrapper HSM. Interdiction lint : pattern Buffer.*dek hors C4 flaggé par ESLint rule custom. | C4 | Audit code + test roundtrip TC-INV-09. | Alternance PRE vs KMS direct tentante à des fins de perf. Interdit par invariant YAML forbidden. |
| INV-287-03 | WORM original inchangé | Accès read-only à proofs.document_blob_ref ; aucun service du périmètre n'ouvre le blob en écriture. | C1, C4, C10 | TC-NOM-13, TC-NR-02 : SHA-256 avant/après cycle complet. | Export ZIP pourrait copier le blob chiffré — toléré (pas mutation). |
| INV-287-04 | OTP requis à chaque session | RecipientSessionService exige recipient_session_id sur toutes les routes post-activation ; session invalide → réauth obligatoire. | C3 | TC-NOM-02, TC-ERR-09. | Bearer-link implicite si cookie session non bridgé au token. Mitigation : session liée au share_link_id + IP+UA fingerprint faible (log only). |
| INV-287-05 | Identité éphémère unique par lien | PreCryptoService.generateEphemeralIdentity() appelé à chaque création de lien, recipient_ephemeral_cert_der_b64 stocké par share_link_id avec contrainte UNIQUE. | C4, C13 | TC-NOM-12. | Collision HSM clé — probabilité cryptographique négligeable. |
| INV-287-06 | États fermés (5 états) | Enum PostgreSQL share_state_enum + validation TypeScript discriminated union. Aucun string libre. | C13, C1 | TC-INV-01. | Ajout futur d'état : migration + bump enum. |
| INV-287-07 | Transitions non listées interdites | ShareStateMachine classe pure (pattern FSM table) : canTransition(from, to). Toute UPDATE state passe par cette fonction. | C1 | TC-INV-02, TC-NEG-12. | Contournement via SQL direct — bloqué par trigger PG prevent_forbidden_state_transition. |
| INV-287-08 | Révocation effective <= 2s P95 | Publication Redis pub/sub share.revoked + invalidation cache session local (TTL cache 500ms). | C1, C3 | TC-NOM-09, mesure perf k6 (N>=200 échantillons, cf. Point de vigilance #5). | Charge sortant du nominal (1 lien, 10 RPS, staging mono) non couverte — documenté. |
| INV-287-09 | Expiration TTL coupe les accès | Worker scan 30s + check inline expires_at < now() dans ShareProofGuard. | C1, C7 | TC-NOM-10, TC-ERR-11. | Drift horloge entre instances. Mitigation : now() PostgreSQL side, pas Node.js. |
| INV-287-10 | REVOKED/EXPIRED terminaux | ShareStateMachine refuse toute transition sortante depuis ces états. Trigger PG défense en profondeur. | C1, C13 | TC-INV-02, TC-NEG-12. | Réactivation demandée business — refusée par design. |
| INV-287-11 | Destinataire ne peut créer de partage | SharingController.create() vérifie ownership via JwtAuthGuard ; session destinataire n'a pas de JWT owner. | C1 | TC-ERR-18, TC-NEG-05. | Confusion de contexte d'auth — tests d'isolation de rôles. |
| INV-287-12 | Anti-enumeration 404 générique | Middleware GenericErrorMapper dans SharingController scope destinataire : mappe NotFoundException, ForbiddenException, InvalidTokenException, SessionExpiredException → { error: 'NOT_FOUND' } avec message statique. | C1 | TC-INV-06, TC-ERR-05, TC-NEG-02. | Point de vigilance #1 : 429 sur OTP_BLOCKED/REVOKED introduit un signal différenciant. Documenté comme écart accepté (spec impose 429) ; mitigation : appliquer jitter sur temps de réponse 429 pour homogénéiser latence avec 404. |
| INV-287-13 | Journal append-only + hash-chain + ancrage Merkle | Table share_audit_events sans DELETE/UPDATE (permissions PG + trigger prevent_audit_mutation). Chaque row : prev_hash, event_hash=sha256(canonical(payload)+prev_hash). Worker Merkle batch 100 entries → racine → ancrage blockchain (service existant). | C5, C6, C13 | TC-NOM-14, TC-ERR-14, TC-INV-14. | Ordre multi-batchs post-crash (Point de vigilance #14) — sérialisation par audit_sequence_number BIGSERIAL + worker mono-instance via advisory lock PG. |
| INV-287-15 | Rétention RGPD J+90 + audit conservé | RetentionWorker cron quotidien + hook opportuniste sur transition terminale programmant un job J+90. Suppression/anonymisation colonnes destinataire ; audit préservé (colonnes dédiées recipient_email_anonymized post-purge). | C8 | TC-NOM-19, TC-INV-12. | Journal contient déjà recipient_email → stocké en version hashée (recipient_email_sha256) dans share_audit_events, pas en clair (cf. Point de vigilance #15). |
| INV-287-16 | Secrets temporaires chiffrés au repos | Colonnes *_envelope_b64 chiffrées via KMS envelope ; DEK en mémoire uniquement dans HSM ; lint CI no-plaintext-key sur migrations. | C4, C13 | TC-INV-08, TC-NEG-06. | Backup PostgreSQL — hors périmètre story, assumée sécurisée par infra. |
| INV-287-17 | Trust-store obligatoire | TrustStoreService chargé au bootstrap NestJS ; exception bloquante si trust-store absent/corrompu → app ne démarre pas. Fail-closed documenté. | C9 | TC-INV-07, TC-ERR-16, TC-NEG-08. | Définition de "incomplet" (Point de vigilance #25) : check présence racine plateforme + OCSP stapling (si configuré) ; absence racine = fail, absence OCSP = warning + rejet si TRUST_STORE_REQUIRE_OCSP=true. |
| INV-287-18 | Tests roundtrip crypto | Suite test/crypto-roundtrip/ : pre.reKeyGen → reEncrypt → décapsulation simulée client avec vecteurs fixes. Obligatoire avant merge (CI gate). | C4, C14 | TC-INV-09. | Stub cryptographique — interdit, lint CI. |
| INV-287-19 | Purge proactive artefacts temporaires | RetentionWorker.purgeOnBoot() au démarrage + hook ShareStateMachine.onTerminal() planifie purge artefacts crypto temporaires (IV jetables, capsules mises en cache côté worker). | C8, C1 | TC-NOM-17, TC-NEG-04. | Crash du worker → next boot purge. |
| INV-287-20 | Lock + idempotence + rate-limit + réconciliation + clearing | Redis (ioredis) pour locks (TTL 600s scope share:*) et rate-limit (token bucket par IP/lien/preuve/compte). Table share_idempotency_keys avec contrainte UNIQUE. Worker réconciliation cron 300s. | C1, C7 | TC-NOM-05/06/15/16/18, TC-ERR-04/08/12/13/19/20, TC-NEG-01/09/10/13. | Redis down → fail-closed (retour 503) ; pas de fallback local (cohérence distribuée). |
| INV-287-21 | Atomicité DB + audit synchrone | Audit append-only dans la même transaction SQL que l'UPDATE d'état. Si AuditJournalService.append() throw → rollback transaction → 503 vers client. Ancrage Merkle async (worker séparé). | C5, C1 | TC-INV-04, TC-INV-05, TC-NOM-16, TC-ERR-14. | Outbox pattern rejeté ici (trop permissif vs fail-closed). |
| INV-287-22 | Contrôle inter-module | ShareProofGuard (C12) enregistré dans ProofsModule scope. Aucune route Proofs destinataire ne bypasse. | C12 | TC-INV-03, TC-NEG-05. | Ajout futur de route dans ProofsModule sans décorateur @ShareContext() → bypass. Mitigation : test de non-régression checklist sur ProofsController. |
| INV-287-23 | Révocation non rétroactive | Journal immuable (trigger PG). Révocation ajoute un nouvel événement, ne modifie jamais les précédents. | C5 | TC-NOM-14. | — |
| INV-287-24 | Mentions UX explicites | API owner renvoie { warnings: ['no-drm-ack-required' (si non ack), 'revocation-future-only'] } sur GET /shares. Persistance user_preferences.no_drm_ack_at_utc. | C1 | TC-NOM-11. | Scope ack = compte utilisateur (ADR-287-02). |
| INV-287-25 | Règles testables ou hors périmètre | Matrice de tests §2 spec exhaustive ; toute règle non couverte → documentée §10 (Hors périmètre). | C14 | Audit Gate 5 | — |
4. Mapping critères d'acceptation → mécanismes¶
| Critère | Mécanisme(s) | Composant | Observable | Risque |
|---|---|---|---|---|
| CA-287-01 | DTO CreateShareDto + pipeline validation ; default ttl_seconds=604800 ; bornes @Min(900) @Max(2592000) | C1 | HTTP 400 sur hors-bornes, réponse 201 avec expires_at = created_at + 604800s si omis | — |
| CA-287-02 | INSERT … state='PENDING_ACTIVATION' | C1, C13 | Row DB | — |
| CA-287-03 | ShareProofGuard refuse si pas de recipient_session_id valide | C12, C3 | 404 générique sur route proof sans session | — |
| CA-287-04 | OtpService incrément + transition état + otp_blocked_until_utc | C2 | Row share_otp_state, row share_audit_events(LINK_OTP_BLOCKED) (événement interne, pas dans enum D-287-23) | Nouvel événement audit interne — cf. Point #26 |
| CA-287-05 | OtpService.recordBlock() incrémente blocks_count, trigger NotificationDispatcher à 3 | C2, C11 | Row notification + log canal | — |
| CA-287-06 | Redis token bucket otp-resend:{share_link_id} | C2 | 429 + Retry-After | — |
| CA-287-07 | Transition ACTIVE + session + audit LINK_ACTIVATED | C2, C3, C5 | Row + événement audit | — |
| CA-287-08 | Pub/sub share.revoked + invalidation cache session + mesure P95 | C1, C3 | Metric Prometheus share_revocation_latency_p95_ms | Mesure requiert perf test dédié (k6, N≥200) |
| CA-287-09 | Worker expiration + check inline guard | C7, C12 | 404 générique post-TTL | — |
| CA-287-10 | ShareStateMachine.canTransition() + trigger PG | C1, C13 | Tentative transition rejetée (exception) | — |
| CA-287-11 | ShareProofGuard refuse sans session | C12 | 404 | — |
| CA-287-12 | Payload réponse conforme §5.4 F-287-03 | C1, C4 | Réponse contient 6 champs listés | Format encrypted_document : inline base64 streamé (ADR-287-01) |
| CA-287-13 | ExportComposerService + feature flag | C10 | ZIP vérifiable offline | Désactivé par défaut |
| CA-287-14 | Audit zero-knowledge (test sécurité) | C14 | Logs+traffic+dump+DB scrubbed | Point #11 — protocole déterministe |
| CA-287-15 | Contrôle hash WORM avant/après | C14 | SHA-256 identique | — |
| CA-287-16 | Journal append-only + worker Merkle + ancrage | C5, C6 | Rows audit + batch row + TXID blockchain | — |
| CA-287-17 | Immuabilité DB (trigger) | C5 | Rows inchangées post-revoke | — |
| CA-287-18 | GenericErrorMapper + jitter 429 | C1 | Enveloppe homogène 404 | Point #1 |
| CA-287-19 | PreCryptoService.generateEphemeralIdentity() par lien | C4 | Certs distincts | — |
| CA-287-20 | SharingController.create gated par JWT owner | C1 | 404 générique si session destinataire (anti-enum) | — |
| CA-287-21 | Redis token bucket par scope | C1 | 429 | — |
| CA-287-22 | Redis SET NX locks | C1 | 409 SHARE_BUSY | — |
| CA-287-23 | Table idempotence + JCS hash payload | C1 | 409 conflict / replay identique | Point #17 — JCS RFC8785 |
| CA-287-24 | ReconciliationWorker | C7 | État cohérent post-run | — |
| CA-287-25 | RetentionWorker.purgeOnBoot + hook terminal | C8 | Absence artefacts | — |
| CA-287-26 | API warnings[] + flag user_preferences.no_drm_ack_at_utc | C1 | Ack row | Scope compte (ADR-287-02) |
| CA-287-27 | Page activation destinataire : bandeau RGPD obligatoire (front) + event audit LINK_VIEWED avec flag rgpd_info_shown=true | C1 + front (hors périmètre backend, stubbé PD-287-FRONT) | Event audit | Stub front tracé PD-287-FRONT |
| CA-287-28 | Suite roundtrip CI gate | C14 | Tests verts | — |
| CA-287-29 | TrustStoreService fail-closed | C9 | 503 si trust-store incomplet | Point #25 |
| CA-287-30 | RetentionWorker J+90 | C8 | Rows purgées, audit conservé | — |
| CA-287-31 | OtpService transition → REVOKED sur otp_failed_attempts_total >= 50 | C2 | État final + notification | Point #4 — DoS accepté par spec |
5. Mapping tests → mécanismes + observables¶
| Test ID | Réf spec | Mécanisme(s) | Point(s) d'observation | Niveau |
|---|---|---|---|---|
| TC-NOM-01 | F-287-01, CA-287-01/02 | Création flow complet | Row DB, event audit, réponse HTTP | Integration |
| TC-NOM-02 | F-287-02, CA-287-03/07/27 | Activation OTP | Transition état, session, event, bandeau RGPD audit | Integration |
| TC-NOM-03 | F-287-02, CA-287-04 | 5 échecs OTP → OTP_BLOCKED | Row share_otp_state.otp_blocked_until_utc | Integration |
| TC-NOM-04 | 5.5 FSM | OTP_BLOCKED → ACTIVE, reset compteur | Row session + OTP state | Integration |
| TC-NOM-05 | CA-287-05 | 3 blocages → notif | Row notification | Integration |
| TC-NOM-06 | CA-287-06 | Rate-limit renvoi OTP | Redis counter + 429 | Integration |
| TC-NOM-07 | F-287-03, CA-287-12 | Consultation nominale | Payload complet 6 champs + audit LINK_VIEWED | Integration |
| TC-NOM-08 | F-287-04, CA-287-13 | Export ZIP (feature-flag ON) | ZIP valide, signature vérifiée offline | Integration |
| TC-NOM-09 | INV-287-08, CA-287-08 | Révocation → round-trip | Pub/sub + 404 suivant | Integration + Perf |
| TC-NOM-10 | F-287-06, CA-287-09 | Expiration TTL | Transition EXPIRED + event | Integration |
| TC-NOM-11 | CA-287-26, INV-287-24 | Warnings UX | warnings[] puis flag ack | Integration (API) |
| TC-NOM-12 | INV-287-05 | Identités éphémères distinctes | Certs distincts en base | Unit |
| TC-NOM-13 | INV-287-03 | WORM inchangé | SHA-256 original identique | Integration |
| TC-NOM-14 | INV-287-13/23 | Journal + hash-chain + ancrage | Rows audit + TXID | Integration |
| TC-NOM-15 | INV-287-20, CA-287-22/23 | Lock + idempotence | Redis keys + 409 | Integration |
| TC-NOM-16 | INV-287-21, CA-287-24 | Atomicité + réconciliation | Transaction + orphelins rattrapés | Integration |
| TC-NOM-17 | INV-287-19, CA-287-25 | Purge artefacts | Filesystem/DB sans résiduels | Integration |
| TC-NOM-18 | INV-287-20, CA-287-31 | Seuil OTP cumulé | Transition REVOKED + notif | Integration |
| TC-NOM-19 | INV-287-15, CA-287-30 | Rétention J+90 | Rows purgées | Integration |
| TC-ERR-01..03 | D-287-01/02/07 | Validation DTO | HTTP 400 | Unit |
| TC-ERR-04 | CA-287-21 | Rate-limit création | 429 | Integration |
| TC-ERR-05 | CA-287-18 | Anti-enum homogène | 4 cas → même 404 | Integration |
| TC-ERR-06 | ERR-287-06 | OTP invalide < seuil | 404 + compteur+1 | Integration |
| TC-ERR-07 | ERR-287-07 | Seuil session atteint | 429 + OTP_BLOCKED | Integration |
| TC-ERR-08 | ERR-287-08 | Renvoi OTP > limite | 429 | Integration |
| TC-ERR-09 | ERR-287-09 | Session inactive | 404 + reauth_required | Integration |
| TC-ERR-10 | INV-287-08 | Révocation en session | Refus round-trip suivant | Integration |
| TC-ERR-11 | INV-287-09 | Expiration en session | Refus | Integration |
| TC-ERR-12 | CA-287-22 | Lock non acquis | 409 SHARE_BUSY | Integration |
| TC-ERR-13 | CA-287-23 | Idempotence conflit | 409 IDEMPOTENCY_CONFLICT | Integration |
| TC-ERR-14 | INV-287-13/21 | Journal indispo | 503 fail-closed | Integration (fault injection) |
| TC-ERR-15 | INV-287-02 | Crypto PRE échec | 503 | Integration (fault injection) |
| TC-ERR-16 | INV-287-17 | Trust-store absent | 503 au bootstrap | Integration |
| TC-ERR-17 | ERR-287-17 | ZIP trop gros | 413 | Integration |
| TC-ERR-18 | CA-287-20 | Re-partage destinataire | 404 | Integration |
| TC-ERR-19 | ERR-287-19 | Idempotence hors fenêtre | 409 | Integration |
| TC-ERR-20 | CA-287-31 | Compteur=49 + échec | 429 + REVOKED | Integration |
| TC-INV-01..14 | Invariants | Contrôles dédiés | Rows + logs + metrics | Unit+Integration |
| TC-INV-11 | INV-287-01 | Zero-knowledge bornée | Capture tcpdump + grep mémoire + scan logs | Sécurité (protocole déterministe) |
| TC-NR-01..05 | Non-régression | Snapshot API | Golden files | Integration |
| TC-NEG-01..14 | Adversarial | Fuzz + brute-force + race | Logs sécurité | Sécurité |
| Perf P95 | CA-287-08, INV-287-08 | k6 scenario N≥200 req | Histogramme latence | Perf |
5bis. Périmètre de test¶
| Niveau | In scope | Hors scope (justification) |
|---|---|---|
| Unitaire | Tous composants C1–C12 : DTOs, ShareStateMachine, OtpService, PreCryptoService mocks HSM, parseurs JCS, GenericErrorMapper, TrustStoreService | — |
| Intégration | Interactions SharingModule ↔ ProofsModule ↔ Redis ↔ PostgreSQL ↔ stub HSM ; workers (réconciliation, retention, Merkle) contre DB réelle | Intégration Mail SMTP réelle → stubbée (stub trace PD-287-MAIL-STUB, exploité via MailService existant) |
| E2E | Flux complets owner → recipient → revoke/expire via API gateway staging | Front destinataire (hors périmètre backend, tracé PD-287-FRONT) |
| Perf | P95 révocation (CA-287-08), P95 création (§5.2), N≥200 échantillons staging mono-instance charge nominale (1 lien, 10 RPS) | Scénarios multi-instances / charge sur-nominale → PD-287-PERF-EXT |
| Sécurité | Fuzz token (length/charset), brute-force OTP multi-session, grep mémoire process, scan logs, scan DB secrets clairs, trust-store fail-closed, replay idempotence | Pen-test manuel tiers → hors périmètre MVP (PD-287-PENTEST) |
Couverture minimale attendue : 80 % lignes + 80 % branches sur le périmètre in-scope (C1–C12). Seuils mesurés via Jest coverage + --coverageThreshold.
6. Gestion des erreurs¶
Palette HTTP et mapping :
| Cas | Code | Corps | Effet bord |
|---|---|---|---|
| Validation DTO (ERR-287-01/02/03) | 400 | { error: 'VALIDATION_ERROR', details: [...] } | Aucun |
| Quota/rate-limit création (ERR-287-04) | 429 | { error: 'RATE_LIMITED' }, Retry-After: <s> | Aucun |
| Destinataire : toute erreur invalidité/inexistence (ERR-287-05/06/09/10/11) | 404 | { error: 'NOT_FOUND' } (jitter latence) | Incrément compteur OTP si applicable |
| OTP session bloqué (ERR-287-07) | 429 | { error: 'RATE_LIMITED' }, Retry-After: 900 | Transition OTP_BLOCKED |
| Resend OTP rate-limit (ERR-287-08) | 429 | Retry-After | Aucun |
| Lock non acquis (ERR-287-12) | 409 | { error: 'SHARE_BUSY' } | Aucun |
| Idempotence conflit (ERR-287-13) | 409 | { error: 'IDEMPOTENCY_CONFLICT' } | Aucun |
| Idempotence hors fenêtre (ERR-287-19) | 409 | { error: 'IDEMPOTENCY_WINDOW_EXPIRED' } | Aucun |
| Journal indispo (ERR-287-14) | 503 | { error: 'SERVICE_UNAVAILABLE' } | Rollback transaction |
| Crypto échec (ERR-287-15) | 503 | idem | Aucun |
| Trust-store (ERR-287-16) | 503 | idem | Aucun (ou crash boot si fatal) |
| ZIP trop gros (ERR-287-17) | 413 | { error: 'PAYLOAD_TOO_LARGE' } | Aucun |
| Re-partage destinataire (ERR-287-18) | 404 | { error: 'NOT_FOUND' } | Event audit sécurité (anti-enum INV-287-12) |
| Cumul OTP = 50 (ERR-287-20) | 429 | Retry-After: 0 | Transition REVOKED + notif owner |
Anti-catch-absorb (learning 2026-03-08) : tout catch qui appelle auditLog DOIT re-throw OU utiliser finally. Règle ESLint custom audit-must-throw déployée.
7. Impacts sécurité¶
| Axe | Risque | Mitigation |
|---|---|---|
| Zero-knowledge | Fuite document en mémoire/log | Backend ne déchiffre jamais. Scrubber logs. Scan CI secrets clairs (detect-secrets). |
| Anti-enumeration | 429 différenciant (écart #1) | Jitter latence 429 aligné P95 404, masquage header X-OTP-Blocked-Until. Risque résiduel documenté. |
| DoS via token volé | Seuil 50 OTP → REVOKED terminal (écart #4) | Rate-limit IP open_requests_per_ip_per_min=30 en amont. Owner peut recréer un lien. Dette : mécanisme de "freeze" intermédiaire non implémenté (hors périmètre MVP, suivi PD-287-EXT-1). |
| OTP TTL | Spec absente (écart #10) | Ajout plan : OTP_CODE_TTL_SECONDS=300s (constante, non configurable MVP), appliquée à la validation. Documenté en Point de vigilance #10 et Hypothèse H-plan-03. |
| Chiffrement données destinataire (écart #15) | Email/IP/UA en clair en DB | Ajout plan : colonnes recipient_email, client_ip, user_agent stockées via pgcrypto pgp_sym_encrypt clé dérivée KMS. recipient_email_sha256 indexé pour recherche. |
| Trust-store incomplet (écart #25) | Vérification faible | Définition : racine plateforme obligatoire + CRL téléchargeable + OCSP stapling si TRUST_STORE_REQUIRE_OCSP=true. Check explicite au bootstrap. |
| Ordre Merkle multi-batchs (écart #14) | Tree poisoning | Worker mono-instance via pg_advisory_lock(MERKLE_WORKER_LOCK_ID) + séquenceur audit_sequence_number. |
| Idempotence hashing (écart #17) | Faux positif conflit | JCS RFC 8785 appliqué avant hashing (@transmute/json-canon-rs ou équivalent). |
| Session hijacking | Session id deviné | recipient_session_id = UUIDv4 + cookie HttpOnly; Secure; SameSite=Strict + fingerprint IP log-only. |
| Replay URL token | Token propagé par email forwarding | OTP à chaque session (INV-287-04) bride l'impact ; log IP+UA pour détection anomale ; risque accepté (UX vs sécurité). |
| Timing attack OTP | Comparaison naïve | crypto.timingSafeEqual obligatoire. |
Conformité RGPD : - Consentement RGPD affiché avant activation (CA-287-27, côté front stubbé). - Droit à l'oubli : RetentionWorker J+90 ; request DELETE /owner/shares/:id/recipient-data possible en avance sur purge automatique (hors spec MVP — tracé PD-287-EXT-2). - Audit conservation légale : justifiée par valeur probatoire (art. 6.1.c RGPD obligation légale / 6.1.f intérêt légitime).
8. Hypothèses techniques¶
| ID | Hypothèse | Impact si faux |
|---|---|---|
| H-plan-01 | ProofsModule existant expose ProofsService.getEncryptedDocument(proof_id) et ProofsService.assertOwner(proof_id, userId). | Refactor ProofsModule requis (hors périmètre). |
| H-plan-02 | Service HSM existant fournit PRE (re-key-gen, re-encrypt) via wrapper ; sinon, intégration lib externe (Umbral PRE ou NuCypher ReCrypt). | Lot additionnel C4 ×2 charge ; décision architecture formelle Gate 5. |
| H-plan-03 | OTP TTL = 300s retenu (écart Gate 3 #10, non décidé par PO). | Valeur à confirmer Gate 5 ; ajustable par constante sans migration. |
| H-plan-04 | Ancrage blockchain : service existant BlockchainAnchorClient fournissant TXID sur envoi racine Merkle. | Intégration externe si absent. |
| H-plan-05 | Trust-store plateforme = fichier PEM + politique JSON chargés via env TRUST_STORE_PATH. | Alternative Vault dynamic secrets si spec infra impose. |
| H-plan-06 | Scope mémorisation warning "pas de DRM" = compte utilisateur serveur (ADR-287-02, écart #16). | Refactor UX côté propriétaire si scope device. |
| H-plan-07 | Front destinataire stubbé côté backend (stub PD-287-FRONT) — l'API renvoie toutes les données nécessaires mais le rendu est livré par une autre story. | — |
| H-plan-08 | Idempotence canonique via JCS RFC 8785 (écart #17). | Tous clients doivent soit envoyer JCS soit accepter 409 sur divergence. |
| H-plan-09 | Reauth endpoints /reauth/request-otp et /reauth/confirm partagent le même rate-limiter que l'activation initiale (écart #3). | Surface attaque reauth = activation. |
9. Points de vigilance (risques, dette, pièges)¶
- Anti-enumeration vs 429 (écart Gate 3 #1 — Bloquant) : spec impose 429 sur OTP_BLOCKED/REVOKED, INV-287-12 impose 404 homogène. Plan suit spec (429) + jitter latence. Risque résiduel de fingerprinting par latence ; documenté et accepté ; à reconfronter Gate 8 via métriques.
- Format payload consultation (écart #2 — Bloquant) : ADR-287-01 retient le document chiffré inline (base64) dans la réponse JSON vs URL présignée, car INV-287-01 interdit l'exposition d'un flux clair et une URL présignée impliquerait un service Proofs servant du chiffré — acceptable mais plus de surface. Taille borne :
max_inline_document_bytes = 50 MB, au-delà → 413 ou streaming binaireapplication/octet-streammultipart (tracé PD-287-LARGE). - Endpoints reauth (écart #3 — Bloquant) : plan les formalise (
POST /sharing/:token/reauth/request-otp,POST /sharing/:token/reauth/confirm) avec le même rate-limit et la même anti-enum que l'activation initiale. Audit eventLINK_REAUTH_*ajouté (hors enum D-287-23 — extension spec mineure à tracer dans YAML index). - DoS via seuil OTP cumulé (écart #4 — Bloquant) : plan suit spec (REVOKED terminal). Mitigation : rate-limit IP en amont + possibilité pour owner de recréer un lien. Piste d'évolution tracée PD-287-EXT-1 (seuil intermédiaire non-terminal).
- SLA P95 non déterministe en test unitaire (écart #5 — Bloquant) : exige suite perf dédiée (k6 N≥200). TC-NOM-09 split en TC-NOM-09a (fonctionnel single-request) et TC-NOM-09b (perf). Budget CI : job nightly perf-staging.
- FSM no-op (écart #6 — Majeur) :
ShareStateMachine.transition(from, to)oùfrom==to→throw ForbiddenTransitionErrorcôté API, mapping 409 owner / 404 destinataire. Plan explicite. - INV-287-08 configurabilité (écart #7 — Majeur) : plan fixe valeur non configurable MVP (constante
REVOCATION_P95_SLA_MS=2000), conforme §5.3. - Reset compteur OTP session (écart #8 — Majeur) : plan documente reset sur transition OTP_BLOCKED→ACTIVE + sur ACTIVE frais (post-activation valide). Règle explicitée dans
OtpService.resetSessionCounter(). - Formats
owner_key_ref/encrypted_doc_ref(écart #9 — Majeur) : plan définitowner_key_ref= HSM KeyHandle (opaque string ≤ 128 chars) etencrypted_document= base64 inline (cf. point 2). À proposer comme extension au §5.1 spec. - OTP TTL (écart #10 — Majeur) :
OTP_CODE_TTL_SECONDS=300s(5 min) par défaut. Cohérent avec pratique OTP mail (Stripe 15min, GitHub 10min — 5min plus strict). Ajuster Gate 5 si PO challenge. - Grep mémoire process non déterministe (écart #11 — Majeur) : TC-INV-11 formalisé en protocole reproductible : (1) lancer test sous Node
--expose-gc, (2) déclencher cycle complet, (3)global.gc()×3, (4)process.memoryUsage().heapSnapshot, (5) grep heap dump (node --heapsnapshot-signal=SIGUSR2) pour motifs document clair. Tolérance : heap dump post-3×GC sans motif. Scriptscripts/security/memory-scan.sh. max_consultations_per_link(écart #12 — Majeur) : paramètre §5.2 sans contrat → plan traite comme feature optionnelle désactivée par défaut (max_consultations_per_link=0 → illimité). Si activé par config : incrément compteur surLINK_VIEWED, transition →EXPIREDquand atteint +LINK_EXPIRED(reason: consultation_limit). Pas de test obligatoire MVP (feature dormante).- 429 sur états FSM (écart #13 — Majeur) : plan applique 429 +
Retry-Afternon informatif (Retry-After: 0pour REVOKED), conformité spec. Risque de monitoring ambigu mitigé par label métriqueretry_reason. - Ordre Merkle multi-batchs (écart #14 — Majeur) : plan impose worker Merkle mono-instance via
pg_advisory_lock+audit_sequence_numberstrictement monotone (BIGSERIAL) + batchs ordonnés par séquence. Post-crash : reprise depuis le dernier batch ancré confirmé. - Chiffrement données destinataire (écart #15 — Majeur) : plan ajoute chiffrement
pgp_sym_encryptclé KMS surrecipient_email/client_ip/user_agent. Migration + index hashé pour recherche. - Scope mémorisation warning DRM (écart #16 — Majeur) : ADR-287-02 retient compte utilisateur serveur.
- Idempotence canonique (écart #17 — Majeur) : JCS RFC 8785. Documenté dans l'API (
Idempotency-Keydoc page). - Borne D-287-31 vs seuil REVOKED (écart #18 — Mineur) : plan définit check
count >= 50avant incrément (sémantique : la 50e tentative échouée déclenche REVOKED, stockage max 49 ou 50 selon transaction). Garde-fou race via lock OTP. - H-287-07 manquante (écart #19 — Mineur) : coquille spec. Plan non impacté.
- Export skip (écart #20 — Mineur) : tests export
describe.skipconditionnel surprocess.env.EXPORT_ZIP_ENABLED !== 'true'. Documenté CI. - D-287-09 formule vs regex (écart #21 — Mineur) : plan traite comme règle de cohérence, validée par trigger PG
CHECK (expires_at = created_at + make_interval(secs => ttl_seconds)). - Ownership mutable (écart #22 — Mineur) : plan documente hypothèse ownership immuable durant la vie du lien. Si futur transfert : REVOKED automatique (trigger PG
ON UPDATE proofs.owner_user_id). Hors périmètre MVP, tracé PD-287-EXT-3. - Diagramme d'état incomplet (écart #23 — Mineur) : plan ne modifie pas la spec, documente en annotation.
- Périmètre INV-287-01 (écart #24 — Mineur) : plan énumère le périmètre d'audit : services C1, C2, C3, C4, C5, C6, C7, C8, C10, C12 + logs applicatifs + DB secondaires (replicas). Exclu : systèmes externes (HSM, KMS, Blockchain client).
- Trust-store incomplet (écart #25 — Mineur) : définition formelle dans
TrustStoreService. - Événements audit hors enum D-287-23 : plan étend l'enum contractuel
audit_event_typeavec 3 événements additionnels :LINK_OTP_BLOCKED,LINK_REAUTH_REQUESTED,LINK_REAUTH_CONFIRMED. Ces événements sont inclus dans la même chaîne hash et le même journal append-only (pas de table séparée). Cette extension est une correction spec proposée alignée sur les flux réauth documentés dans la spec §5.5. Les tests TC correspondants doivent couvrir ces événements. - Charge Redis : single-instance staging MVP. Multi-instance → tracé PD-287-HA.
- HSM rate-limit :
PreCryptoServicepeut saturer HSM en cas de pic. Circuit breaker (opossum) + file d'attente bornée.
10. Hors périmètre¶
- Front destinataire (web/mobile) : livré par story dédiée (stub
PD-287-FRONT). Les tests contractuels TC-NOM-02 (info RGPD avant activation) et TC-NOM-11 (warning DRM propriétaire) sont vérifiés côté backend via les réponses API (champrgpd_notice_urldans la réponse activation, champno_drm_ack_requireddans la réponse création). La vérification UI complète est déléguée àPD-287-FRONT. - Pentest manuel tiers :
PD-287-PENTEST. - Multi-instance HA Redis/Postgres :
PD-287-HA. - Pages OAuth / SSO destinataire (invitation remplace compte) : hors scope par spec.
- Mobile offline consultation : exclu par spec §2.
- Partage inter-comptes ProbatioVault : exclu par spec.
- Décision judiciaire admissibilité : non technique.
- Révocation partielle (consultation vs export) : exclue par spec.
- Heartbeats signés (preuve d'absence d'accès) : hors MVP.
- Transfert ownership pendant vie du lien :
PD-287-EXT-3. - Seuil intermédiaire non-terminal anti-DoS OTP :
PD-287-EXT-1. - API
DELETE /owner/shares/:id/recipient-dataon-demand :PD-287-EXT-2. - Scénarios perf sur-nominaux :
PD-287-PERF-EXT. - Performance multi-instance / failover : hors périmètre MVP.
Annexe A — ADR courts¶
ADR-287-01 — Livraison du document chiffré destinataire - Contexte : spec §5.4 F-287-03 dit "document chiffré" ; diagramme dit encrypted_doc_ref. - Décision : livraison inline base64 dans la réponse JSON (jusqu'à 50 MB), sinon stream binaire. - Conséquences : simplicité de l'atomicité zero-knowledge, pas de second service exposé. Charge réseau accrue vs URL présignée ; acceptable pour documents probatoires (<= 50 MB typique).
ADR-287-02 — Scope mémorisation warning "pas de DRM" - Contexte : écart Gate 3 #16. - Décision : persistance serveur au niveau compte utilisateur (user_preferences.no_drm_ack_at_utc). - Conséquences : cohérence multi-device, testable server-side, aligné privacy (pas de cookie).
ADR-287-03 — Worker Merkle mono-instance - Contexte : écart Gate 3 #14. - Décision : pg_advisory_lock garantit un seul worker actif simultané ; séquenceur audit_sequence_number strict. - Conséquences : SPOF worker (mitigé par failover rapide via advisory lock release sur crash TCP). Alternative multi-instance avec coordination (ZooKeeper/etcd) hors périmètre MVP.