Aller au contenu

PD-103 — Agent Developer — Module M4 : capture-upload

1. Identite agent

  • Agent : agent-developer (Agent B — Claude)
  • Story : PD-103
  • Module : M4 — capture-upload
  • Wave : 2 (depend de M2 crypto-pipeline)
  • Date : 2026-04-03

2. Resume

Module M4 implemente le service d'upload capture probatoire : upload single (< 10 MB) et multipart (>= 10 MB) vers S3 via URLs pre-signees, avec retry borne, controle d'integrite ciphertext (x-amz-content-sha256), annulation cooperative, reprise differee avec conservation des ETags, et POST backend /documents/capture.

Fichiers crees : - src/capture/upload-service.ts — Service principal : presigned URL, upload single, POST backend, retry - src/capture/multipart-upload.ts — Orchestration upload multipart : init, parts, completion, reprise

Fichiers existants reutilises : - src/capture/types.ts — Branded types, constantes, interfaces (DTOs, PresignedUrlResponse, etc.) - src/crypto/utils.ts — bytesToHex pour header x-amz-content-sha256 - @noble/hashes/sha256 — SHA-256 pour controle integrite S3 (INV-103-06) - src/config/index.ts — config.apiUrl pour base URL backend - Pattern : src/services/upload/uploadNetwork.ts (fetchWithTimeout, abort) - Pattern : src/services/upload/uploadRetry.ts (backoff exponentiel + jitter)

3. Artefacts livres

Fichier Role Lignes
src/capture/upload-service.ts Service upload single + presigned URL + POST backend + retry ~420
src/capture/multipart-upload.ts Upload multipart : init, parts, completion, abort, reprise ~440

4. Architecture

4.1 Decisions architecturales

architectural_decisions:
  - decision: "x-amz-content-sha256 au lieu de Content-MD5 pour l'integrite S3"
    rationale: "@noble/hashes/sha256 est deja present dans le projet ; evite d'ajouter une dependance MD5 (crypto-js/md5 ou autre). SHA-256 est plus fort et supporte nativement par S3."
    alternatives_considered:
      - "Content-MD5 via crypto-js"
      - "Double header Content-MD5 + x-amz-content-sha256"
    trade_offs: "SHA-256 est legerement plus couteux que MD5 mais negligeable sur des chunks de 5 MB."

  - decision: "Separation CaptureUploadService (single + orchestration) et CaptureMultipartUpload (multipart specifique)"
    rationale: "Respecte le plan (upload-service.ts + multipart-upload.ts). Le service principal decide du mode (single/multipart) selon le seuil 10 MB et delegue au module multipart si necessaire."
    alternatives_considered:
      - "Classe unique avec modes internes"
      - "Fonctions pures sans classe"
    trade_offs: "Deux fichiers mais chacun reste sous 500 lignes ; separation claire des responsabilites."

4.2 Pattern utilise

Retry borne avec backoff exponentiel + jitter (pattern src/services/upload/uploadRetry.ts) : - Jitter CSPRNG obligatoire (crypto.getRandomValues, pas Math.random) - Annulation cooperative via AbortSignal - Erreurs non-retryable propagees immediatement (4xx sauf 500+)

Multipart avec reprise partielle (§5.5bis) : - Init multipart → upload parts avec ETags → CompleteMultipartUpload - En cas d'echec part : MultipartUploadError porte les completedParts et uploadId - L'orchestrateur (M6) peut persister ces donnees dans DeferredCapturePayload.multipartState - A la reprise : URLs fraiches demandees, parts deja completees skippees

Progression granulaire : - Single : 0% → 100% - Multipart : progression part par part avec currentPart / totalParts

4.3 Dependances

M4 (capture-upload)
├── M2 (capture-crypto) — consomme CryptoCapturePipelineResult (ciphertext, nonce, tag, dek)
├── types.ts — PresignedUrlResponse, MultipartInitResponse, CaptureIngestPayload, constantes
├── @noble/hashes/sha256 — x-amz-content-sha256 integrite S3
├── src/crypto/utils.ts — bytesToHex
└── src/config — apiUrl

