Skip to content

Secrets / Управление секретами ​

Обзор ​

В проекте используется двухуровневая система хранения секретов:

  1. Глобальные секреты (Doppler) — общие для всех интеграций
  2. Per-integration секреты (D1) — уникальные для каждой интеграции, зашифрованы
┌──────────────────────────────────────────────────────────────┐
│                         DOPPLER                               │
│                                                               │
│  Глобальные секреты (одинаковые для всех интеграций):         │
│  • CRYPTO_KEY + CRYPTO_SALT (шифрование D1 secrets)           │
│  • OPENAI_API_KEY, CLAUDE_API_KEY, GROQ_API_KEY            │
│  • DATABASE_URL (Neon PostgreSQL)                             │
│                                                               │
└────────────────────────────┬─────────────────────────────────┘
                             │
        ┌────────────────────┼────────────────────┐
        │                    │                    │
        ▼                    ▼                    ▼
   ┌─────────┐         ┌─────────┐         ┌─────────┐
   │  local  │         │   dev   │         │  prod   │
   │         │         │         │         │         │
   │.dev.vars│         │CF Secret│         │CF Secret│
   └─────────┘         └─────────┘         └─────────┘

┌──────────────────────────────────────────────────────────────┐
│                    CLOUDFLARE D1 (integ-db)                   │
│                                                               │
│  Per-integration секреты (зашифрованы CRYPTO_KEY + CRYPTO_SALT): │
│  • sofa/NETHUNT_EMAIL                                         │
│  • sofa/NETHUNT_API_KEY                                       │
│  • sofa/NETHUNT_FOLDER_ID                                     │
│  • sofa/PLATFORM_ASSISTANT_ID                                     │
│  • booking/API_KEY                                            │
│  • ...                                                        │
│                                                               │
└──────────────────────────────────────────────────────────────┘

Глобальные секреты (Doppler) ​

Что хранится в Doppler ​

СекретНазначение
CRYPTO_KEYКлюч AES-256-GCM для шифрования D1 credentials (min 32 chars)
CRYPTO_SALTСоль для PBKDF2 key derivation (min 16 chars)
OPENAI_API_KEYAPI ключ OpenAI
CLAUDE_API_KEYAPI ключ Anthropic (Claude)
GEMINI_API_KEYAPI ключ Google Gemini
GROQ_API_KEYAPI ключ Groq
DATABASE_URLURL Neon PostgreSQL (если используется)
ACCESS_TOKENТокен авторизации для Gateway

Конфиги Doppler ​

ConfigНазначениеИспользуется
localЛокальная разработка.dev.vars генерация
devDev окружениеCloudflare Workers Secrets
prodProductionCloudflare Workers Secrets

Команды Doppler ​

bash
# Установка
brew install dopplerhq/cli/doppler

# Логин
doppler login

# Привязка проекта (в корне репозитория)
doppler setup
# Выбрать: project = integ-core, config = local

# Просмотр секретов
doppler secrets --config local
doppler secrets --config dev
doppler secrets --config prod

# Скачать секреты в env формате
doppler secrets download --config local --format env-no-quotes --no-file

Per-integration секреты (D1) ​

Структура хранения ​

Секреты хранятся в таблице creds в D1:

sql
CREATE TABLE creds (
  id TEXT PRIMARY KEY,
  integration_name TEXT NOT NULL,  -- 'sofa', 'booking', etc.
  key TEXT NOT NULL,                -- 'NETHUNT_API_KEY', etc.
  encrypted_value TEXT NOT NULL,    -- AES-256-GCM encrypted
  created_at TEXT,
  updated_at TEXT,
  UNIQUE(integration_name, key)
);

Пакет @happ-integ/creds ​

typescript
import { Creds } from "@happ-integ/creds";

const creds = new Creds({
	d1: env.DB,
	masterKey: env.CRYPTO_KEY,
	salt: env.CRYPTO_SALT,
});

