openapi: 3.1.0
info:
  title: Pneumags Address Service
  version: 1.0.0
  description: >-
    API privée d'autocomplétion et de normalisation d'adresses canadiennes.
    Les clients n'appellent jamais Google Places directement.
  contact:
    name: MartinLe5 Technologies Inc.
servers:
  - url: https://address-api.pneumags.com
    description: Production
security: []
tags:
  - name: Public
  - name: Addresses
  - name: M5X
paths:
  /api/v1/health:
    get:
      tags: [Public]
      summary: Vérifie que le processus API répond
      operationId: getHealth
      responses:
        "200":
          description: Service disponible
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthResponse"
  /api/v1/version:
    get:
      tags: [Public]
      summary: Retourne la version publique
      operationId: getVersion
      responses:
        "200":
          description: Version publique
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VersionResponse"
  /api/v1/addresses/autocomplete:
    options:
      tags: [Addresses]
      summary: Prévalidation CORS
      operationId: optionsAutocomplete
      responses:
        "204":
          description: Origine autorisée
        "403":
          $ref: "#/components/responses/Forbidden"
    post:
      tags: [Addresses]
      summary: Suggère au plus cinq adresses canadiennes
      operationId: autocompleteCanadianAddress
      security:
        - HmacClientId: []
          HmacTimestamp: []
          HmacNonce: []
          HmacSignature: []
        - M5xBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AutocompleteRequest"
            examples:
              saintJerome:
                value:
                  input: 2305 rue isa
                  sessionToken: 123e4567-e89b-42d3-a456-426614174000
      responses:
        "200":
          description: Suggestions normalisées
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AutocompleteResponse"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/ReplayDetected"
        "429":
          $ref: "#/components/responses/RateLimited"
        "502":
          $ref: "#/components/responses/ProviderInvalid"
        "503":
          $ref: "#/components/responses/ProviderUnavailable"
  /api/v1/addresses/details:
    options:
      tags: [Addresses]
      summary: Prévalidation CORS
      operationId: optionsDetails
      responses:
        "204":
          description: Origine autorisée
        "403":
          $ref: "#/components/responses/Forbidden"
    post:
      tags: [Addresses]
      summary: Termine la session et retourne une adresse canadienne structurée
      operationId: getCanadianAddressDetails
      security:
        - HmacClientId: []
          HmacTimestamp: []
          HmacNonce: []
          HmacSignature: []
        - M5xBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DetailsRequest"
            examples:
              selectedPlace:
                value:
                  placeId: ChIJ_example
                  sessionToken: 123e4567-e89b-42d3-a456-426614174000
      responses:
        "200":
          description: Adresse structurée
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DetailsResponse"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: Origine refusée ou pays non autorisé
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                country:
                  value:
                    success: false
                    error:
                      code: COUNTRY_NOT_ALLOWED
                      message: Seules les adresses canadiennes sont autorisées.
        "409":
          $ref: "#/components/responses/ReplayDetected"
        "429":
          $ref: "#/components/responses/RateLimited"
        "502":
          $ref: "#/components/responses/ProviderInvalid"
        "503":
          $ref: "#/components/responses/ProviderUnavailable"
  /api/v1/m5x/installations/token:
    post:
      tags: [M5X]
      summary: Émet un jeton temporaire pour une installation M5X Facturation
      description: >-
        Relais d'émission réservé à M5X Facturation. Le poste présente sa clé
        d'installation, dont seule l'empreinte SHA-256 est conservée par le
        service. La clé maître ADDRESS_API_MASTER_KEY n'est jamais utilisée par
        cette route. Installation inconnue, clé fausse et installation révoquée
        renvoient une réponse identique.
      operationId: issueM5xInstallationToken
      security:
        - M5xInstallationKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/InstallationTokenRequest"
            examples:
              defaut:
                value:
                  installationId: m5x-9f2c4a7b1d6e0835ac91b47f
      responses:
        "200":
          description: Jeton temporaire émis
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InstallationTokenResponse"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ProviderUnavailable"
  /api/v1/m5x/token/revoke:
    post:
      tags: [M5X]
      summary: Révoque un jeton temporaire avant son expiration
      description: >-
        Le porteur révoque son propre jeton, sans corps de requête. Détenir un
        jeton valide constitue l'authentification. L'opération est idempotente
        et ne touche jamais au secret global de signature.
      operationId: revokeM5xToken
      security:
        - M5xBearer: []
      responses:
        "204":
          description: Jeton révoqué ou déjà révoqué
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "503":
          $ref: "#/components/responses/ProviderUnavailable"
