Aller au contenu

PD-103 — Agent Developer — Module M13 : capture-migration

1. Identite agent

  • Agent : agent-developer (Agent B — Claude)
  • Story : PD-103
  • Module : M13 — capture-migration
  • Wave : 1 (fondation, sans dependance inter-agents)
  • Date : 2026-04-03

2. Resume

Module M13 implemente les migrations DDL pour la capture probatoire d'ecran. Deux tables sont creees dans le schema vault_secure : - capture_events — table mutable portant l'etat courant de chaque capture (FSM 8 etats) - capture_audit_log — journal probatoire append-only (INSERT uniquement, UPDATE/DELETE interdits par trigger)

Fichiers crees : - src/database/migrations/1742400000000-PD103-CreateCaptureEvents.ts — Table principale + enum + index + RLS - src/database/migrations/1742400000001-PD103-CreateCaptureAuditLog.ts — Journal append-only + trigger immutabilite

Aucun fichier existant modifie.

3. Artefacts livres

Fichier Role Lignes
src/database/migrations/1742400000000-PD103-CreateCaptureEvents.ts Table capture_events, enum, index, RLS, trigger updated_at ~250
src/database/migrations/1742400000001-PD103-CreateCaptureAuditLog.ts Table capture_audit_log, trigger immutabilite, index ~120

4. Architecture

4.1 Pattern utilise

Pattern identique aux migrations existantes du projet (PD-272, PD-80, PD-255) : - MigrationInterface de TypeORM 0.3.x - Schema vault_secure pour les tables applicatives - Row-Level Security (RLS) avec politique user_id - gen_random_uuid() pour les cles primaires (extension pgcrypto deja activee) - down() vide en convention production (PD-79) - Commentaires SQL pour documentation inline

4.2 Table capture_events

Table mutable portant l'etat courant de chaque capture dans la FSM a 8 etats.

Schema