// Получить все credentials для интеграции
interface SofaCreds {
	NETHUNT_EMAIL: string;
	NETHUNT_API_KEY: string;
	NETHUNT_FOLDER_ID: string;
	PLATFORM_ASSISTANT_ID: string;
}
const secrets = await creds.get<SofaCreds>("sofa");
console.log(secrets.NETHUNT_API_KEY);

// Установить credential
await creds.set("sofa", "NEW_KEY", "secret-value");

// Получить один credential
const apiKey = await creds.getOne("sofa", "NETHUNT_API_KEY");

// Удалить credential
await creds.delete("sofa", "OLD_KEY");

// Список всех интеграций
const integrations = await creds.listIntegrations();
// => ['sofa', 'booking']

// Список ключей интеграции
const keys = await creds.listKeys("sofa");
// => ['NETHUNT_EMAIL', 'NETHUNT_API_KEY', 'NETHUNT_FOLDER_ID', 'PLATFORM_ASSISTANT_ID']

Шифрование ​

Используется AES-256-GCM через Web Crypto API:

  • CRYPTO_KEY + CRYPTO_SALT используются для derive key через PBKDF2
  • Каждое значение имеет уникальный IV (initialization vector)
  • IV хранится вместе с зашифрованными данными

Workflow по окружениям ​

Local (Локальная разработка) ​

Doppler (config: local)
        │
        ▼
  generate-env.script.ts
        │
        ├──> .env (корень проекта)
        │
        └──> integrations/sofa/.dev.vars
                    │
                    ▼
             wrangler dev
        (читает .dev.vars автоматически)

Команды:

bash
# Генерация .dev.vars из Doppler
pnpm generate:env -- local

# Запуск (автоматически генерирует env)
pnpm start sofa

# Запуск без генерации
SKIP_ENV=true pnpm start sofa

Файл .dev.vars:

env
ENVIRONMENT=local
CRYPTO_KEY=your-32-char-master-key-here!!!
CRYPTO_SALT=your-16-char-salt-here
OPENAI_API_KEY=sk-...
CLAUDE_API_KEY=sk-ant-...
GEMINI_API_KEY=...
GROQ_API_KEY=gsk_...

Dev / Production (Cloudflare) ​

Doppler (config: dev/prod)
        │
        ▼
  sync-cf-secrets.ts
        │
        ▼
Cloudflare Workers Secrets
        │
        ▼
   wrangler deploy
  (секреты доступны в env)

Команды:

bash
# Синхронизация секретов Doppler → Cloudflare (dev)
cd integrations/sofa
pnpm sync-secrets:dev

# Синхронизация секретов Doppler → Cloudflare (prod)
pnpm sync-secrets:prod

# Полный деплой (build + sync + deploy)
pnpm deploy:dev   # dev
pnpm deploy       # production

Скрипт sync-cf-secrets.ts ​

Синхронизирует секреты из Doppler в Cloudflare Workers Secrets:

bash
# Использование (из директории интеграции)
cd integrations/sofa
tsx ../../scripts/sync-cf-secrets.ts dev
tsx ../../scripts/sync-cf-secrets.ts production

Что делает:

  1. Получает секреты из Doppler (doppler secrets --config <config> --json)
  2. Валидирует обязательные секреты (DATABASE_URL, OPENAI_API_KEY)
  3. Загружает каждый секрет в Cloudflare (wrangler secret put <key> --env <env>)

Обязательные секреты:

typescript
const REQUIRED_SECRETS = ["DATABASE_URL", "OPENAI_API_KEY"];

Добавление новых credentials ​

Глобальные (Doppler) ​

  1. Открыть https://dashboard.doppler.com
  2. Выбрать project integ-core
  3. Выбрать config (local / dev / prod)
  4. Добавить секрет
  5. Для локальной разработки: pnpm generate:env -- local
  6. Для dev/prod: pnpm sync-secrets:dev или pnpm sync-secrets:prod

Per-integration (D1) ​

Через код или CLI:

typescript
// В handler или admin endpoint
const creds = new Creds({ d1: env.DB, masterKey: env.CRYPTO_KEY, salt: env.CRYPTO_SALT });

// Добавить credential
await creds.set("sofa", "NEW_API_KEY", "secret-value");