4.4 Flux detaille

upload-service.ts                         multipart-upload.ts
┌──────────────────┐                     ┌──────────────────────┐
│ CaptureUploadSvc │                     │ CaptureMultipartUpld │
│                  │                     │                      │
│ upload()         │                     │ execute()            │
│  1. presignURL   │                     │  1. initMultipart    │
│  2. if < 10MB:   │                     │  2. for each part:   │
│     uploadSingle │                     │     uploadPartRetry  │
│  3. if >= 10MB: ─┼────────────────────>│  3. completeMpart    │
│     delegate     │                     │                      │
│  4. POST /cap    │                     │ abort()              │
│                  │                     │  AbortMultipartUpld  │
│ resumeDeferred() │                     │                      │
│  = upload()      │                     │ _refreshPartUrls()   │
│                  │                     │  URLs fraiches       │
│ cancelUpload()   │                     └──────────────────────┘
│  abort best-eff  │
└──────────────────┘

5. Mapping invariants

Invariant Mecanisme Observable
INV-103-06 Header x-amz-content-sha256 sur chaque PUT S3 (single + chaque part multipart) Header present dans requete
INV-103-22 upload() retourne seulement apres S3 OK + POST backend 202/200 Aucun resultat partiel
INV-103-23 Exception propagee apres retries epuises → orchestrateur transite UPLOAD_DEFERRED CaptureUploadError / MultipartUploadError
INV-103-24 resumeDeferred() redemande systematiquement une URL pre-signee fraiche Nouvelle URL avant PUT
INV-103-29 cancelUpload() + MultipartUpload.abort() : AbortMultipartUpload best-effort Appel backend abort
INV-103-31 capture_id passe tel quel depuis metadata (deja lowercase par asCaptureId) Payload POST lowercase

6. Mapping tests contractuels

Test ID Reference spec Mecanisme Niveau
TC-NOM-01 INV-103-06, 22, 30 upload single + POST avec dek_wrapped_b64 + kek_id + x-amz-content-sha256 Integration
TC-NOM-05 INV-103-23 Retries epuises → exception propagee (orchestrateur gere UPLOAD_DEFERRED) Unit
TC-NOM-06 INV-103-24 resumeDeferred() redemande presigned URL Integration
TC-NOM-13 §5.5bis, INV-103-22 Multipart : echec part → reprise partielle avec ETags conserves Integration
TC-NOM-14 INV-103-29 cancelUpload() appelle abort multipart Unit
TC-ERR-01 INV-103-23 Retry borne epuise → exception Unit
TC-ERR-06 §5.1 POST backend avec champ invalide → HTTP 400 Unit
TC-ERR-14 INV-103-06 Checksum mismatch → rejet + retry Unit
TC-ERR-16 INV-103-34 POST backend → 422 UNWRAP_DEK_FAILED Unit
TC-ERR-17 ER-103-17 POST backend → 503 KEY_SERVICE_UNAVAILABLE Unit
TC-INV-09 INV-103-29 Annulation signal → abort multipart + purge Integration

7. API publique

CaptureUploadService

class CaptureUploadService {
  constructor(apiBaseUrl?: string);

  /** Upload complet : presigned URL → S3 PUT → POST /documents/capture */
  upload(
    ciphertext: Uint8Array,
    metadata: CaptureMetadata,
    hashSha3_256: Sha3_256Hash,
    cryptoArtifacts: CaptureUploadCryptoArtifacts,
    jwtToken: string,
    ocr?: OcrResult,
    options?: CaptureUploadOptions,
  ): Promise<CaptureUploadResult>;

  /** Reprise differee (nouvelle URL pre-signee + meme flux) */
  resumeDeferred(/* memes params */): Promise<CaptureUploadResult>;

  /** Annulation S3 : abort multipart best-effort */
  cancelUpload(captureId: CaptureId, uploadId: string | null, jwtToken: string): Promise<void>;
}

CaptureMultipartUpload