CREATE TABLE vault_secure.capture_events (
  id                              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  capture_id                      UUID NOT NULL,
  user_id                         UUID NOT NULL,

  -- FSM state (INV-103-28: close-world 8 states)
  state                           vault_secure.capture_state_enum NOT NULL DEFAULT 'CAPTURED',
  signature_status                VARCHAR(20) NOT NULL DEFAULT 'PENDING_SIGNATURE',
  hsm_signature_ref               VARCHAR(256),

  -- Crypto (INV-103-09, INV-103-30)
  dek_wrapped                     BYTEA NOT NULL,
  kek_id                          VARCHAR(64) NOT NULL,
  aes_gcm_nonce_b64               VARCHAR(16) NOT NULL,
  aes_gcm_tag_b64                 VARCHAR(24) NOT NULL,

  -- Content
  hash_sha3_256                   VARCHAR(64) NOT NULL,
  mime_type                       VARCHAR(10) NOT NULL DEFAULT 'image/png',
  size_bytes                      BIGINT NOT NULL,

  -- Metadata
  device_id                       UUID NOT NULL,
  app_version                     VARCHAR(32) NOT NULL,
  timestamp_device                TIMESTAMPTZ NOT NULL,

  -- OCR (optionnel)
  ocr_text                        TEXT,
  ocr_confidence                  NUMERIC(3,2),
  ocr_language                    VARCHAR(35),

  -- Idempotence (INV-103-37)
  payload_canonical_sha256        VARCHAR(64) NOT NULL,

  -- Upload S3
  upload_object_key               TEXT,

  -- Sealing references
  merkle_proof_ref                TEXT,
  tsa_token_ref                   TEXT,
  tx_hash                         VARCHAR(66),

  -- SEAL_DELAYED (INV-103-35, INV-103-36)
  seal_delayed                    BOOLEAN NOT NULL DEFAULT FALSE,
  seal_delayed_conforming_cycles  INTEGER NOT NULL DEFAULT 0,

  -- Timestamps
  created_at                      TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  updated_at                      TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

Enum

CREATE TYPE vault_secure.capture_state_enum AS ENUM (
  'CAPTURED',
  'UPLOADING',
  'UPLOAD_DEFERRED',
  'UPLOADED',
  'PENDING_SEAL',
  'SEALED',
  'ANCHOR_CONFIRMED',
  'CANCELLED'
);

Contraintes

Contrainte Type Invariant
uq_capture_events_capture_id UNIQUE capture_id unique globalement
chk_signature_status CHECK IN ('PENDING_SIGNATURE', 'SIGNED')
chk_hash_sha3_256 CHECK ~ '^[a-f0-9]{64}$'
chk_payload_canonical_sha256 CHECK ~ '^[a-f0-9]{64}$'
chk_size_bytes CHECK >= 1 AND <= 524288000
chk_mime_type CHECK = 'image/png'
chk_ocr_confidence CHECK IS NULL OR (>= 0 AND <= 1)
chk_seal_delayed_cycles CHECK >= 0 AND <= 10
chk_kek_id CHECK ~ '^[A-Za-z0-9._-]{1,64}$'

Index

Index Colonnes Condition Justification
idx_capture_events_state state Requetes FSM (reconciliation, listing)
idx_capture_events_user_id user_id Filtrage par utilisateur + RLS
idx_capture_events_capture_id capture_id Lookup par capture_id (deja UNIQUE mais explicite)
idx_capture_events_reconciliation created_at WHERE state NOT IN ('CANCELLED', 'ANCHOR_CONFIRMED') Scan non-terminaux (M12 reconciliation)
idx_capture_events_seal_delayed seal_delayed, state WHERE seal_delayed = TRUE Requetes SEAL_DELAYED (M12 clearing)
idx_capture_events_idempotence capture_id, payload_canonical_sha256 Lookup rapide idempotence (M10)

RLS

ALTER TABLE vault_secure.capture_events ENABLE ROW LEVEL SECURITY;

CREATE POLICY capture_events_user_policy
ON vault_secure.capture_events
FOR ALL
USING (user_id = current_setting('app.current_user_id')::uuid);

Trigger updated_at

Reutilisation de la fonction existante vault_secure.update_timestamp() :

CREATE TRIGGER trg_capture_events_updated_at
BEFORE UPDATE ON vault_secure.capture_events
FOR EACH ROW
EXECUTE FUNCTION vault_secure.update_timestamp();

4.3 Table capture_audit_log

Journal probatoire append-only. Chaque transition d'etat, chaque action significative est loguee ici. Les UPDATE et DELETE sont interdits par trigger (pattern PD-272).

Schema

CREATE TABLE vault_secure.capture_audit_log (
  id                UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  capture_id        UUID NOT NULL,
  event_type        VARCHAR(64) NOT NULL,
  payload_json      JSONB NOT NULL DEFAULT '{}',
  created_at        TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

Contraintes

Contrainte Type Invariant
chk_event_type_not_empty CHECK event_type <> ''

Index

Index Colonnes Justification
idx_capture_audit_log_capture_id capture_id Historique par capture
idx_capture_audit_log_created_at created_at Requetes temporelles (audit, compliance)

Trigger immutabilite

CREATE OR REPLACE FUNCTION vault_secure.capture_audit_log_immutable()
RETURNS TRIGGER AS $$
BEGIN
  IF TG_OP = 'UPDATE' THEN
    RAISE EXCEPTION 'capture_audit_log: modification interdite — journal immutable'
      USING ERRCODE = 'restrict_violation';
  ELSIF TG_OP = 'DELETE' THEN
    RAISE EXCEPTION 'capture_audit_log: suppression interdite — journal immutable'
      USING ERRCODE = 'restrict_violation';
  END IF;
  RETURN NULL;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_capture_audit_log_immutable
BEFORE UPDATE OR DELETE ON vault_secure.capture_audit_log
FOR EACH ROW
EXECUTE FUNCTION vault_secure.capture_audit_log_immutable();

4.4 Diagramme ER

erDiagram
    capture_events {
        UUID id PK
        UUID capture_id UK
        UUID user_id
        capture_state_enum state
        VARCHAR signature_status
        VARCHAR hsm_signature_ref
        BYTEA dek_wrapped
        VARCHAR kek_id
        VARCHAR aes_gcm_nonce_b64
        VARCHAR aes_gcm_tag_b64
        VARCHAR hash_sha3_256
        VARCHAR mime_type
        BIGINT size_bytes
        UUID device_id
        VARCHAR app_version
        TIMESTAMPTZ timestamp_device
        TEXT ocr_text
        NUMERIC ocr_confidence
        VARCHAR ocr_language
        VARCHAR payload_canonical_sha256
        TEXT upload_object_key
        TEXT merkle_proof_ref
        TEXT tsa_token_ref
        VARCHAR tx_hash
        BOOLEAN seal_delayed
        INTEGER seal_delayed_conforming_cycles
        TIMESTAMPTZ created_at
        TIMESTAMPTZ updated_at
    }

    capture_audit_log {
        UUID id PK
        UUID capture_id
        VARCHAR event_type
        JSONB payload_json
        TIMESTAMPTZ created_at
    }

    capture_events ||--o{ capture_audit_log : "capture_id"

5. Code des migrations

5.1 Migration 1 — 1742400000000-PD103-CreateCaptureEvents.ts

import { MigrationInterface, QueryRunner } from 'typeorm';

/**
 * PD-103: Create capture_events table
 *
 * Table mutable portant l'etat courant de chaque capture probatoire d'ecran.
 * FSM monotone 8 etats (INV-103-28), close-world enum.
 *
 * Invariants couverts:
 *   - INV-103-08: garde sealed via CHECK signature_status
 *   - INV-103-09: dek_wrapped BYTEA, jamais clair en base
 *   - INV-103-28: capture_state_enum 8 valeurs
 *   - INV-103-31: capture_id normalisé lowercase (applicatif)
 *   - INV-103-34: kek_id obligatoire pour rotation keyring
 *   - INV-103-35/36: seal_delayed + compteur cycles conformes
 *   - INV-103-37: payload_canonical_sha256 pour idempotence
 *
 * @see PD-103-specification.md §5.1, §5.7, §5.9
 */
export class PD103CreateCaptureEvents1742400000000 implements MigrationInterface {
  name = 'PD103CreateCaptureEvents1742400000000';

  public async up(queryRunner: QueryRunner): Promise<void> {
    // Create enum type for capture state (8 states, close-world)
    await queryRunner.query(`
      CREATE TYPE vault_secure.capture_state_enum AS ENUM (
        'CAPTURED',
        'UPLOADING',
        'UPLOAD_DEFERRED',
        'UPLOADED',
        'PENDING_SEAL',
        'SEALED',
        'ANCHOR_CONFIRMED',
        'CANCELLED'
      );
    `);

    // Create capture_events table
    await queryRunner.query(`
      CREATE TABLE vault_secure.capture_events (
        id                              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
        capture_id                      UUID NOT NULL,
        user_id                         UUID NOT NULL,

        -- FSM state
        state                           vault_secure.capture_state_enum NOT NULL DEFAULT 'CAPTURED',
        signature_status                VARCHAR(20) NOT NULL DEFAULT 'PENDING_SIGNATURE',
        hsm_signature_ref               VARCHAR(256),

        -- Crypto envelope (INV-103-09, INV-103-30)
        dek_wrapped                     BYTEA NOT NULL,
        kek_id                          VARCHAR(64) NOT NULL,
        aes_gcm_nonce_b64               VARCHAR(16) NOT NULL,
        aes_gcm_tag_b64                 VARCHAR(24) NOT NULL,

        -- Content identity
        hash_sha3_256                   VARCHAR(64) NOT NULL,
        mime_type                       VARCHAR(10) NOT NULL DEFAULT 'image/png',
        size_bytes                      BIGINT NOT NULL,

        -- Device metadata
        device_id                       UUID NOT NULL,
        app_version                     VARCHAR(32) NOT NULL,
        timestamp_device                TIMESTAMPTZ NOT NULL,

        -- OCR (optional, INV-103-04/05)
        ocr_text                        TEXT,
        ocr_confidence                  NUMERIC(3,2),
        ocr_language                    VARCHAR(35),

        -- Idempotence fingerprint (INV-103-37)
        payload_canonical_sha256        VARCHAR(64) NOT NULL,

        -- Upload S3 reference
        upload_object_key               TEXT,

        -- Sealing references (populated post-SEALED)
        merkle_proof_ref                TEXT,
        tsa_token_ref                   TEXT,
        tx_hash                         VARCHAR(66),

        -- SEAL_DELAYED monitoring (INV-103-35, INV-103-36)
        seal_delayed                    BOOLEAN NOT NULL DEFAULT FALSE,
        seal_delayed_conforming_cycles  INTEGER NOT NULL DEFAULT 0,

        -- Timestamps
        created_at                      TIMESTAMPTZ NOT NULL DEFAULT NOW(),
        updated_at                      TIMESTAMPTZ NOT NULL DEFAULT NOW(),

        -- Constraints
        CONSTRAINT uq_capture_events_capture_id UNIQUE (capture_id),
        CONSTRAINT chk_signature_status CHECK (signature_status IN ('PENDING_SIGNATURE', 'SIGNED')),
        CONSTRAINT chk_hash_sha3_256 CHECK (hash_sha3_256 ~ '^[a-f0-9]{64}$'),
        CONSTRAINT chk_payload_canonical_sha256 CHECK (payload_canonical_sha256 ~ '^[a-f0-9]{64}$'),
        CONSTRAINT chk_size_bytes CHECK (size_bytes >= 1 AND size_bytes <= 524288000),
        CONSTRAINT chk_mime_type CHECK (mime_type = 'image/png'),
        CONSTRAINT chk_ocr_confidence CHECK (ocr_confidence IS NULL OR (ocr_confidence >= 0 AND ocr_confidence <= 1)),
        CONSTRAINT chk_seal_delayed_cycles CHECK (seal_delayed_conforming_cycles >= 0 AND seal_delayed_conforming_cycles <= 10),
        CONSTRAINT chk_kek_id CHECK (kek_id ~ '^[A-Za-z0-9._-]{1,64}$')
      );
    `);

    // Index: state for FSM queries and reconciliation
    await queryRunner.query(`
      CREATE INDEX idx_capture_events_state
      ON vault_secure.capture_events (state);
    `);

    // Index: user_id for user queries + RLS
    await queryRunner.query(`
      CREATE INDEX idx_capture_events_user_id
      ON vault_secure.capture_events (user_id);
    `);

    // Partial index: non-terminal states for reconciliation (M12)
    await queryRunner.query(`
      CREATE INDEX idx_capture_events_reconciliation
      ON vault_secure.capture_events (created_at)
      WHERE state NOT IN ('CANCELLED', 'ANCHOR_CONFIRMED');
    `);

    // Partial index: SEAL_DELAYED active captures
    await queryRunner.query(`
      CREATE INDEX idx_capture_events_seal_delayed
      ON vault_secure.capture_events (seal_delayed, state)
      WHERE seal_delayed = TRUE;
    `);

    // Composite index: idempotence lookup (M10)
    await queryRunner.query(`
      CREATE INDEX idx_capture_events_idempotence
      ON vault_secure.capture_events (capture_id, payload_canonical_sha256);
    `);

    // Enable RLS
    await queryRunner.query(`
      ALTER TABLE vault_secure.capture_events ENABLE ROW LEVEL SECURITY;
    `);

    // RLS policy: users can only access their own captures
    await queryRunner.query(`
      CREATE POLICY capture_events_user_policy
      ON vault_secure.capture_events
      FOR ALL
      USING (user_id = current_setting('app.current_user_id')::uuid);
    `);

    // Trigger: auto-update updated_at (reuse existing function)
    await queryRunner.query(`
      CREATE TRIGGER trg_capture_events_updated_at
      BEFORE UPDATE ON vault_secure.capture_events
      FOR EACH ROW
      EXECUTE FUNCTION vault_secure.update_timestamp();
    `);

    // Comments
    await queryRunner.query(`
      COMMENT ON TABLE vault_secure.capture_events IS
      'Capture probatoire d''ecran — etat courant FSM 8 etats (PD-103). INV-103-28: close-world state machine.';
    `);

    await queryRunner.query(`
      COMMENT ON COLUMN vault_secure.capture_events.dek_wrapped IS
      'DEK wrappe RSA-OAEP-SHA256 (INV-103-09). JAMAIS stocke en clair.';
    `);

    await queryRunner.query(`
      COMMENT ON COLUMN vault_secure.capture_events.payload_canonical_sha256 IS
      'Fingerprint SHA-256 du payload canonique pour idempotence (INV-103-37). Champs OCR exclus.';
    `);

    await queryRunner.query(`
      COMMENT ON COLUMN vault_secure.capture_events.seal_delayed IS
      'Flag SEAL_DELAYED (INV-103-35). Positionne par reconciliation si PENDING_SEAL > sealSla.';
    `);

    await queryRunner.query(`
      COMMENT ON COLUMN vault_secure.capture_events.seal_delayed_conforming_cycles IS
      'Compteur cycles conformes consecutifs pour clearing SEAL_DELAYED (INV-103-36). Reset si cycle non conforme.';
    `);
  }

  /**
   * down() vide — convention production PD-79.
   * En production, les migrations destructives ne sont jamais executees automatiquement.
   */
  public async down(_queryRunner: QueryRunner): Promise<void> {
    // Convention PD-79: down() is intentionally empty for production safety.
    // Manual rollback procedure:
    //   DROP TABLE vault_secure.capture_events;
    //   DROP TYPE vault_secure.capture_state_enum;
  }
}

5.2 Migration 2 — 1742400000001-PD103-CreateCaptureAuditLog.ts

import { MigrationInterface, QueryRunner } from 'typeorm';

/**
 * PD-103: Create capture_audit_log table (journal probatoire append-only)
 *
 * Event store INSERT-ONLY pour la tracabilite complete des captures probatoires.
 * Les UPDATE et DELETE sont interdits par trigger (pattern PD-272).
 *
 * Invariants couverts:
 *   - INV-103-25: chaque transition d'etat est loguee
 *   - INV-103-10: journal append-only, idempotent
 *   - Plan §11bis: separation capture_events (mutable) / capture_audit_log (immutable)
 *
 * @see PD-103-specification.md §5.6, §5.8
 */
export class PD103CreateCaptureAuditLog1742400000001 implements MigrationInterface {
  name = 'PD103CreateCaptureAuditLog1742400000001';

  public async up(queryRunner: QueryRunner): Promise<void> {
    // Create capture_audit_log table (append-only)
    await queryRunner.query(`
      CREATE TABLE vault_secure.capture_audit_log (
        id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
        capture_id      UUID NOT NULL,
        event_type      VARCHAR(64) NOT NULL,
        payload_json    JSONB NOT NULL DEFAULT '{}',
        created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW(),

        -- Constraints
        CONSTRAINT chk_event_type_not_empty CHECK (event_type <> '')
      );
    `);

    // Index: lookup by capture_id for audit trail
    await queryRunner.query(`
      CREATE INDEX idx_capture_audit_log_capture_id
      ON vault_secure.capture_audit_log (capture_id);
    `);

    // Index: temporal queries for compliance/audit
    await queryRunner.query(`
      CREATE INDEX idx_capture_audit_log_created_at
      ON vault_secure.capture_audit_log (created_at);
    `);

    // Immutability function (pattern PD-272)
    await queryRunner.query(`
      CREATE OR REPLACE FUNCTION vault_secure.capture_audit_log_immutable()
      RETURNS TRIGGER AS $$
      BEGIN
        IF TG_OP = 'UPDATE' THEN
          RAISE EXCEPTION 'capture_audit_log: modification interdite — journal immutable'
            USING ERRCODE = 'restrict_violation';
        ELSIF TG_OP = 'DELETE' THEN
          RAISE EXCEPTION 'capture_audit_log: suppression interdite — journal immutable'
            USING ERRCODE = 'restrict_violation';
        END IF;
        RETURN NULL;
      END;
      $$ LANGUAGE plpgsql;
    `);

    // Immutability trigger
    await queryRunner.query(`
      CREATE TRIGGER trg_capture_audit_log_immutable
      BEFORE UPDATE OR DELETE ON vault_secure.capture_audit_log
      FOR EACH ROW
      EXECUTE FUNCTION vault_secure.capture_audit_log_immutable();
    `);

    // Comments
    await queryRunner.query(`
      COMMENT ON TABLE vault_secure.capture_audit_log IS
      'Journal probatoire append-only pour captures d''ecran (PD-103). UPDATE/DELETE interdits par trigger. INV-103-25.';
    `);

    await queryRunner.query(`
      COMMENT ON COLUMN vault_secure.capture_audit_log.event_type IS
      'Type d''evenement: STATE_TRANSITION, PURGE, UPLOAD_RETRY, SEAL_DELAYED_SET, SEAL_DELAYED_CLEARED, ORPHAN_GC, etc.';
    `);
  }

  /**
   * down() vide — convention production PD-79.
   */
  public async down(_queryRunner: QueryRunner): Promise<void> {
    // Convention PD-79: down() is intentionally empty for production safety.
    // Manual rollback procedure:
    //   DROP TRIGGER trg_capture_audit_log_immutable ON vault_secure.capture_audit_log;
    //   DROP FUNCTION vault_secure.capture_audit_log_immutable();
    //   DROP TABLE vault_secure.capture_audit_log;
  }
}

6. Couverture de tests

6.1 Strategie

Les migrations DDL ne sont pas testables en Jest unitaire (pas de base PostgreSQL). Les tests d'integration des migrations sont couverts par : - Execution migration:run en CI avec base PostgreSQL Docker - Schema validator existant (npm run migration:validate) - Tests d'integration M14 (capture-tests-backend) qui valident le schema post-migration

6.2 Matrice de couverture TC-* -> mecanismes DDL

Test-ID Mecanisme DDL Couverture
TC-NOM-07 Table capture_events existe, colonnes signature_status, state M14 integration
TC-NOM-15 Index idx_capture_events_idempotence + colonne payload_canonical_sha256 M14 integration
TC-INV-03 Colonne dek_wrapped BYTEA NOT NULL, pas de colonne DEK clair Audit schema
TC-INV-05 pg_advisory_xact_lock sur capture_id (M10, pas DDL mais utilise UNIQUE) M14 integration
TC-INV-08 Colonnes seal_delayed, seal_delayed_conforming_cycles M14 integration
TC-INV-11 GC orphelins S3 utilise scan capture_events par upload_object_key M14 integration
TC-INV-14 Colonne seal_delayed + state avec index partiel M14 integration
TC-ERR-09 Colonne seal_delayed positionnable par reconciliation M14 integration

6.3 Validation structurelle attendue

Les assertions suivantes doivent etre verifiees par migration:validate ou M14 :

  1. vault_secure.capture_events existe avec toutes les colonnes specifiees
  2. vault_secure.capture_audit_log existe avec trigger immutabilite
  3. Enum vault_secure.capture_state_enum contient exactement 8 valeurs
  4. CHECK chk_signature_status n'autorise que PENDING_SIGNATURE et SIGNED
  5. CHECK chk_hash_sha3_256 valide le format hex lowercase 64 chars
  6. CHECK chk_payload_canonical_sha256 valide le format hex lowercase 64 chars
  7. UNIQUE uq_capture_events_capture_id empeche les doublons
  8. RLS active sur capture_events avec politique user_id
  9. Trigger trg_capture_audit_log_immutable bloque UPDATE et DELETE
  10. Index partiel idx_capture_events_reconciliation filtre les etats terminaux

7. Decisions architecturales

7.1 VARCHAR vs PostgreSQL ENUM pour signature_status

  • Decision : VARCHAR(20) avec CHECK constraint au lieu de CREATE TYPE ... AS ENUM.
  • Rationale : signature_status n'a que 2 valeurs (PENDING_SIGNATURE, SIGNED) et risque d'evoluer. Les CHECK constraints sont plus simples a modifier que les enums PostgreSQL (qui necessitent ALTER TYPE ADD VALUE). Convention observee dans PD-282, PD-279.
  • Alternatives considerees : CREATE TYPE signature_status_enum (plus strict mais moins flexible), BOOLEAN is_signed (perd la semantique).
  • Trade-offs : Moins de type-safety au niveau DB, mais plus de flexibilite pour les evolutions futures.

7.2 BYTEA vs TEXT pour dek_wrapped

  • Decision : BYTEA pour le DEK wrappe au lieu de TEXT (base64).
  • Rationale : Le DEK wrappe est un artefact binaire. Le stocker en BYTEA evite l'encodage/decodage base64 cote backend et reduit la taille de stockage (~33% plus compact). Convention observee dans PD-80 (wrapped_dek BYTEA).
  • Alternatives considerees : TEXT avec stockage base64 (plus simple cote API mais plus volumineux), VARCHAR(4096) (taille fixe arbitraire).
  • Trade-offs : Le backend doit convertir dek_wrapped_b64 (base64 du POST) en BYTEA a l'insertion. Conversion triviale en TypeORM.

8. Hypotheses

ID Hypothese Impact si faux
HYP-M13-01 La fonction vault_secure.update_timestamp() existe deja (creee par migration anterieure) Si absente, creer la fonction dans la migration M13 avant le trigger
HYP-M13-02 L'extension pgcrypto est deja activee (migration 1705000000000-EnablePgcryptoExtension.ts) Si absente, gen_random_uuid() echouera
HYP-M13-03 Le schema vault_secure existe deja Si absent, creer le schema dans une migration prealable
HYP-M13-04 Le timestamp 1742400000000 est posterieur a la derniere migration existante (1742300000000) Si conflit de timestamp, ajuster le prefixe

9. Dependances

Dependance Direction Module
Migration 1705000000000-EnablePgcryptoExtension.ts Prerequis Extension pgcrypto (gen_random_uuid)
Migration existante update_timestamp() Prerequis Fonction trigger existante
M9 capture-ingest Consommateur M9 inserera dans capture_events
M10 capture-idempotence Consommateur M10 interrogera payload_canonical_sha256
M11 capture-kek-keyring Consommateur M11 lira/ecrira dek_wrapped, kek_id
M12 capture-reconciliation Consommateur M12 scannera via index partiels
M14 capture-tests-backend Consommateur M14 testera le schema post-migration

10. Invariants couverts

Invariant Couverture DDL Mecanisme
INV-103-08-sealed-guard CHECK chk_signature_status Seules valeurs PENDING_SIGNATURE, SIGNED autorisees
INV-103-09-envelope-encryption Colonne dek_wrapped BYTEA NOT NULL Pas de colonne DEK clair — wrapping obligatoire
INV-103-25-transition-uploaded-pending-seal Table capture_audit_log append-only Journal probatoire avec trigger immutabilite
INV-103-28-terminal-states Enum capture_state_enum 8 valeurs Close-world au niveau DB (applicatif pour transitions)
INV-103-31-capture-id-normalization capture_id UUID type natif UUID PostgreSQL est case-insensitive par nature
INV-103-34-kek-keyring-rotation Colonne kek_id VARCHAR(64) NOT NULL + CHECK kek_id obligatoire avec format valide
INV-103-35-seal-delayed-trigger Colonne seal_delayed BOOLEAN Support DDL pour le flag
INV-103-36-seal-delayed-clearing Colonne seal_delayed_conforming_cycles INTEGER + CHECK <= 10 Compteur avec borne max
INV-103-37-idempotency-canonical-fingerprint Colonne payload_canonical_sha256 VARCHAR(64) NOT NULL + CHECK + index Fingerprint stocke et indexe pour comparaison rapide

11. Limites connues

  1. Pas de FK vers users : La table capture_events n'a pas de FOREIGN KEY vers vault_secure.users car la table users peut vivre dans un schema/base differente selon la topologie de deploiement. L'integrite referentielle est assuree applicativement par le guard JWT.

  2. Pas de FK capture_audit_log.capture_idcapture_events.capture_id : Le journal append-only ne doit pas etre bloque par des operations sur capture_events. L'integrite est assuree applicativement par M9.

  3. down() vide : Convention production PD-79. Les rollbacks destructifs ne sont jamais executes automatiquement. Un commentaire documente la procedure manuelle.

  4. Pas de partition : La table capture_events n'est pas partitionnee. Si le volume depasse le million de lignes, envisager un partitionnement par created_at (range monthly).

12. Checklist qualite

  • Convention de nommage timestamp conforme (posterieur a la derniere migration existante)
  • Schema vault_secure utilise (convention projet)
  • RLS active sur la table mutable
  • Trigger immutabilite sur le journal append-only (pattern PD-272)
  • down() vide avec documentation rollback (convention PD-79)
  • Commentaires SQL sur table et colonnes sensibles
  • Index partiels pour la reconciliation (non-terminaux)
  • CHECK constraints alignes avec les formats §5.1 de la specification
  • Aucune modification de fichier existant
  • Aucune deviation par rapport a la specification
  • Aucune fonctionnalite non prevue ajoutee
  • Patterns existants (PD-80, PD-272, PD-255) respectes