// Проверить
const keys = await creds.listKeys("sofa");
console.log(keys);

Или через wrangler D1:

bash
# Подключиться к D1 (через CLI нужно вручную зашифровать)
wrangler d1 execute integ-db --command "SELECT * FROM creds"

D1 Setup ​

Создание баз данных ​

bash
# Создать отдельные базы для dev и prod
wrangler d1 create integ-db-dev
wrangler d1 create integ-db-prod

Применение миграций ​

Миграции (включая создание таблицы creds) применяются автоматически при вызове POST /{integration}/setup. SQL-файлы и ручные команды wrangler d1 execute / wrangler d1 migrations apply не используются.

Все миграции (глобальные и integration-specific) являются code-based и управляются через @happ-integ/core.

Обновление wrangler.toml ​

После создания баз, обновить ID в wrangler.toml для каждого окружения:

toml
# Для локальной разработки
[[d1_databases]]
binding = "INTEG_DB"
database_name = "integ-db"
database_id = "00000000-0000-0000-0000-000000000001"

# DEV окружение
[[env.dev.d1_databases]]
binding = "INTEG_DB"
database_name = "integ-db-dev"
database_id = "YOUR_DEV_DATABASE_ID"  # ← из вывода wrangler d1 create integ-db-dev

# PRODUCTION окружение
[[env.production.d1_databases]]
binding = "INTEG_DB"
database_name = "integ-db-prod"
database_id = "YOUR_PROD_DATABASE_ID"  # ← из вывода wrangler d1 create integ-db-prod

Ручная установка секретов в Cloudflare ​

Если нужно установить секреты без Doppler:

bash
# Установить секрет
wrangler secret put CRYPTO_KEY --env production
wrangler secret put CRYPTO_SALT --env production
# Ввести значение интерактивно

# Список секретов
wrangler secret list --env production

# Удалить секрет
wrangler secret delete OLD_SECRET --env production

Troubleshooting ​

"D1Database is required" ​

Проверить wrangler.toml:

toml
[[d1_databases]]
binding = "INTEG_DB"  # ← должно совпадать с env.INTEG_DB в коде
database_name = "integ-db"
database_id = "valid-uuid"

"masterKey must be at least 32 characters" ​

CRYPTO_KEY должен быть минимум 32 символа, CRYPTO_SALT — минимум 16:

bash
# Генерация надёжного ключа
openssl rand -base64 32

"Failed to decrypt credential" ​

  • Убедиться, что у воркера и у integ-api одинаковые CRYPTO_KEY/CRYPTO_SALT одного окружения
  • Если ключ изменился — см. «Ротация CRYPTO_KEY» ниже

Doppler login issues ​

bash
# Переавторизация
doppler logout
doppler login

# Проверить текущий проект
doppler configure

Використання секретів у пакетах ​

