openapi: 3.0.3
info:
  title: hook-relay
  version: 0.1.0
  description: >
    Доставка вебхуков. Событие подписывается HMAC-SHA256 (заголовок X-Relay-Signature: t=<unix>,v1=<hex>,
    подписывается "<t>.<тело>"), доставляется получателю, при сбое повторяется с экспоненциальной
    задержкой и джиттером, после лимита попыток уходит в dead-letter. Адреса получателей проверяются
    на SSRF после DNS-разрешения.
servers:
  - url: http://localhost:8113
security:
  - {}
  - bearer: []
paths:
  /v1/endpoints:
    post:
      summary: Добавить получателя
      description: Секрет подписи возвращается только в этом ответе.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url: { type: string, format: uri, maxLength: 2048 }
                description: { type: string, maxLength: 200 }
      responses:
        "201": { description: Создан }
        "400": { description: Некорректный url или адрес заблокирован (invalid_url, ssrf_blocked) }
        "401": { description: Нужен токен }
        "429": { description: Лимит получателей }
    get:
      summary: Список получателей (без секретов)
      responses:
        "200": { description: Список }
  /v1/endpoints/{id}:
    delete:
      summary: Удалить получателя вместе с его доставками
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "204": { description: Удалён }
        "404": { description: Не найден }
  /v1/events:
    post:
      summary: Поставить событие в очередь доставки
      description: >
        Идемпотентность: заголовок Idempotency-Key или поле idempotency_key. Повтор с тем же ключом для
        того же получателя возвращает прежнюю доставку (200, duplicate=true) без новой отправки.
      parameters:
        - { name: Idempotency-Key, in: header, required: false, schema: { type: string, maxLength: 128 } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [endpoint_id, type]
              properties:
                endpoint_id: { type: string }
                type: { type: string, pattern: "^[A-Za-z0-9._:-]{1,64}$" }
                payload: {}
                idempotency_key: { type: string, maxLength: 128 }
      responses:
        "201": { description: Принято }
        "200": { description: Дубль по ключу идемпотентности }
        "400": { description: Некорректный ввод }
        "404": { description: Получатель не найден }
        "429": { description: Очередь переполнена }
  /v1/deliveries:
    get:
      summary: Лента доставок (новые сверху)
      parameters:
        - { name: status, in: query, schema: { type: string, enum: [pending, delivered, dead] } }
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 200, default: 50 } }
      responses:
        "200": { description: Доставки }
  /v1/deliveries/{id}:
    get:
      summary: Одна доставка
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200": { description: Доставка }
        "404": { description: Не найдена }
  /v1/deliveries/{id}/retry:
    post:
      summary: Вернуть доставку из dead-letter в очередь
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200": { description: Поставлена в очередь }
        "404": { description: Не найдена }
        "409": { description: Доставка не в dead-letter }
  /v1/stats:
    get:
      summary: Счётчики очереди
      responses:
        "200": { description: pending, delivered, dead, endpoints }
  /healthz:
    get:
      summary: Проверка живости (проверяет доступность БД)
      responses:
        "200": { description: ok }
        "503": { description: БД недоступна }
components:
  securitySchemes:
    bearer: { type: http, scheme: bearer, description: "Нужен, если задан API_TOKEN" }
