Перейти к содержанию

API - Hono на Cloudflare Workers

Общее описание

API-шлюз платформы AACSearch, реализованный на фреймворке Hono и развёрнутый как Cloudflare Worker (hono-saas). Выступает единой точкой входа для всех клиентских запросов, обеспечивая аутентификацию, авторизацию, биллинг, rate limiting и проксирование запросов к Typesense.

Расположение: /root/api

Структура проекта

/root/api/
├── wrangler.jsonc              # Конфигурация CF Workers + Secrets Store
├── package.json                # Зависимости и скрипты
├── tsconfig.json               # Конфигурация TypeScript
├── .dev.vars                   # Локальные секреты для wrangler dev
├── src/
│   ├── index.ts                # Точка входа, middleware chain, exports
│   ├── env.ts                  # Типы биндингов, SecretBinding, resolveSecrets()
│   ├── middleware/
│   │   ├── auth.ts             # Auth.js + Zitadel OIDC
│   │   ├── apiAuth.ts          # 4-факторная аутентификация
│   │   ├── bearer.ts           # JWT верификация с кешированием JWKS
│   │   ├── tenant.ts           # Определение тенанта по поддомену
│   │   ├── billingGate.ts      # Проверка наличия подписки (Lago)
│   │   ├── rateLimit.ts        # Ограничение частоты запросов (KV)
│   │   └── featureGate.ts      # Проверка доступных фич (Lago entitlements)
│   ├── modules/
│   │   ├── search/
│   │   │   ├── routes.ts       # POST /api/v1/search
│   │   │   ├── apiKeysRoutes.ts # Управление scoped API keys
│   │   │   ├── typesense.ts    # Клиент Typesense
│   │   │   └── scopedKey.ts    # Верификация scoped search key
│   │   ├── documents/
│   │   │   └── routes.ts       # CRUD документов
│   │   ├── billing/
│   │   │   ├── routes.ts       # Эндпоинты биллинга (1168 строк)
│   │   │   ├── lagoClient.ts   # Фабрика Lago SDK
│   │   │   ├── usageTracking.ts # Учёт использования
│   │   │   ├── lagoWebhooks.ts # Обработка вебхуков Lago
│   │   │   └── dashboardRoutes.ts # Дашборд биллинга
│   │   ├── typesense/
│   │   │   └── routes.ts       # Прокси-шлюз к Typesense API (800+ строк)
│   │   ├── auth/
│   │   │   └── routes.ts       # Zitadel V2 API proxy (login/register/reset)
│   │   ├── data-sources/
│   │   │   ├── routes.ts       # CRUD источников данных
│   │   │   ├── queue.ts        # Обработка очереди задач (crawl, reindex)
│   │   │   ├── crawl.ts        # Web-краулинг
│   │   │   ├── parsers.ts      # Парсеры (Feed/HTML)
│   │   │   ├── db.ts           # D1 запросы
│   │   │   └── schema.sql      # Схема D1
│   │   ├── reindex/
│   │   │   ├── routes.ts       # API переиндексации
│   │   │   └── executor.ts     # Исполнитель задач
│   │   └── ai/
│   │       ├── routes.ts       # RAG/AI эндпоинты
│   │       └── presets.ts      # AI-пресеты
│   ├── cron/
│   │   └── index.ts            # Планировщик (daily + 15-min)
│   └── gen/
│       ├── typesense/          # Сгенерированные типы Typesense OpenAPI
│       └── search-client/      # Сгенерированные типы публичного Search API
├── scripts/                    # Вспомогательные скрипты
└── specs/
    └── typesense/openapi.yml   # Спецификация Typesense API

Конфигурация Cloudflare Worker (wrangler.jsonc)

Основные параметры

Параметр Значение
Имя воркера hono-saas
Compatibility Date 2026-02-08
KV Namespace (CACHE) 26582ff95322434cb3a395bef5c0acc0
D1 Database (DB) aacsearch-data-sources
Queue (CRAWL_QUEUE) jobs-queue
Secrets Store 26a0f16b21cb435c8c842e2a18787e56
Cron 0 2 * * * (ежедневно в 2:00 UTC), */15 * * * * (каждые 15 мин)

Маршруты