Пакети @happ-integ/* автоматично підтримують Doppler секрети через env binding. Не потрібно вручну передавати API ключі — просто передайте env об'єкт:

Приклад використання ​

typescript
import { LLMService } from "@happ-integ/llm";
import { VectorService } from "@happ-integ/vector";
import { QdrantClient } from "@happ-integ/qdrant";

// LLM — автоматично бере OPENAI_API_KEY, CLAUDE_API_KEY, GOOGLE_API_KEY, GROQ_API_KEY з env
const llm = new LLMService({
  primary: "groq",
  env, // ← передаємо env binding
});

// Vector — автоматично бере QDRANT_URL, QDRANT_API_KEY тощо
const vector = new VectorService({
  primary: "qdrant",
  env, // ← передаємо env binding
});

// Qdrant — автоматично бере QDRANT_URL, QDRANT_API_KEY
const qdrant = new QdrantClient({ env });

Підтримувані пакети та їх секрети ​

ПакетСекрети з env
@happ-integ/llmOPENAI_API_KEY, CLAUDE_API_KEY, GEMINI_API_KEY, GROQ_API_KEY
@happ-integ/openaiOPENAI_API_KEY
@happ-integ/vectorQDRANT_URL, QDRANT_API_KEY, PINECONE_API_KEY, WEAVIATE_URL, WEAVIATE_API_KEY
@happ-integ/qdrantQDRANT_URL, QDRANT_API_KEY
@happ-integ/neonDATABASE_URL
@happ-integ/happ-voiceVOICE_ASSISTANT_URL, VOICE_ASSISTANT_SECRET
@happ-integ/platform-apiPLATFORM_API_URL, PLATFORM_ACCESS_TOKEN

Пріоритет конфігурації ​

Пакети використовують такий пріоритет для отримання секретів:

1. Явно переданий параметр (apiKey, url тощо)
2. Значення з env binding (env.OPENAI_API_KEY тощо)

Примітка: process.env не підтримується — пакети працюють тільки в Cloudflare Workers де секрети доступні через env binding.


Кто может звать воркер ​

Два уровня, оба обязательны.

1. Воркеры не публикуются наружу. В templates/wrangler.integration.toml.tpl стоит workers_dev = false. Без него у каждой интеграции живёт публичный integ-<name>-<env>.<subdomain>.workers.dev, который обходит gateway целиком: ни проверки токена, ни рейт-лимита, ни трейса. Единственный вход — gateway через Service Binding.

2. Хендлер знает, пришёл ли запрос через gateway. Gateway удаляет присланный клиентом x-integ-internal и ставит свой из INTEG_INTERNAL_SECRET. Воркер сверяет его константным по времени сравнением.

typescript
export const myHandler = defineHandler({
  name: "my-handler",
  endpoint: "POST /webhook/my-handler",
  // auth: "internal" — по умолчанию. Новый маршрут закрыт, пока не откроют явно.
  async handler(ctx) { ... },
});

export const providerCallback = defineHandler({
  name: "provider-callback",
  endpoint: "POST /webhook/provider-callback",
  auth: "public", // зовут снаружи, минуя gateway
  async handler(ctx) { ... },
});

Раскатка в два шага. INTEG_INTERNAL_AUTH в Doppler:

ЗначениеПоведение
не задано / logзапрос пропускается, факт «пришло без заголовка» пишется в лог как handler_unauthenticated
enforce401

Начинать всегда с log: пометить внутренним маршрут, который реально зовёт внешний сервис, — значит молча сломать клиента, а 119 маршрутов по именам не классифицируются надёжно. Сутки в log, потом по собранным handler_unauthenticated проставить auth: "public" тем, кому надо, и переключить на enforce.

Если INTEG_INTERNAL_SECRET не задан, проверка не выполняется вовсе — воркер, задеплоенный до появления секрета, продолжает работать. Отсутствие секрета в проде означает, что второй уровень выключен.

/setup и /cron/trigger по-прежнему закрыты ACCESS_TOKEN на самом gateway. /health объявлен public.


Security Best Practices ​

  1. Никогда не коммитить .env, .dev.vars (добавлены в .gitignore)
  2. CRYPTO_KEY и CRYPTO_SALT должны быть разными для dev и prod
  3. Ротация ключей — см. «Ротация CRYPTO_KEY»
  4. Минимальные права — каждая интеграция видит только свои credentials
  5. Аудит — D1 хранит created_at, updated_at для каждого секрета

Ротация CRYPTO_KEY ​

Без простоя: пока идёт перешифровка, воркеры и integ-api читают и новым, и старым ключом.

  1. В Doppler integ-core и integ-api (одно окружение): старые значения → CRYPTO_KEY_PREVIOUS/CRYPTO_SALT_PREVIOUS, новые → CRYPTO_KEY/CRYPTO_SALT.
  2. Секреты в воркеры: tsx scripts/sync-cf-secrets.ts <dev|production>; integ-api — пересоздать контейнер.
  3. pnpm cli creds:rekey --env <dev|production> --dry-run, затем без --dry-run. Повторный запуск безопасен: строка обновляется, только если не менялась с момента чтения.
  4. Через сутки без ошибок Failed to decrypt удалить *_PREVIOUS из Doppler и воркеров.

Связанные документы ​