diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..5f31b24 --- /dev/null +++ b/.env.example @@ -0,0 +1,74 @@ +# ───────────────────────────────────────────────────────────────────── +# Production .env template для CryptoWallet API. +# Скопируй: cp .env.example .env && chmod 600 .env && nano .env +# ───────────────────────────────────────────────────────────────────── + +# ─── HashiCorp Vault (для master-key, JWT public keys, CSRF secret) ── +VAULT_ADDR=https://vault.your-domain.com +VAULT_ROLE_ID=00000000-0000-0000-0000-000000000000 +VAULT_SECRET_ID=00000000-0000-0000-0000-000000000000 +VAULT_MOUNT_POINT=dev-secrets +VAULT_SECRET_PATH=crypto/master +VAULT_JWT_KID_PATH=jwt/kid +VAULT_JWT_KIDS_PREFIX=jwt/kids/ +VAULT_CSRF_PATH=csrf +VAULT_CRYPTO_KEY_PATH=crypto/master + +# ─── JWT (приём от bitok external issuer) ──────────────────────────── +JWT_ALGORITHM=RS256 +JWT_ISSUER=bitok +JWT_AUDIENCE=cryptowallet-api + +# ─── API runtime ───────────────────────────────────────────────────── +API_PORT=3001 +LOG_LEVEL=info + +# CORS — comma-separated list разрешённых origins (фронтенд hosts) +CORS_ORIGINS=https://app.your-domain.com,https://admin.your-domain.com +CORS_ALLOW_CREDENTIALS=true + +# ─── Postgres (для docker-compose) ─────────────────────────────────── +# Эти переменные используются compose'ом для создания/connect к Postgres контейнеру. +# Если оператор использует external managed Postgres — игнорь POSTGRES_* и впиши +# connection string в DATABASE_URL ниже. +POSTGRES_USER=cryptowallet +POSTGRES_PASSWORD=__GENERATE_STRONG_PASSWORD__ +POSTGRES_DB=cryptowallet + +# Если используешь managed/external Postgres — раскомментируй и заполни: +# DATABASE_URL=postgres://user:pass@host:5432/dbname + +# ─── KeyDB / Redis (idempotency cache + NearIntents asset map cache) ─ +# REDIS_HOST=keydb имя service в compose — НЕ меняй если работаешь через compose +REDIS_HOST=keydb +REDIS_PORT=6379 +REDIS_PASSWORD=__GENERATE_STRONG_PASSWORD__ +REDIS_DB=0 + +# ─── Внешние API ───────────────────────────────────────────────────── +# CoinGecko — для prices/dynamics (без ключа работает с rate limits) +COINGECKO_API_KEY= + +# Jupiter — для SOL custodial swap (без ключа = lower rate limits) +JUPITER_API_KEY= + +# Jupiter referral — если хочешь чтобы SOL swap fees шли через Jupiter feeAccount. +# Сейчас у нас атомарный 0.7% fee atomic в swap-orchestrator (на APP_FEE_WALLET_SOL), +# referral отдельный механизм. Можно оставить пустым. +JUPITER_REFERRAL_ACCOUNT= + +# TronGrid — для TRX queries (без ключа 3 req/sec, с ключом 100/sec) +# Получить: https://www.trongrid.io/dashboard +TRON_API_KEY= + +# Outbound HTTP proxy (опц.) — если хотите чтобы Jupiter/Relay/NearIntents calls +# шли через proxy (rotation IP для rate limits). Формат: http://user:pass@host:port +OUTBOUND_PROXY_URL= + +# ─── App fee wallets ───────────────────────────────────────────────── +# Эти адреса HARDCODED в backend (apps/api/src/lib/app-fee.ts), НЕ настраиваются +# через env. Если нужно изменить — code review + rebuild. +# EVM (ETH+BSC): 0xeb9fbf0d137ef5ea7b9959044c2ed44ec1206c68 +# SOL: DQkQegoX698XkcXZ6VX9P1qUpbV64Sgjz1BCPFgfWpjD +# TRX: TRwpFjnfMBe4aDJbHYEqeUVCG1auF8wFXP +# BTC: — (not collectable) diff --git a/README.md b/README.md index d0bcb7b..81bcacd 100644 --- a/README.md +++ b/README.md @@ -28,28 +28,55 @@ vault kv put dev-secrets/jwt/kids/ \ ⚠️ **Master-key менять нельзя** — все existing `encrypted_mnemonic` станут нерасшифровываемыми. API на старте делает self-test: пытается декриптить любой существующий mnemonic и фейлится если ключ не подошёл. -## Deploy +## Deploy (через docker compose) ```bash -# Залить bundle на сервер +# 1. Залить bundle на сервер scp -P 2222 -r deployserver/ server@:~/cryptowallet/ -# На сервере: заполнить .env, поднять +# 2. На сервере: заполнить .env ssh server@ -p 2222 -cd ~/cryptowallet +cd ~/cryptowallet/deployserver cp .env.example .env chmod 600 .env -nano .env # заполни VAULT_*, JWT_*, CORS_ORIGINS -./start.sh +nano .env # заполни VAULT_*, JWT_*, REDIS_PASSWORD, POSTGRES_PASSWORD, CORS_ORIGINS + +# 3. Поднять stack (api + postgres + keydb) +docker compose up -d --build + +# 4. Проверить +docker compose ps +docker compose logs -f api +curl http://localhost:3001/api/health ``` -В `.env` **обязательны**: `VAULT_ADDR`, `VAULT_ROLE_ID`, `VAULT_SECRET_ID`, `JWT_ISSUER=bitok`, `JWT_AUDIENCE`, `CORS_ORIGINS`. +В `.env` **обязательны**: `VAULT_ADDR`, `VAULT_ROLE_ID`, `VAULT_SECRET_ID`, `JWT_ISSUER=bitok`, `JWT_AUDIENCE`, `CORS_ORIGINS`, `POSTGRES_PASSWORD`, `REDIS_PASSWORD`. + +**Что в compose:** +- `api` — наш Node API из multi-stage Dockerfile (read-only fs, uid 1001, port 127.0.0.1:3001) +- `postgres` — хранилище (internal only, без exposed ports) +- `keydb` — Redis-compatible для idempotency + asset map cache (internal only) + +**Чего НЕТ в compose** (production не использует — оператор настраивает отдельно): +- HashiCorp Vault — production использует HA cluster (URL в `VAULT_ADDR`) +- JWT issuer — production принимает JWT от bitok external service +- Web UI — frontend деплоится отдельно (Vercel / nginx / CDN) +- Nginx reverse proxy + TLS — оператор сам ставит перед `127.0.0.1:3001` + +**Если используешь managed Postgres** (e.g. AWS RDS): +- Раскомментируй `DATABASE_URL` в `.env` +- Закомментируй `postgres` service в `docker-compose.yml` + убери `depends_on.postgres` из `api` ## Update / Rebuild ```bash -scp -P 2222 -r deployserver/apps server@:~/cryptowallet/ -ssh server@ -p 2222 'cd cryptowallet && docker compose up -d --build' +# Только app (postgres/keydb остаются — данные не теряются) +scp -P 2222 -r deployserver/apps deployserver/Dockerfile deployserver/docker-compose.yml \ + server@:~/cryptowallet/deployserver/ +ssh server@ -p 2222 'cd cryptowallet/deployserver && docker compose up -d --build api' + +# Полный rebuild +ssh server@ -p 2222 'cd cryptowallet/deployserver && docker compose up -d --build --force-recreate' ``` ## Endpoints diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e0fbc9f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,161 @@ +# ───────────────────────────────────────────────────────────────────── +# Production docker-compose для CryptoWallet API. +# +# Что в этом stack: +# - api — наш Node.js API (multi-stage build из ./Dockerfile) +# - postgres — хранилище encrypted_mnemonic + audit_log +# - keydb — Redis-compatible для idempotency cache + asset map cache +# +# Что НЕ включено (production не использует — оператор настраивает отдельно): +# - Vault — production использует HashiCorp Vault HA cluster (VAULT_ADDR в .env) +# - JWT signer— production принимает JWT от bitok (внешний сервис, JWT_ISSUER в .env) +# - web-ui — фронтенд деплоится отдельно (Vercel / nginx CDN / etc.) +# - vault-init — production Vault уже инициализирован оператором (см. README pre-deploy) +# +# Security hardening: +# - api запускается под uid 1001, read-only fs, cap_drop ALL, no-new-privileges +# - api порт 3001 биндится на 127.0.0.1 (наружу через nginx + TLS, оператор) +# - postgres + keydb без exposed ports (только internal docker network) +# - .env с secrets — 600 permission, не в репо +# +# Usage: +# cp .env.example .env # заполнить все VAULT_*, JWT_*, REDIS_PASSWORD, CORS_ORIGINS, etc. +# chmod 600 .env +# docker compose up -d --build +# docker compose logs -f api +# ───────────────────────────────────────────────────────────────────── + +services: + api: + build: + # Context = parent dir (мы внутри deployserver/, апи берёт apps/api/ из родителя) + context: .. + dockerfile: deployserver/Dockerfile + image: cryptowallet-api:latest + container_name: cw-api + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + keydb: + condition: service_healthy + env_file: + - .env + environment: + # Override DB/Redis host для docker network (если в .env стоят prod hosts — + # внутри compose их replace на internal service names) + POSTGRES_HOST: postgres + POSTGRES_PORT: "5432" + REDIS_HOST: keydb + REDIS_PORT: "6379" + ports: + # Loopback only — наружу через nginx reverse proxy + TLS + - "127.0.0.1:3001:3001" + read_only: true + tmpfs: + - /tmp + cap_drop: + - ALL + security_opt: + - no-new-privileges:true + deploy: + resources: + limits: + memory: 512M + cpus: "1.0" + pids: 200 + reservations: + memory: 256M + healthcheck: + test: ["CMD", "wget", "-qO-", "http://localhost:3001/api/health"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 15s + logging: + driver: json-file + options: + max-size: "10m" + max-file: "5" + + postgres: + image: postgres:16-alpine + container_name: cw-postgres + restart: unless-stopped + environment: + POSTGRES_USER: ${POSTGRES_USER:?POSTGRES_USER required} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD required} + POSTGRES_DB: ${POSTGRES_DB:-cryptowallet} + volumes: + - pgdata:/var/lib/postgresql/data + # Bind-mount schema.sql если оператор хочет авто-init на свежей БД. + # На existing — pg ignores 01-schema.sql (initdb запускается только если /var/lib/postgresql/data пуст). + # См. README — лучше прогонять schema руками: psql -f cryptowallet-schema.sql + # - ./cryptowallet-schema.sql:/docker-entrypoint-initdb.d/01-schema.sql:ro + # Internal only — никаких ports на host + expose: + - "5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB:-cryptowallet}"] + interval: 10s + timeout: 3s + retries: 5 + logging: + driver: json-file + options: + max-size: "10m" + max-file: "5" + + keydb: + image: eqalpha/keydb:alpine + container_name: cw-keydb + restart: unless-stopped + volumes: + - keydb_data:/data + expose: + - "6379" + command: + - keydb-server + - --requirepass + - "${REDIS_PASSWORD:?REDIS_PASSWORD required}" + - --dir + - /data + - --appendonly + - "yes" + - --appendfsync + - everysec + - --save + - "900" + - "1" + - --save + - "300" + - "10" + - --save + - "60" + - "10000" + - --maxmemory + - "256mb" + - --maxmemory-policy + - "allkeys-lru" + healthcheck: + test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"] + interval: 10s + timeout: 3s + retries: 5 + deploy: + resources: + limits: + memory: 320M + logging: + driver: json-file + options: + max-size: "10m" + max-file: "5" + +volumes: + pgdata: + keydb_data: + +networks: + default: + driver: bridge