Паттерн Зона
api.aacsearch.com/* cf8a427718fe4e801feaf840620e0bd3
docs.aacsearch.com/* та же зона

Переменные окружения

ZITADEL_ISSUER=https://auth.aacsearch.com
ZITADEL_CLIENT_ID=359289240758059011
ZITADEL_JWKS_URI=https://auth.aacsearch.com/oauth/v2/keys
LAGO_API_URL=https://billing-api.aacsearch.com/api/v1
TYPESENSE_URL=http://aacsearch.com:13000
TYPESENSE_COLLECTION=documents
TENANT_ROOT_DOMAIN=aacsearch.com

Секреты (Cloudflare Secrets Store)

Все секреты хранятся в Secrets Store (ID: 26a0f16b21cb435c8c842e2a18787e56) и персистентны между деплоями. Не требуют переустановки после wrangler deploy.

Секрет Назначение
AUTH_SECRET Подпись сессии Auth.js
ZITADEL_CLIENT_SECRET OIDC client secret
ZITADEL_SERVICE_ACCOUNT_TOKEN PAT сервисного аккаунта
ZITADEL_AUDIENCE OIDC audience
LAGO_API_KEY Ключ Lago API
LAGO_WEBHOOK_HMAC_KEY HMAC-ключ для вебхуков Lago
TYPESENSE_PARENT_SEARCH_KEY Родительский ключ поиска
TYPESENSE_API_KEY Серверный ключ Typesense (aac)
OPENAI_API_KEY Ключ OpenAI (для AI-модулей)

Тип SecretBinding

В production Secrets Store возвращает { get(): Promise<string> }, в dev (.dev.vars) — обычную строку. Тип:

type SecretBinding = string | { get(): Promise<string> };

Секреты разрешаются один раз за запрос через resolveSecrets(env) и сохраняются в контексте Hono (c.get("secrets")) или передаются параметром в cron/queue обработчики.


Цепочка Middleware

Запросы проходят через последовательную цепочку middleware, каждый из которых добавляет контекст или отклоняет запрос:

Запрос
0. resolveSecrets()     — Разрешение SecretBinding → строки
  │                       Результат: secrets в контексте Hono
1. CORS                 — Разрешённые origins:
  │                       app/platform/billing/docs.aacsearch.com
2. authConfig()         — Инициализация Auth.js
3. Auth V2 Routes       — /api/auth/v2/* (login, register, reset)
  │                       Публичные маршруты, без Bearer
4. Auth.js Routes       — /api/auth/* (OIDC sign-in, callback, sign-out)
5. tenantResolver()     — Извлечение slug из поддомена
  │                       acme.aacsearch.com → slug = "acme"
6. apiAuth()            — 4-факторная аутентификация:
  │                       1) X-AACSEARCH-API-KEY (scoped key, клиентский JS)
  │                       2) Bearer JWT (SDK, server-to-server)
  │                       3) aac_session cookie (V2 Session, Panel SPA)
  │                       4) Auth.js session cookie (legacy)
  │                       Результат: tenantId в контексте
7. billingGate()        — Проверка активной подписки
  │                       KV кеш → webhook маркер → Lago API (fallback)
  │                       402 если нет подписки, 502 если Lago недоступен
8. rateLimit()          — Ограничение частоты запросов
  │                       KV счётчик: ratelimit:{tenantId}:{minute}
  │                       429 если лимит превышен
9. featureGate()        — Проверка доступности фич
  │                       Lago entitlements → KV кеш
  │                       403 если фича недоступна
Обработчик маршрута

Аутентификация

Auth.js + Zitadel OIDC (файл: src/middleware/auth.ts)

Используется @hono/auth-js с провайдером Zitadel: - Client ID: 359289240758059011 - Issuer: https://auth.aacsearch.com - basePath: /api/auth

Поток JWT Callback

  1. Из профиля пользователя извлекается urn:zitadel:iam:user:resourceowner:id → сохраняется как token.org_id
  2. Из JWT claim извлекается plan_code (инжектируется Zitadel Action) → сохраняется в токене
  3. Сессия содержит org_id и plan_code

Bearer JWT верификация (файл: src/middleware/bearer.ts)

  1. Извлечение токена из заголовка Authorization: Bearer <jwt>
  2. Проверка подписи через JWKS:
  3. Сначала KV кеш zitadel:jwks (TTL 1 час)
  4. Затем удалённый запрос к ZITADEL_JWKS_URI
  5. Валидация issuer и audience
  6. Извлечение tenantId из claim:
  7. urn:zitadel:iam:user:resourceowner:id (основной)
  8. org_id (запасной)
  9. resource_owner (запасной)
  10. Извлечение plan_code (если есть)

apiAuth Middleware (файл: src/middleware/apiAuth.ts)

4-факторная аутентификация в порядке приоритета:

Приоритет Метод Назначение
1 X-AACSEARCH-API-KEY header Scoped search key для клиентского JS
2 Authorization: Bearer <jwt> SDK / server-to-server (Zitadel JWT)
3 aac_session cookie V2 Session API (Panel SPA)
4 Auth.js session cookie Legacy браузерный flow

V2 Session верифицируется через Zitadel GET /v2/sessions/{sessionId}, результат кешируется в KV на 5 минут. Все методы устанавливают tenantId в контексте.


Rate Limiting (файл: src/middleware/rateLimit.ts)

Лимиты по планам

План Запросов/мин
starter_v1 100
professional_v1 500
enterprise_v1 0 (без ограничений)
payg_v1 1000

Механизм

  1. Получение planCode из контекста (Bearer JWT, billingGate или featureGate)
  2. Если planCode отсутствует — проверка KV: billing:{tenantId}:entitlement или billing:{tenantId}:subscription_active
  3. Вычисление минутного окна: Math.floor(Date.now() / 60000)
  4. KV ключ: ratelimit:{tenantId}:{minute} (TTL 1 мин)
  5. Инкремент счётчика
  6. HTTP 429 если лимит превышен

Заголовки ответа

  • X-RateLimit-Limit — Максимум запросов
  • X-RateLimit-Remaining — Осталось запросов
  • X-RateLimit-Reset — Время сброса счётчика

Feature Gates (файл: src/middleware/featureGate.ts)

Доступные фичи

synonyms, curations, stopwords, presets, vector_search, rag,
multi_search, analytics_advanced, bulk_import, export, stemming,
nl_search, geo_search, faceting, grouping, semantic_search,
image_search, voice_search, joins, scoped_api_keys, conversations

Привязка маршрутов к фичам

Маршрут Фича
/api/v1/engine/conversations/* rag
/api/v1/engine/synonym_sets/* synonyms
/api/v1/engine/curation_sets/* curations
/api/v1/engine/stopwords/* stopwords
/api/v1/engine/presets/* presets
/api/v1/engine/stemming/* stemming
/api/v1/engine/nl_search_models/* nl_search
/api/v1/engine/multi_search multi_search

Механизм

  1. Получение subscription external_id из KV или Lago API
  2. Запрос entitlements: lago.subscriptions.findAllSubscriptionEntitlements()
  3. Кеширование в KV: entitlements:{tenantId} (TTL 5 мин)
  4. Вебхуки инвалидируют кеш при изменении подписки
  5. HTTP 403 если фича недоступна, 402 если нет подписки

Модули

Search (файл: src/modules/search/routes.ts)

Эндпоинт: POST /api/v1/search

Запрос

{
  "q": "поисковый запрос",
  "limit": 20,
  "page": 1,
  "filters": { "status": "active", "category": "news" }
}

Ответ

{
  "total": 150,
  "hits": [
    {
      "id": "doc-123",
      "title": "Заголовок документа",
      "snippet": "...фрагмент с <mark>подсветкой</mark>...",
      "score": 0.95
    }
  ]
}

Поток обработки

  1. Валидация запроса (Zod)
  2. Вызов searchTypesense() с изоляцией тенанта
  3. Построение filter_by: tenant_id:={tenantId} && ...safe_filters
  4. Безопасные фильтры: status, type, category, lang (whitelist)
  5. Генерация scoped search key (HMAC, TTL 10 мин)
  6. Асинхронная отправка usage event
  7. Маппинг ответа: Typesense text_matchscore, highlights → snippet

Scoped Search Key (файл: src/modules/search/scopedKey.ts)

Алгоритм генерации ключа с ограниченными правами:

  1. Создание JSON параметров: {filter_by: "...", expires_at: <unix_timestamp + 600>}
  2. HMAC: HMAC-SHA256(parentSearchKey, paramsJSON) → base64
  3. Извлечение префикса: первые 4 символа parentSearchKey
  4. Комбинирование: digest + prefix + paramsJSON → base64

Documents (файл: src/modules/documents/routes.ts)

Эндпоинт Метод Описание
/api/v1/documents POST Индексация одного документа
/api/v1/documents/bulk POST Массовая индексация (до 1000)
/api/v1/documents/{id} DELETE Удаление документа

Схема документа

{
  "id": "doc-1",
  "title": "Заголовок",
  "content": "Содержимое документа",
  "type": "document",
  "category": "news",
  "status": "active",
  "lang": "en",
  "metadata": {}
}

Автоматически добавляемые поля: - tenant_id — ID тенанта (изоляция) - created_at — Unix timestamp - Значения по умолчанию: type = "document", status = "active", lang = "en"

Typesense Gateway (файл: src/modules/typesense/routes.ts)

Прокси-шлюз к Typesense API с изоляцией тенантов через префиксирование коллекций.

Стратегия изоляции

Все коллекции имеют формат: t_{tenantId}__{collectionName}

Внешнее имя Внутреннее имя
products t_12345__products
articles t_12345__articles

Разрешённые эндпоинты

/collections, /multi_search, /aliases, /analytics,
/presets, /stopwords, /curation_sets, /synonym_sets

Заблокированные эндпоинты

/keys, /debug, /stats.json, /metrics.json,
/operations, /analytics/flush

Перезапись запросов

Операция Перезапись
POST /collections body.namet_{tenantId}__name
POST /multi_search searches[].collection → с префиксом
PUT /aliases/{id} body.collection_name → с префиксом
POST /analytics/rules Имя правила + source_collections → с префиксом
POST /analytics/events body.name → с префиксом

Перезапись ответов

Из ответов удаляется префикс тенанта: - Списки коллекций, алиасов, правил курирования, синонимов, стоп-слов, пресетов — фильтруются по тенанту и депрефиксируются

Определение и отслеживание фич

Из URL, query string и тела запроса определяются используемые фичи:

Признак Фича
_geoloc, geo(...) geo_search
vector_query semantic_search
image_query, image_embedding image_search
voice_query voice_search
join, join_as joins
group_by grouping
/multi_search multi_search

Auth V2 Proxy (файл: src/modules/auth/routes.ts)

Прокси к Zitadel V2 API для клиентской панели. Все эндпоинты публичные (без Bearer), PAT сервисного аккаунта хранится на сервере.

Эндпоинт Метод Описание
/api/auth/v2/login POST Создание сессии (проверка пароля)
/api/auth/v2/register POST Регистрация + авто-логин
/api/auth/v2/password-reset POST Запрос письма для сброса пароля
/api/auth/v2/password-set POST Установка нового пароля по коду
/api/auth/v2/session GET Проверка текущей сессии
/api/auth/v2/logout POST Удаление сессии

Data Sources (файл: src/modules/data-sources/)

Модуль управления источниками данных (RSS/Atom фиды, веб-страницы) с автоматическим краулингом.

Эндпоинт Метод Описание
/api/v1/data-sources GET Список источников
/api/v1/data-sources POST Создание источника
/api/v1/data-sources/:id PUT Обновление
/api/v1/data-sources/:id DELETE Удаление

Хранение: D1 (SQLite), асинхронная обработка: Queue (Cloudflare Queues).

Reindex (файл: src/modules/reindex/)

Управление задачами полной переиндексации коллекций.

AI (файл: src/modules/ai/)

RAG/AI эндпоинты и пресеты для AI-поиска.

API Keys (файл: src/modules/search/apiKeysRoutes.ts)

Генерация и управление scoped API-ключами для клиентского поиска.


Биллинг

Lago Client (файл: src/modules/billing/lagoClient.ts)

export function createLagoClient(env: LagoEnv) {
  return Client(env.LAGO_API_KEY, { baseUrl: env.LAGO_API_URL });
}

КРИТИЧНО: LAGO_API_URL должен заканчиваться на /api/v1 — SDK конкатенирует пути напрямую.

Маршруты биллинга (файл: src/modules/billing/routes.ts)

Эндпоинт Метод Описание
/api/v1/billing/customers GET Получение данных о клиенте тенанта
/api/v1/billing/customers POST Создание/обновление клиента
/api/v1/billing/subscriptions GET Список подписок
/api/v1/billing/subscriptions POST Создание подписки
/api/v1/billing/wallets GET Список кошельков
/api/v1/billing/wallets POST Создание кошелька
/api/v1/billing/wallet/topup POST Пополнение кошелька
/api/v1/billing/invoices GET Список инвойсов
/api/v1/billing/events POST Отправка события использования

Дашборд биллинга (файл: src/modules/billing/dashboardRoutes.ts)

Эндпоинт Описание
/api/v1/billing/dashboard/usage Статистика использования за текущий период
/api/v1/billing/dashboard/invoices История инвойсов
/api/v1/billing/dashboard/subscription Детали текущей подписки

Отслеживаемые метрики: search_request, vector_search, rag_query, documents_stored, geo_query, multi_search_query, semantic_query, image_query, voice_query, join_query

Учёт использования (файл: src/modules/billing/usageTracking.ts)

Биллинговые события

search_request, vector_search, rag_query, documents_indexed,
documents_stored, api_call, synonym_set_created, curation_rule_created,
geo_query, multi_search_query, semantic_query, image_query,
voice_query, join_query

Структура события

{
  "transaction_id": "uuid-v4",
  "external_subscription_id": "sub-123",
  "code": "search_request",
  "timestamp": 1707523200,
  "properties": {
    "tenant_id": "org-456",
    "query": "поисковый запрос"
  }
}

Отправка неблокирующая через executionCtx.waitUntil().

Вебхуки Lago (файл: src/modules/billing/lagoWebhooks.ts)

Эндпоинт: POST /webhooks/lago (без аутентификации)

Верификация подписи

  • Заголовок: X-Lago-Signature
  • Алгоритм: X-Lago-Signature-Algorithm (hmac или jwt, по умолчанию hmac)
  • HMAC: base64(HMAC-SHA256(key, body))base64, НЕ hex!

Дедупликация

KV ключ: wh:{uniqueKey} (TTL 7 дней). UniqueKey из заголовка X-Lago-Unique-Key или SHA-256 хеш тела.

Обработка событий

Событие Действие
subscription.started Кеш subscription_active + plan_code + очистка entitlements + синхронизация plan_code в Zitadel
subscription.terminated Удаление всех биллинг-кешей + удаление plan_code из Zitadel
subscription.updated Очистка кешей, обновление plan_code, синхронизация в Zitadel
invoice.paid_credit_added Кеш последнего кредитного события
invoice.finalized Кеш деталей инвойса
wallet.depleted Кеш события истощения кошелька

Синхронизация с Zitadel

При наличии ZITADEL_SERVICE_ACCOUNT_TOKEN: - POST {issuer}/management/v1/metadata/plan_code с plan_code (base64) - DELETE {issuer}/management/v1/metadata/plan_code при отмене подписки - Выполняется неблокирующе через waitUntil


Billing Gate (файл: src/middleware/billingGate.ts)

Каскад проверок наличия активной подписки:

1. KV кеш: billing:{tenantId}:entitlement (TTL 5 мин)
   ├─ Найдено → результат
   └─ Не найдено ↓

2. KV маркер: billing:{tenantId}:subscription_active (TTL 30 дней)
   ├─ Найдено → результат
   └─ Не найдено ↓

3. Lago API (таймаут 5 сек):
   lago.customers.findAllCustomerSubscriptions(tenantId)
   ├─ Активная подписка → кеширование + OK
   ├─ Нет подписки → HTTP 402
   └─ Lago недоступен → HTTP 502

Кеширование отрицательных результатов: - Неоплаченные тенанты: 60 сек - Оплаченные тенанты: 300 сек


Планировщик задач (Cron)

Расписания

Расписание Задача
0 2 * * * (ежедневно в 2:00 UTC) reportDocumentStorage() — подсчёт документов
*/15 * * * * (каждые 15 мин) dispatchScheduledCrawls() — запуск краулинга