class CaptureMultipartUpload {
  constructor(apiBaseUrl: string);

  /** Upload multipart complet ou en reprise partielle */
  execute(
    ciphertext: Uint8Array,
    metadata: CaptureMetadata,
    jwtToken: string,
    options?: MultipartUploadOptions,
  ): Promise<MultipartUploadResult>;

  /** Abort multipart S3 best-effort (INV-103-29) */
  abort(captureId: string, uploadId: string, jwtToken: string): Promise<void>;
}

Types export

/** Erreur upload avec code metier + httpStatus + isRetryable */
CaptureUploadError

/** Erreur multipart avec resumeState (completedParts, uploadId) pour reprise */
MultipartUploadError
  toResumeState(): MultipartResumeState | null

/** Options upload : retries, backoff, signal, onProgress */
CaptureUploadOptions

/** Resultat upload : uploadObjectKey + ingestResponse */
CaptureUploadResult

/** Artefacts crypto passes au service (nonce, tag, dek_wrapped, kek_id) */
CaptureUploadCryptoArtifacts

8. Gestion des erreurs

Situation Code HTTP Retryable Action orchestrateur
Reseau indisponible UPLOAD_FAILED Oui Retry → UPLOAD_DEFERRED
Timeout PUT S3 UPLOAD_FAILED Oui Retry → UPLOAD_DEFERRED
S3 500+ UPLOAD_FAILED 5xx Oui Retry → UPLOAD_DEFERRED
Part multipart echoue UPLOAD_FAILED UPLOAD_DEFERRED + ETags
Annulation signal UPLOAD_FAILED Non CANCELLED + abort S3
Payload invalide 400 code metier 400 Non CANCELLED
Conflict idempotence UPLOAD_FAILED 409 Non Log + traiter selon politique
Unwrap DEK echoue UNWRAP_DEK_FAILED 422 Non CANCELLED
Rate-limit UPLOAD_FAILED 429 Non Retry apres delai
HSM indisponible KEY_SERVICE_UNAVAILABLE 503 Oui Retry backend

9. Hypotheses

ID Hypothese Impact si faux
HU-01 Le backend expose POST /documents/capture/presign pour obtenir URL pre-signee Adapter endpoint
HU-02 Le backend expose POST /documents/capture/multipart-init et /multipart-complete Adapter endpoints multipart
HU-03 S3 retourne un header ETag sur chaque PUT part reussi Fallback ETag synthetique
HU-04 Le JWT est passe en parametre par l'orchestrateur (pas de refresh automatique dans M4) Le refresh JWT est la responsabilite de M6

10. Points hors perimetre M4

  • Transition FSM : responsabilite de M1 (state-machine) via M6 (orchestrator)
  • Persistance locale ciphertext : responsabilite de M7 (purge-manager)
  • Stockage deferred dans Zustand : responsabilite de M5 (capture-store)
  • Refresh JWT : responsabilite de M6 (orchestrator) ou module auth existant
  • Purge locale post-UPLOADED : responsabilite de M7

11. Matrice de couverture tests

Test-ID Fichiers de test
TC-NOM-01 src/__tests__/capture/upload-service.test.ts (a creer)
TC-NOM-05 src/__tests__/capture/upload-service.test.ts
TC-NOM-06 src/__tests__/capture/upload-service.test.ts
TC-NOM-13 src/__tests__/capture/multipart-upload.test.ts (a creer)
TC-NOM-14 src/__tests__/capture/upload-service.test.ts
TC-ERR-01 src/__tests__/capture/upload-service.test.ts
TC-ERR-14 src/__tests__/capture/multipart-upload.test.ts
TC-ERR-16 src/__tests__/capture/upload-service.test.ts
TC-ERR-17 src/__tests__/capture/upload-service.test.ts
TC-INV-09 src/__tests__/capture/multipart-upload.test.ts

Note : Les fichiers de test ne sont pas dans le perimetre de cet agent (Wave 2). Ils seront crees par l'agent de test ou lors de Wave 4 (M14 capture-tests-backend).