PD-103 — Agent Developer — Module capture-types¶
1. Identite agent¶
- Agent : agent-developer (Agent B — Claude)
- Story : PD-103
- Module : capture-types
- Projet : ProbatioVault-app (docs)
2. Mission¶
Definir l'ensemble des types TypeScript partages pour le module capture probatoire PD-103 : branded types, union litterale des etats, interfaces de donnees, constantes contractuelles, regex de validation, DTOs, et types d'erreur. Ce module est la fondation de type-safety pour tous les autres modules (M1..M8).
3. Fichier cible¶
src/capture/types.ts
4. Implementation¶
// ============================================================================
// src/capture/types.ts — PD-103
// ----------------------------------------------------------------------------
// Types TypeScript : etats, branded types, DTOs, constantes contractuelles
// pour la capture probatoire d'ecran et scellement automatique.
//
// Alignement : PD-103-specification.md v3, §3, §4, §5.1, §5.2, §5.7, §5.12
// ============================================================================
// =============================================================================
// Branded Types (INV contract : pas de string nu pour capture_id / device_id)
// Pattern existant : src/types/seal.ts, src/types/upload.ts
// =============================================================================
/** UUID v4 lowercase — identifiant unique de capture (§3, INV-103-31) */
export type CaptureId = string & { readonly __brand: "CaptureId" };
/** UUID v4 — identifiant du device (§5.1) */
export type DeviceId = string & { readonly __brand: "DeviceId" };
/** SHA3-256 hex lowercase, 64 chars (§5.1, INV-103-03) */
export type Sha3_256Hash = string & { readonly __brand: "Sha3_256Hash" };
/** AES-GCM nonce base64, 16 chars / 12 bytes (§5.1) */
export type AesGcmNonceB64 = string & { readonly __brand: "AesGcmNonceB64" };
/** AES-GCM auth tag base64, 24 chars / 16 bytes (§5.1) */
export type AesGcmTagB64 = string & { readonly __brand: "AesGcmTagB64" };
/** DEK wrappe RSA-OAEP-SHA256, base64 (§5.1, INV-103-30) */
export type DekWrappedB64 = string & { readonly __brand: "DekWrappedB64" };
/** Identifiant de la KEK publique utilisee au wrapping (§5.1, INV-103-34) */
export type KekId = string & { readonly __brand: "KekId" };
/** SemVer version applicative (§5.1) */
export type AppVersion = string & { readonly __brand: "AppVersion" };
/** RFC3339 UTC timestamp device (§5.1) */
export type TimestampDevice = string & { readonly __brand: "TimestampDevice" };
/** Texte OCR UTF-8 imprimable, 0..20000 chars (§5.1) */
export type OcrText = string & { readonly __brand: "OcrText" };
/** Tag BCP-47, 2..35 chars (§5.1) */
export type OcrLanguage = string & { readonly __brand: "OcrLanguage" };
/** Cle S3 de l'objet uploade (§5.12) */
export type UploadObjectKey = string & { readonly __brand: "UploadObjectKey" };
// =============================================================================
// Constructeurs branded types (pattern existant : asSealId, toDocId)
// =============================================================================
export function asCaptureId(value: string): CaptureId {
return value.toLowerCase() as CaptureId; // INV-103-31 : normalisation lowercase
}
export function asDeviceId(value: string): DeviceId {
return value as DeviceId;
}
export function asSha3_256Hash(value: string): Sha3_256Hash {
return value as Sha3_256Hash;
}
export function asAesGcmNonceB64(value: string): AesGcmNonceB64 {
return value as AesGcmNonceB64;
}
export function asAesGcmTagB64(value: string): AesGcmTagB64 {
return value as AesGcmTagB64;
}
export function asDekWrappedB64(value: string): DekWrappedB64 {
return value as DekWrappedB64;
}
export function asKekId(value: string): KekId {
return value as KekId;
}
export function asAppVersion(value: string): AppVersion {
return value as AppVersion;
}
export function asTimestampDevice(value: string): TimestampDevice {
return value as TimestampDevice;
}
export function asOcrText(value: string): OcrText {
return value as OcrText;
}
export function asOcrLanguage(value: string): OcrLanguage {
return value as OcrLanguage;
}
export function asUploadObjectKey(value: string): UploadObjectKey {
return value as UploadObjectKey;
}
// =============================================================================
// CaptureState — Union litterale exhaustive des 8 etats PD-103 (§5.7)
// =============================================================================
export const CAPTURE_STATES = [
"CAPTURED",
"UPLOADING",
"UPLOAD_DEFERRED",
"UPLOADED",
"PENDING_SEAL",
"SEALED",
"ANCHOR_CONFIRMED",
"CANCELLED",
] as const;
export type CaptureState = (typeof CAPTURE_STATES)[number];
export const TERMINAL_CAPTURE_STATES: readonly CaptureState[] = [
"ANCHOR_CONFIRMED",
"CANCELLED",
] as const;
// =============================================================================
// SignatureStatus — Enum backend (§5.1, INV-103-08)
// =============================================================================
export const SIGNATURE_STATUSES = ["PENDING_SIGNATURE", "SIGNED"] as const;
export type SignatureStatus = (typeof SIGNATURE_STATUSES)[number];
// =============================================================================
// Transitions autorisees — Table exhaustive (§5.7)
// =============================================================================
export const ALLOWED_TRANSITIONS: Readonly<Record<CaptureState, readonly CaptureState[]>> = {
CAPTURED: ["UPLOADING", "CANCELLED"],
UPLOADING: ["UPLOADED", "UPLOAD_DEFERRED", "CANCELLED"],
UPLOAD_DEFERRED: ["UPLOADING", "CANCELLED"],
UPLOADED: ["PENDING_SEAL"],
PENDING_SEAL: ["SEALED"],
SEALED: ["ANCHOR_CONFIRMED"],
ANCHOR_CONFIRMED: [],
CANCELLED: [],
} as const;
// =============================================================================
// Validation patterns — Regex contractuelles (§5.1)
// =============================================================================
export const CAPTURE_VALIDATION_PATTERNS = {
/** UUID v4 (accepte upper/lower, normalise ensuite en lowercase) */
UUID_V4: /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/,
/** SHA3-256 hex lowercase, 64 chars */
HASH_SHA3_256: /^[a-f0-9]{64}$/,
/** RFC3339 UTC strict (§5.1) */
TIMESTAMP_DEVICE: /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?Z$/,
/** SemVer (§5.1) */
APP_VERSION: /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/,
/** AES-GCM nonce base64 — 16 chars / 12 bytes */
AES_GCM_NONCE_B64: /^[A-Za-z0-9+/]{16}$/,
/** AES-GCM auth tag base64 — 24 chars / 16 bytes */
AES_GCM_TAG_B64: /^[A-Za-z0-9+/]{22}==$/,
/** DEK wrappe base64 — 128..4096 chars */
DEK_WRAPPED_B64: /^[A-Za-z0-9+/]+={0,2}$/,
/** KEK ID opaque — 1..64 chars alphanum + ._- */
KEK_ID: /^[A-Za-z0-9._-]{1,64}$/,
/** BCP-47 language tag */
OCR_LANGUAGE: /^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/,
} as const;
// =============================================================================
// Fonctions de validation (pattern existant : isValidDocId, isValidSha3Hash)
// =============================================================================
export function isValidCaptureId(id: string): boolean {
return CAPTURE_VALIDATION_PATTERNS.UUID_V4.test(id);
}
export function isValidHashSha3_256(hash: string): boolean {
return CAPTURE_VALIDATION_PATTERNS.HASH_SHA3_256.test(hash);
}
export function isValidTimestampDevice(ts: string): boolean {
return CAPTURE_VALIDATION_PATTERNS.TIMESTAMP_DEVICE.test(ts);
}
export function isValidAppVersion(version: string): boolean {
return CAPTURE_VALIDATION_PATTERNS.APP_VERSION.test(version);
}
export function isValidAesGcmNonceB64(nonce: string): boolean {
return CAPTURE_VALIDATION_PATTERNS.AES_GCM_NONCE_B64.test(nonce);
}
export function isValidAesGcmTagB64(tag: string): boolean {
return CAPTURE_VALIDATION_PATTERNS.AES_GCM_TAG_B64.test(tag);
}
export function isValidDekWrappedB64(dek: string): boolean {
return CAPTURE_VALIDATION_PATTERNS.DEK_WRAPPED_B64.test(dek) && dek.length >= 128 && dek.length <= 4096;
}
export function isValidKekId(kekId: string): boolean {
return CAPTURE_VALIDATION_PATTERNS.KEK_ID.test(kekId);
}
export function isValidOcrLanguage(lang: string): boolean {
return CAPTURE_VALIDATION_PATTERNS.OCR_LANGUAGE.test(lang);
}
export function isTerminalCaptureState(state: CaptureState): boolean {
return (TERMINAL_CAPTURE_STATES as readonly string[]).includes(state);
}
// =============================================================================
// Constantes contractuelles (§5.2, §5.3)
// =============================================================================
/** 500 MB en bytes — taille max image (§5.2) */
export const CAPTURE_SIZE_LIMIT = 524_288_000;
/** 10 MB en bytes — seuil multipart fixe produit (§5.2) */
export const MULTIPART_THRESHOLD = 10_000_000;
/** 5 MB en bytes — taille chunk multipart par defaut (§5.2) */
export const DEFAULT_CHUNK_SIZE = 5_242_880;
/** Bornes chunk size (§5.2) */
export const MIN_CHUNK_SIZE = 5_242_880;
export const MAX_CHUNK_SIZE = 52_428_800;
/** Max retries upload auto — defaut (§5.2) */
export const DEFAULT_UPLOAD_RETRIES = 3;
export const MIN_UPLOAD_RETRIES = 0;
export const MAX_UPLOAD_RETRIES = 5;
/** Backoff base retry en ms (§5.2) */
export const DEFAULT_BACKOFF_BASE_MS = 1_000;
export const MIN_BACKOFF_BASE_MS = 1_000;
export const MAX_BACKOFF_BASE_MS = 10_000;
/** TTL URL pre-signee S3 en secondes (§5.2) */
export const DEFAULT_PRESIGNED_URL_TTL_S = 900;
export const MIN_PRESIGNED_URL_TTL_S = 60;
export const MAX_PRESIGNED_URL_TTL_S = 900;
/** TTL session upload differe en secondes — defaut 24h (§5.2, §5.3) */
export const DEFAULT_DEFERRED_UPLOAD_TTL_S = 86_400;
export const MIN_DEFERRED_UPLOAD_TTL_S = 3_600;
export const MAX_DEFERRED_UPLOAD_TTL_S = 86_400;
/** Tolerance skew timestamp_device en secondes — fixe +-300s (§5.2) */
export const TIMESTAMP_SKEW_TOLERANCE_S = 300;
/** Rate-limit POST /documents/capture — defaut (§5.2) */
export const DEFAULT_RATE_LIMIT_PER_MIN = 60;
/** TTL objet S3 orphelin en secondes — defaut (§5.2) */
export const DEFAULT_ORPHAN_TTL_S = 900;
export const MIN_ORPHAN_TTL_S = 300;
export const MAX_ORPHAN_TTL_S = 3_600;
/** Profondeur keyring KEK — anciennes cles (§5.2) */
export const DEFAULT_KEK_KEYRING_DEPTH = 3;
/** Cycles conformes consecutifs pour clearing SEAL_DELAYED (§5.9, INV-103-36) */
export const SEAL_DELAYED_CLEARING_CYCLES = 3;
/** Latence capture P95 max en ms (§5.2, CA-103-01) */
export const CAPTURE_LATENCY_SLA_MS = 3_000;
/** Latence capture->upload P95 max en ms (§5.2, CA-103-08) */
export const CAPTURE_UPLOAD_SLA_MS = 5_000;
/** Seal SLA defaut en ms — 10 min (§5.3) */
export const SEAL_SLA_MS = 600_000;
/** Longueur max ocr_text (§5.1) */
export const OCR_TEXT_MAX_LENGTH = 20_000;
/** MIME type contractuel capture (§5.1) */
export const CAPTURE_MIME_TYPE = "image/png" as const;
/** Lock distribue TTL defaut en secondes (§5.9) */
export const DEFAULT_LOCK_TTL_S = 300;
/** Lock retry delay en secondes (§5.9) */
export const LOCK_RETRY_DELAY_S = 30;
// =============================================================================
// Metadonnees capture — Interface locale (§5.4)
// =============================================================================
export interface CaptureMetadata {
readonly captureId: CaptureId;
readonly deviceId: DeviceId;
readonly appVersion: AppVersion;
readonly mimeType: typeof CAPTURE_MIME_TYPE;
readonly sizeBytes: number;
readonly timestampDevice: TimestampDevice;
}
// =============================================================================
// OCR result — Interface locale optionnelle (§5.4, INV-103-04)
// =============================================================================
export interface OcrResult {
readonly ocrText: OcrText;
readonly ocrConfidence: number; // [0.00, 1.00]
readonly ocrLanguage: OcrLanguage;
}
// =============================================================================
// Crypto artifacts — Resultats du pipeline crypto local (§5.4)
// =============================================================================
export interface CaptureEncryptionResult {
readonly ciphertext: Uint8Array;
readonly aesGcmNonceB64: AesGcmNonceB64;
readonly aesGcmTagB64: AesGcmTagB64;
readonly dekWrappedB64: DekWrappedB64;
readonly kekId: KekId;
}
// =============================================================================
// POST /documents/capture — DTO requete (§5.12)
// =============================================================================
export interface CaptureIngestPayload {
readonly capture_id: string; // CaptureId, serialise lowercase
readonly hash_sha3_256: string; // Sha3_256Hash
readonly timestamp_device: string; // TimestampDevice RFC3339 UTC
readonly device_id: string; // DeviceId
readonly app_version: string; // AppVersion SemVer
readonly mime_type: typeof CAPTURE_MIME_TYPE;
readonly size_bytes: number;
readonly aes_gcm_nonce_b64: string; // AesGcmNonceB64
readonly aes_gcm_tag_b64: string; // AesGcmTagB64
readonly dek_wrapped_b64: string; // DekWrappedB64
readonly kek_id: string; // KekId
readonly upload_object_key: string; // UploadObjectKey — reference S3
readonly ocr_text?: string; // OcrText — optionnel
readonly ocr_confidence?: number; // [0.00, 1.00] — optionnel
readonly ocr_language?: string; // OcrLanguage BCP-47 — optionnel
}
// =============================================================================
// POST /documents/capture — DTO reponse (§5.12)
// =============================================================================
export interface CaptureIngestResponse {
readonly capture_id: string;
readonly status: CaptureState;
readonly signature_status: SignatureStatus;
readonly created_at: string;
}
// =============================================================================
// Presigned URL — DTO (§5.5, §5.5bis)
// =============================================================================
export interface PresignedUrlResponse {
readonly url: string;
readonly object_key: string;
readonly expires_at: string; // ISO 8601 UTC
}
export interface MultipartInitResponse {
readonly upload_id: string;
readonly parts: readonly PresignedPartUrl[];
readonly object_key: string;
}
export interface PresignedPartUrl {
readonly part_number: number;
readonly url: string;
}
export interface CompletedPart {
readonly partNumber: number;
readonly etag: string;
}
// =============================================================================
// Upload differe — Payload local chiffre (§5.5, M5/M7)
// =============================================================================
export interface DeferredCapturePayload {
readonly captureId: CaptureId;
readonly ciphertextPath: string; // chemin local fichier chiffre
readonly metadata: CaptureMetadata;
readonly hashSha3_256: Sha3_256Hash;
readonly aesGcmNonceB64: AesGcmNonceB64;
readonly aesGcmTagB64: AesGcmTagB64;
readonly dekWrappedB64: DekWrappedB64;
readonly kekId: KekId;
readonly ocr?: OcrResult;
readonly deferredAt: string; // ISO 8601 UTC
readonly multipartState?: MultipartResumeState;
}
export interface MultipartResumeState {
readonly uploadId: string;
readonly objectKey: string;
readonly completedParts: readonly CompletedPart[];
readonly totalParts: number;
readonly chunkSize: number;
}
// =============================================================================
// Erreurs capture — Codes metier (§6, §5.12)
// =============================================================================
export const CAPTURE_ERROR_CODES = {
TIMESTAMP_SKEW_EXCEEDED: "TIMESTAMP_SKEW_EXCEEDED",
UNWRAP_DEK_FAILED: "UNWRAP_DEK_FAILED",
KEY_SERVICE_UNAVAILABLE: "KEY_SERVICE_UNAVAILABLE",
HASH_MISMATCH: "HASH_MISMATCH",
ENCRYPTION_FAILED: "ENCRYPTION_FAILED",
UPLOAD_FAILED: "UPLOAD_FAILED",
UPLOAD_SESSION_EXPIRED: "UPLOAD_SESSION_EXPIRED",
INVALID_TRANSITION: "INVALID_TRANSITION",
TERMINAL_STATE: "TERMINAL_STATE",
CHECKSUM_MISMATCH: "CHECKSUM_MISMATCH",
OCR_FAILED: "OCR_FAILED",
PURGE_FAILED: "PURGE_FAILED",
PRESIGNED_URL_EXPIRED: "PRESIGNED_URL_EXPIRED",
} as const;
export type CaptureErrorCode = (typeof CAPTURE_ERROR_CODES)[keyof typeof CAPTURE_ERROR_CODES];
export interface CaptureError {
readonly code: CaptureErrorCode;
readonly message: string;
readonly httpStatus?: number;
}
// =============================================================================
// HTTP error mapping (§6)
// =============================================================================
export const CAPTURE_HTTP_ERROR_KEYS: Readonly<Record<number, string>> = {
400: "capture.error.badRequest",
401: "capture.error.unauthorized",
403: "capture.error.forbidden",
409: "capture.error.conflict",
422: "capture.error.unwrapFailed",
429: "capture.error.rateLimited",
503: "capture.error.serviceUnavailable",
} as const;
// =============================================================================
// Transition log — Historique des transitions (observabilite §8)
// =============================================================================
export interface CaptureTransitionLog {
readonly captureId: CaptureId;
readonly from: CaptureState;
readonly to: CaptureState;
readonly timestamp: string; // ISO 8601 UTC
readonly reason?: string;
}
// =============================================================================
// Payload canonique d'idempotence — Cles triees alphabetiquement (§5.12)
// =============================================================================
export interface CanonicalPayload {
readonly aes_gcm_nonce_b64: string;
readonly aes_gcm_tag_b64: string;
readonly capture_id: string; // lowercase
readonly content_hash: string; // lowercase hash_sha3_256
readonly dek_wrapped_b64: string;
readonly kek_id: string;
readonly mime_type: typeof CAPTURE_MIME_TYPE;
readonly size_bytes: number;
readonly upload_object_key: string;
}
// =============================================================================
// Upload progress (M4, M8 — indicateur progression UI)
// =============================================================================
export interface CaptureUploadProgress {
readonly percent: number; // 0..100
readonly bytesUploaded: number;
readonly bytesTotal: number;
readonly currentPart?: number; // multipart only
readonly totalParts?: number; // multipart only
}
// =============================================================================
// Purge result (M7 — purgeStale + purge locale post-UPLOADED)
// =============================================================================
export interface CapturePurgeResult {
readonly filesDeleted: number;
readonly paths: readonly string[];
readonly captureId?: CaptureId;
}
// =============================================================================
// Orchestrator result (M6 — resultat du flux complet)
// =============================================================================
export type CaptureOrchestratorResult =
| { readonly success: true; readonly captureId: CaptureId; readonly finalState: "UPLOADED" }
| { readonly success: false; readonly captureId: CaptureId; readonly finalState: "UPLOAD_DEFERRED" | "CANCELLED"; readonly error?: CaptureError };
5. Decisions architecturales¶
Decision 1 : Pattern branded types + constructeurs nommes¶
- Decision : Reprendre le pattern existant
seal.ts/upload.ts(branded types + fonctionsasFoo()/toFoo()) plutot que des classes wrapper. - Rationale : Coherence avec le codebase existant (
SealId,DocId,Sha3Hash). Zero overhead runtime, protection compile-time uniquement. - Alternatives : Classes wrapper avec validation au constructeur (rejete : overhead memoire mobile, casse l'interoperabilite JSON).
- Trade-offs : Pas de validation runtime dans le constructeur (la validation est faite separement via
isValidXxx()), mais compatible avec le pattern etabli.
Decision 2 : asCaptureId() normalise en lowercase¶
- Decision : Le constructeur
asCaptureId()applique.toLowerCase()automatiquement (INV-103-31). - Rationale : Garantit l'invariant de normalisation au point d'entree le plus bas. Impossible d'oublier la normalisation en aval.
- Alternatives : Normaliser uniquement dans l'orchestrateur (rejete : fragile, oubli possible dans d'autres modules).
- Trade-offs : Le constructeur a un effet de bord (mutation de casse). Acceptable car c'est contractuel et documentee.
6. Hypotheses¶
- Les regex de validation §5.1 sont prises telles quelles de la specification v3. Toute modification de la spec entrainera une mise a jour de ce fichier.
CanonicalPayloadest defini avec les cles en ordre alphabetique, conforme au contrat §5.12. Le calcul du fingerprint SHA-256 est dans le module M10 backend, pas dans ce fichier de types.CaptureIngestPayloadutilise desstring(pas des branded types) car c'est le DTO wire-format envoye au backend. Les branded types sont utilises en amont dans le code mobile.
7. Matrice de couverture invariants¶
| Invariant | Couvert par | Comment |
|---|---|---|
| INV-103-01-fidelity | CaptureOrchestratorResult | Type-safety du resultat final |
| INV-103-03-hash-local-first | Sha3_256Hash branded type | Distinction compile-time |
| INV-103-06-encryption-file-level | CaptureEncryptionResult | Structure typee du resultat crypto |
| INV-103-09-envelope-encryption | DekWrappedB64, KekId | Branded types pour artefacts crypto |
| INV-103-20..29 transitions | ALLOWED_TRANSITIONS table | Table exhaustive des transitions autorisees |
| INV-103-28-terminal-states | TERMINAL_CAPTURE_STATES, isTerminalCaptureState() | Detection programmatique |
| INV-103-30-key-exchange | DekWrappedB64, KekId, CaptureEncryptionResult | Types dedies wrapping |
| INV-103-31-capture-id-normalization | asCaptureId() avec .toLowerCase() | Normalisation au constructeur |
| INV-103-34-kek-keyring-rotation | KekId branded type | Tracabilite compile-time |
| INV-103-37-idempotency | CanonicalPayload | Structure normative des cles triees |
| INV-103-38-transition-deferred-cancelled | DEFAULT_DEFERRED_UPLOAD_TTL_S | Constante TTL contractuelle |
| INV-103-39-nonce-csprng | AesGcmNonceB64 | Type dedie pour le nonce |
8. Tests contractuels couverts¶
| Test ID | Element type concerne |
|---|---|
| TC-NOM-01 | CaptureIngestPayload (dek_wrapped_b64 + kek_id presents) |
| TC-NOM-12 | isTerminalCaptureState(), TERMINAL_CAPTURE_STATES |
| TC-NOM-15 | asCaptureId() normalisation lowercase |
| TC-ERR-06 | CAPTURE_VALIDATION_PATTERNS (chaque regex) |
| TC-INV-02 | ALLOWED_TRANSITIONS["CAPTURED"] |
| TC-INV-06 | CanonicalPayload (cles triees, OCR exclu) |
| TC-INV-10 | ALLOWED_TRANSITIONS["CANCELLED"], ALLOWED_TRANSITIONS["ANCHOR_CONFIRMED"] |
| TC-NEG-01..18 | CAPTURE_VALIDATION_PATTERNS, CAPTURE_ERROR_CODES |
9. Fichiers non modifies (hors perimetre)¶
Aucun fichier existant n'est modifie. Ce module cree un nouveau fichier src/capture/types.ts qui sera importe par les modules M1..M8.