Все cron-обработчики получают секреты через resolveSecrets(env) в index.ts, затем передают как параметр secrets: ResolvedSecrets.

reportDocumentStorage()

  1. Запрос к Typesense /collections
  2. Агрегация количества документов по тенантам (коллекции t_{tenantId}__*)
  3. Отправка события documents_stored в Lago для каждого тенанта
  4. Агрегация MAX (Lago хранит только максимальное значение за период)

dispatchScheduledCrawls()

  1. Проверка D1 на источники данных с наступившим расписанием
  2. Постановка задач краулинга в Queue (CRAWL_QUEUE)

Очередь задач (Queue)

Queue: jobs-queue (Cloudflare Queues)

Обработчик handleJobQueue() принимает batch сообщений:

Тип Описание
crawl Краулинг источника данных (URL, парсинг, индексация)
reindex Полная переиндексация коллекции
reindex_chunk Обработка чанка переиндексации

Постоянные ошибки (400, 404) → подтверждение + пометка failed. Временные ошибки → retry.


KV Cache — Использование

Ключ TTL Назначение
zitadel:jwks 1 час Кеш JWKS для верификации JWT
billing:{tenantId}:entitlement 5 мин / 1 мин (negative) Кеш entitlement с источником
billing:{tenantId}:subscription_active 30 дней Маркер вебхука с plan_code
billing:{tenantId}:plan_code 5 мин Кешированный plan code
billing:{tenantId}:last_paid_credit 1 день Последнее кредитное событие
billing:{tenantId}:last_invoice 1 день Последний инвойс
billing:{tenantId}:wallet_depleted 1 час Событие истощения кошелька
ratelimit:{tenantId}:{minute} 1 мин Счётчик запросов
wh:{uniqueKey} 7 дней Дедупликация вебхуков
entitlements:{tenantId} 5 мин Entitlements фич
lago:webhook:public_key 1 день Публичный ключ вебхука Lago