components:
  securitySchemes:
    HmacClientId:
      type: apiKey
      in: header
      name: X-Client-ID
      description: Identifiant fixe du client serveur Pneumags.
    HmacTimestamp:
      type: apiKey
      in: header
      name: X-Timestamp
      description: Timestamp Unix en secondes.
    HmacNonce:
      type: apiKey
      in: header
      name: X-Nonce
      description: Valeur aléatoire unique de 16 à 128 caractères.
    HmacSignature:
      type: apiKey
      in: header
      name: X-Signature
      description: HMAC SHA-256 hexadécimal de la chaîne canonique.
    M5xInstallationKey:
      type: apiKey
      in: header
      name: X-M5X-Installation-Key
      description: >-
        Clé propre à une installation M5X. Le service n'en conserve que
        l'empreinte SHA-256.
    M5xBearer:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Jeton HS256 temporaire, signé et révocable côté serveur.
  schemas:
    HealthResponse:
      type: object
      additionalProperties: false
      required: [success, service, status, provider, version, timestamp]
      properties:
        success: { type: boolean, const: true }
        service: { type: string, const: pneumags-address-api }
        status: { type: string, const: healthy }
        provider: { type: string, const: google-places }
        version: { type: string, examples: [v1] }
        timestamp: { type: string, format: date-time }
    VersionResponse:
      type: object
      additionalProperties: false
      required: [success, service, apiVersion, build, environment]
      properties:
        success: { type: boolean, const: true }
        service: { type: string, const: pneumags-address-api }
        apiVersion: { type: string, examples: [v1] }
        build: { type: string }
        environment: { type: string, examples: [production] }
    AutocompleteRequest:
      type: object
      additionalProperties: false
      required: [input, sessionToken]
      properties:
        input:
          type: string
          minLength: 3
          maxLength: 160
          description: Texte Unicode NFC sans caractère de contrôle.
        sessionToken:
          type: string
          format: uuid
          pattern: "^[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}$"
    DetailsRequest:
      type: object
      additionalProperties: false
      required: [placeId, sessionToken]
      properties:
        placeId:
          type: string
          minLength: 3
          maxLength: 512
          pattern: "^[A-Za-z0-9._-]+$"
        sessionToken:
          type: string
          format: uuid
    AddressSuggestion:
      type: object
      additionalProperties: false
      required: [placeId, primaryText, secondaryText, fullText]
      properties:
        placeId: { type: string }
        primaryText: { type: string }
        secondaryText: { type: string }
        fullText: { type: string }
    AutocompleteResponse:
      type: object
      additionalProperties: false
      required: [success, suggestions]
      properties:
        success: { type: boolean, const: true }
        suggestions:
          type: array
          maxItems: 5
          items:
            $ref: "#/components/schemas/AddressSuggestion"
    StructuredAddress:
      type: object
      additionalProperties: false
      required:
        - streetNumber
        - streetName
        - addressLine1
        - addressLine2
        - city
        - provinceCode
        - provinceName
        - postalCode
        - countryCode
        - countryName
        - latitude
        - longitude
        - placeId
        - formattedAddress
      properties:
        streetNumber: { type: string }
        streetName: { type: string }
        addressLine1: { type: string }
        addressLine2: { type: string }
        city: { type: string }
        provinceCode:
          type: string
          enum: [QC, ON, NB, NS, PE, NL, MB, SK, AB, BC, YT, NT, NU]
        provinceName: { type: string }
        postalCode:
          type: string
          pattern: "^[ABCEGHJ-NPRSTVXY][0-9][ABCEGHJ-NPRSTV-Z] [0-9][ABCEGHJ-NPRSTV-Z][0-9]$"
        countryCode: { type: string, const: CA }
        countryName: { type: string, const: Canada }
        latitude: { type: number, minimum: -90, maximum: 90 }
        longitude: { type: number, minimum: -180, maximum: 180 }
        placeId: { type: string }
        formattedAddress: { type: string }
    DetailsResponse:
      type: object
      additionalProperties: false
      required: [success, address]
      properties:
        success: { type: boolean, const: true }
        address:
          $ref: "#/components/schemas/StructuredAddress"
    InstallationTokenRequest:
      type: object
      additionalProperties: false
      required: [installationId]
      properties:
        installationId:
          type: string
          pattern: "^[A-Za-z0-9._:-]{8,128}$"
        ttlSeconds:
          type: integer
          minimum: 60
          maximum: 900
    InstallationTokenResponse:
      type: object
      additionalProperties: false
      required: [success, tokenType, accessToken, expiresIn, scope, installationId]
      properties:
        success: { type: boolean, const: true }
        tokenType: { type: string, const: Bearer }
        accessToken: { type: string, description: Jeton HS256 à courte durée de vie }
        expiresIn: { type: integer, minimum: 60, maximum: 900 }
        scope: { type: string, const: "addresses:read" }
        installationId: { type: string }
    ErrorResponse:
      type: object
      additionalProperties: false
      required: [success, error]
      properties:
        success: { type: boolean, const: false }
        error:
          type: object
          additionalProperties: false
          required: [code, message]
          properties:
            code:
              type: string
              enum:
                - INVALID_REQUEST
                - UNAUTHORIZED
                - FORBIDDEN
                - REPLAY_DETECTED
                - RATE_LIMITED
                - COUNTRY_NOT_ALLOWED
                - PROVIDER_TIMEOUT
                - PROVIDER_UNAVAILABLE
                - PROVIDER_INVALID_RESPONSE
                - INTERNAL_ERROR
            message: { type: string }
  responses:
    InvalidRequest:
      description: Entrée invalide
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
          example:
            success: false
            error: { code: INVALID_REQUEST, message: La requête est invalide. }
    Unauthorized:
      description: Authentification absente ou invalide
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
          example:
            success: false
            error: { code: UNAUTHORIZED, message: Authentification requise. }
    Forbidden:
      description: Client ou origine interdit
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
          example:
            success: false
            error: { code: FORBIDDEN, message: Accès interdit. }
    ReplayDetected:
      description: Nonce ou session déjà consommé
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
          example:
            success: false
            error: { code: REPLAY_DETECTED, message: La requête a déjà été utilisée. }
    RateLimited:
      description: Quota dépassé
      headers:
        Retry-After:
          schema: { type: integer, minimum: 1 }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
          example:
            success: false
            error: { code: RATE_LIMITED, message: Trop de requêtes. }
    ProviderInvalid:
      description: Réponse fournisseur invalide
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
    ProviderUnavailable:
      description: Fournisseur ou stockage de sécurité indisponible
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
