openapi: 3.0.3 info: title: TR-069 Exchange Viewer API description: | API REST pour la récupération, la suppression et le streaming en temps réel des échanges CWMP/TR-069 capturés lors d'une session de trace sur un device. ## Architecture Les échanges SOAP sont capturés par le `CwmpLoggerMiddleware`, persistés dans MongoDB via `ExchangeStoreService`, et diffusés en temps réel via Server-Sent Events (SSE). ## Authentification Tous les endpoints nécessitent une session authentifiée (cookie-based via AdonisJS). ## Feature Flags | Variable d'environnement | Effet | |--------------------------|-------| | `FEATURE_EXCHANGE_PERSISTENCE=false` | Désactive l'écriture MongoDB | | `FEATURE_SSE_ENABLED=false` | Désactive le SSE (endpoint renvoie 503) | | `FEATURE_EXCHANGE_API_SOURCE=file` | Lecture depuis l'ancien système fichier | version: 1.0.0 contact: name: adam-acs Team servers: - url: http://localhost:8080/api/v1 description: Local development paths: /tr069-trace/{deviceId}/exchanges: get: operationId: getExchanges summary: Récupérer les échanges CWMP d'un device description: | Retourne les échanges CWMP structurés pour un device tracé, ordonnés par timestamp ascending. Inclut des métadonnées sur le statut de la trace et la fenêtre d'enregistrement. **Pagination cursor-based** : utiliser `after` pour charger les messages plus récents, `before` pour charger les messages plus anciens, ou `latest=true` pour obtenir les N messages les plus récents (chargement initial). tags: - Exchanges parameters: - $ref: '#/components/parameters/deviceId' - name: limit in: query required: false description: | Nombre maximum de messages retournés. - Défaut : 200 - Minimum : 1 - Maximum : 1000 - Retourne HTTP 400 si non-numérique ou < 1 schema: type: integer minimum: 1 maximum: 1000 default: 200 example: 50 - name: after in: query required: false description: | Timestamp ISO 8601 pour la pagination cursor-based (messages plus récents). Seuls les messages avec un timestamp strictement supérieur à cette valeur sont retournés. Retourne HTTP 400 si le format est invalide. schema: type: string format: date-time example: '2025-01-15T10:30:00.000Z' - name: before in: query required: false description: | Timestamp ISO 8601 pour la pagination inverse (messages plus anciens). Seuls les messages avec un timestamp strictement inférieur à cette valeur sont retournés. Retourne HTTP 400 si le format est invalide. schema: type: string format: date-time example: '2025-01-15T10:35:00.000Z' - name: latest in: query required: false description: | Si `true`, retourne les N messages les plus récents (pour le chargement initial). Combiné avec `limit` pour contrôler le nombre de messages. schema: type: string enum: - 'true' - 'false' example: 'true' responses: '200': description: Liste des échanges avec métadonnées content: application/json: schema: $ref: '#/components/schemas/ExchangeListResponse' examples: activeTrace: summary: Device actuellement tracé avec des échanges value: exchanges: - sequenceNumber: 1 timestamp: '2025-01-15T10:30:00.123Z' direction: 'CPE→ACS' cwmpMethod: 'Inform' sessionId: '10.0.0.1:54321->192.168.1.1:8080' rawXml: '...' - sequenceNumber: 2 timestamp: '2025-01-15T10:30:00.456Z' direction: 'ACS→CPE' cwmpMethod: 'InformResponse' sessionId: '10.0.0.1:54321->192.168.1.1:8080' rawXml: '1' metadata: traceStatus: 'ACTIVE' traceStoppedAt: null recordedFrom: '2025-01-15T10:30:00.123Z' recordedUntil: '2025-01-15T10:35:42.567Z' total: 42 stoppedTrace: summary: Trace arrêtée avec historique conservé value: exchanges: - sequenceNumber: 1 timestamp: '2025-01-15T10:30:00.123Z' direction: 'CPE→ACS' cwmpMethod: 'Inform' sessionId: '10.0.0.1:54321->192.168.1.1:8080' rawXml: '...' metadata: traceStatus: 'STOPPED' traceStoppedAt: '2025-01-15T10:40:00.000Z' recordedFrom: '2025-01-15T10:30:00.123Z' recordedUntil: '2025-01-15T10:35:42.567Z' total: 15 emptyTrace: summary: Device tracé mais aucun échange encore enregistré value: exchanges: [] metadata: traceStatus: 'ACTIVE' traceStoppedAt: null recordedFrom: null recordedUntil: null total: 0 truncatedResult: summary: Résultat partiel (timeout de lecture dépassé) value: exchanges: [] metadata: traceStatus: 'ACTIVE' traceStoppedAt: null recordedFrom: '2025-01-15T10:30:00.123Z' recordedUntil: '2025-01-15T10:35:42.567Z' total: 5000 isTruncated: true '400': description: Paramètre de requête invalide content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' examples: invalidLimit: summary: Paramètre limit invalide value: error: 'Invalid limit parameter. Must be an integer between 1 and 1000.' invalidAfter: summary: Paramètre after invalide value: error: 'Invalid after parameter. Must be a valid ISO 8601 timestamp.' invalidBefore: summary: Paramètre before invalide value: error: 'Invalid before parameter. Must be a valid ISO 8601 timestamp.' '404': description: Le device n'a jamais été tracé (aucun échange historique) content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: error: 'No trace exists for device F8B7FF-Livebox4-LB4-TEST-001' delete: operationId: deleteExchanges summary: Supprimer tous les échanges d'un device description: | Supprime tous les échanges CWMP stockés pour le device spécifié. Retourne 204 sans corps de réponse en cas de succès. Cette opération est irréversible. Si le device est actuellement tracé, les nouveaux échanges continueront d'être enregistrés après la suppression. tags: - Exchanges parameters: - $ref: '#/components/parameters/deviceId' responses: '204': description: Échanges supprimés avec succès (pas de corps de réponse) /tr069-trace/{deviceId}/exchanges/stream: get: operationId: streamExchanges summary: Flux SSE temps réel des échanges CWMP description: | Ouvre une connexion HTTP persistante avec `Content-Type: text/event-stream` qui pousse chaque nouvel échange CWMP dès qu'il est persisté. ## Protocole SSE La connexion utilise le protocole Server-Sent Events (SSE) standard : - **Content-Type** : `text/event-stream` - **Cache-Control** : `no-cache` - **Connection** : `keep-alive` - **X-Accel-Buffering** : `no` (pour nginx) ## Format des événements Chaque événement SSE contient : ``` id: data: ``` Exemple d'événement brut : ``` id: 42 data: {"sequenceNumber":42,"timestamp":"2025-01-15T10:30:00.123Z","direction":"CPE→ACS","cwmpMethod":"Inform","sessionId":"10.0.0.1:54321->192.168.1.1:8080","rawXml":"..."} ``` ## Reconnexion (Last-Event-ID) Lors d'une reconnexion, le client envoie l'en-tête `Last-Event-ID` avec le `sequenceNumber` du dernier événement reçu. Le serveur reprend alors à partir du message suivant (sequenceNumber > Last-Event-ID), sans duplication. Le serveur envoie un champ `retry` au début de la connexion (défaut: 3000ms) pour configurer l'intervalle de reconnexion automatique du client. ## Timeouts serveur Le serveur ferme la connexion dans les cas suivants : - **Inactivité** : aucun nouvel échange poussé pendant 5 minutes - **Durée absolue** : connexion ouverte depuis plus de 2 heures Dans les deux cas, le client devrait se reconnecter automatiquement (mécanisme natif EventSource avec Last-Event-ID). ## Feature Flag Si `FEATURE_SSE_ENABLED=false`, cet endpoint retourne HTTP 503. tags: - Exchanges - SSE parameters: - $ref: '#/components/parameters/deviceId' - name: Last-Event-ID in: header required: false description: | Sequence number du dernier événement reçu par le client. Utilisé pour reprendre le flux après une déconnexion sans duplication. schema: type: string example: '42' responses: '200': description: Flux SSE ouvert avec succès headers: Content-Type: schema: type: string example: text/event-stream Cache-Control: schema: type: string example: no-cache Connection: schema: type: string example: keep-alive X-Accel-Buffering: schema: type: string example: 'no' content: text/event-stream: schema: type: string description: Flux d'événements SSE (voir description pour le format) example: | retry: 3000 id: 1 data: {"sequenceNumber":1,"timestamp":"2025-01-15T10:30:00.123Z","direction":"CPE→ACS","cwmpMethod":"Inform","sessionId":"10.0.0.1:54321->192.168.1.1:8080","rawXml":"..."} id: 2 data: {"sequenceNumber":2,"timestamp":"2025-01-15T10:30:00.456Z","direction":"ACS→CPE","cwmpMethod":"InformResponse","sessionId":"10.0.0.1:54321->192.168.1.1:8080","rawXml":"..."} '404': description: Le device n'est pas actuellement tracé content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: error: 'Device is not being traced' '503': description: Le streaming SSE est désactivé (feature flag) content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: error: 'SSE streaming is currently disabled' components: parameters: deviceId: name: deviceId in: path required: true description: | Identifiant du device (format: `OUI-ProductClass-SerialNumber` ou hash MongoDB `_id`). schema: type: string maxLength: 128 example: 'F8B7FF-Livebox4-LB4-TEST-001' schemas: CwmpExchange: type: object description: Un échange SOAP CWMP individuel capturé required: - sequenceNumber - timestamp - direction - cwmpMethod - sessionId - rawXml properties: sequenceNumber: type: integer description: | Numéro de séquence monotoniquement croissant par device (commence à 1). Utilisé comme identifiant d'événement SSE pour la reconnexion. example: 42 timestamp: type: string format: date-time description: Horodatage UTC de la capture (ISO 8601) example: '2025-01-15T10:30:00.123Z' direction: type: string enum: - 'CPE→ACS' - 'ACS→CPE' description: | Direction du message : - `CPE→ACS` : requête envoyée par le CPE vers l'ACS - `ACS→CPE` : réponse envoyée par l'ACS vers le CPE example: 'CPE→ACS' cwmpMethod: type: string description: | Méthode CWMP extraite du body SOAP : - Nom de la méthode (ex: `Inform`, `GetParameterValues`, `SetParameterValuesResponse`) - `"Empty"` si le body XML est vide (empty POST du CPE) - `"Unknown"` si aucun élément `cwmp:` n'est trouvé dans le body example: 'Inform' sessionId: type: string description: Identifiant de session TCP (format `IP:port->IP:port`) example: '10.0.0.1:54321->192.168.1.1:8080' rawXml: type: string description: | Contenu XML SOAP brut du message (maximum 2 Mo). Peut être vide pour les messages de type "Empty". maxLength: 2097152 example: '...' ExchangeMetadata: type: object description: Métadonnées sur la session de trace du device required: - traceStatus - traceStoppedAt - recordedFrom - recordedUntil - total properties: traceStatus: type: string enum: - ACTIVE - STOPPED description: | Statut actuel de la trace : - `ACTIVE` : le device est en cours de trace - `STOPPED` : la trace a été arrêtée (historique conservé) example: 'ACTIVE' traceStoppedAt: type: string format: date-time nullable: true description: Timestamp d'arrêt de la trace (null si active) example: null recordedFrom: type: string format: date-time nullable: true description: Timestamp du plus ancien échange enregistré example: '2025-01-15T10:30:00.123Z' recordedUntil: type: string format: date-time nullable: true description: Timestamp du plus récent échange enregistré example: '2025-01-15T10:35:42.567Z' total: type: integer description: Nombre total d'échanges stockés pour ce device example: 42 isTruncated: type: boolean description: | Présent uniquement si le résultat est partiel (timeout de lecture dépassé). Le client devrait réessayer avec des paramètres de pagination plus restreints. example: true ExchangeListResponse: type: object description: Réponse de l'endpoint GET /exchanges required: - exchanges - metadata properties: exchanges: type: array items: $ref: '#/components/schemas/CwmpExchange' description: Liste des échanges ordonnés par timestamp ascending metadata: $ref: '#/components/schemas/ExchangeMetadata' ErrorResponse: type: object description: Réponse d'erreur standard required: - error properties: error: type: string description: Message d'erreur décrivant le problème example: 'Invalid limit parameter. Must be an integer between 1 and 1000.' tags: - name: Exchanges description: Gestion des échanges CWMP capturés - name: SSE description: Streaming temps réel via Server-Sent Events