OpenAPI документация

Эндпоинт Описание
GET /doc OpenAPI спецификация (JSON, английский)
GET /doc/en OpenAPI спецификация (английский)
GET /doc/ru OpenAPI спецификация (русский)
GET /docs Scalar API Reference UI (двуязычный)

Обработка ошибок

Глобальный обработчик

app.onError((err, c) => {
  if (err instanceof HTTPException) {
    return c.json({ error: err.message }, err.status);
  }
  console.error(err);
  return c.json({ error: "Internal error", debug: String(err) }, 500);
});

HTTP-коды ответа

Код Значение
401 Отсутствует или невалидная аутентификация
402 Нет активной подписки / фича недоступна
403 Доступ запрещён / фича не включена в план
404 Ресурс не найден
429 Превышен лимит запросов
502 Провайдер биллинга / бэкенд поиска недоступен

Зависимости

Библиотека Версия Назначение
hono 4.9.0 Веб-фреймворк для Workers
@hono/zod-openapi 1.2.1 OpenAPI из Zod-схем
@hono/auth-js 1.1.0 Интеграция Auth.js
@scalar/hono-api-reference 0.9.40 UI документации API
@auth/core 0.37.0 Auth.js core
jose 6.1.3 JWT операции, JWKS
lago-javascript-client 1.0.0 Lago SDK
openapi-fetch 0.13.0 Сгенерированный OpenAPI клиент
zod 4.0.0 Валидация схем
wrangler 4.12.0 CLI Cloudflare Workers

Деплой

Секреты хранятся в Cloudflare Secrets Store и персистентны между деплоями. Пост-деплой шаги не требуются.

cd /root/api
npx wrangler deploy
# Готово! Секреты сохраняются автоматически.

Обновление секрета

npx wrangler secrets-store secret update 26a0f16b21cb435c8c842e2a18787e56 \
  --name SECRET_NAME --remote

Экспорт обработчиков

Worker экспортирует три обработчика:

export default {
  fetch: app.fetch,                    // HTTP-запросы
  async scheduled(event, env, ctx) {   // Cron-триггеры
    const secrets = await resolveSecrets(env);
    await handleScheduled(event, env, ctx, secrets);
  },
  async queue(batch, env, ctx) {       // Queue-сообщения
    const secrets = await resolveSecrets(env);
    await handleJobQueue(batch, env, ctx, secrets);
  },
};