Skip to main content

Контрольный список перед запуском

Выполните все пункты перед выходом в production. Каждый пункт ссылается на соответствующий раздел ниже.
1

Защита от повторного воспроизведения настроена для нескольких экземпляров

Адаптер по умолчанию хранит данные только в памяти. Если вы запускаете более одного процесса (воркеры Gunicorn, кластер PM2, Kubernetes), задайте SUPABASE_URL + SUPABASE_ANON_KEY или используйте RedisReplayAdapter. Подробнее →
2

Переменные окружения хранятся в менеджере секретов

Никогда не вставляйте API-ключи напрямую в код. Используйте .env локально, менеджер секретов — в production. Подробнее →
3

Эндпоинт проверки работоспособности существует

Запросы мониторинга не должны инициировать создание инвойсов. Добавьте маршрут /health перед платными маршрутами. Подробнее →
4

Ответы об ошибках не раскрывают внутренние данные

Перехватывайте ошибки провайдера и возвращайте чистый 503 — без трассировки стека. Подробнее →
5

Цена задана осознанно

priceSats должен отражать реальную ценность. При курсе 1 sat ≈ 0.0006,100sats0.0006, 100 sats ≈ 0.06 за premium-эндпоинт — разумная цена. Не устанавливайте значение 0 по ошибке.
6

Мониторинг доступности эндпоинта инвойсов

Отслеживайте ответ 402 (это обычный ответ, не ошибка). Инструменты вроде UptimeRobot поддерживают ожидаемые пользовательские коды статуса.
7

Ограничение частоты запросов при создании инвойсов

Каждый неаутентифицированный запрос создаёт Lightning-инвойс. Без ограничения частоты злоумышленник может бесплатно исчерпать квоту инвойсов вашего провайдера. Добавьте express-rate-limit перед публичным развёртыванием. Подробнее →

Важно: защита от повторного воспроизведения в production

Адаптер повторного воспроизведения по умолчанию хранит данные только в памяти — он сбрасывается при каждом перезапуске процесса и не работает в нескольких экземплярах сервера. В production с более чем одним процессом (воркеры Gunicorn, поды Kubernetes, кластер PM2) один и тот же preimage может быть принят дважды.Решение: Задайте SUPABASE_URL + SUPABASE_ANON_KEY в окружении. Middleware автоматически использует Supabase как хранилище повторного воспроизведения, которое является общим для всех экземпляров.Для Redis: передайте RedisReplayAdapter явно (см. TypeScript SDK или Python SDK).

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

Никогда не вставляйте ключи напрямую в код. Всегда используйте переменные окружения:
# .env (никогда не коммитьте этот файл)
BLINK_API_KEY=blink_xxx
BLINK_WALLET_ID=your-wallet-id
SUPABASE_URL=https://xxx.supabase.co
SUPABASE_ANON_KEY=sb_publishable_xxx    # безопасно передавать клиентам
SUPABASE_SERVICE_KEY=sb_secret_xxx      # только на стороне сервера — никогда не передавайте клиентам
import 'dotenv/config';
import { BlinkProvider } from 'l402-kit';

const blink = new BlinkProvider(
  process.env.BLINK_API_KEY!,
  process.env.BLINK_WALLET_ID!,
);

Журналирование платежей (Supabase)

Записывайте каждый платёж в Supabase для панели управления расширения VS Code и аналитики:
import { createClient } from '@supabase/supabase-js';

const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_ANON_KEY!,
);

app.use(async (req, res, next) => {
  const original = res.json.bind(res);
  res.json = (body) => {
    // Записываем успешные L402-платежи (после прохождения middleware)
    if (req.l402Paid && req.l402Preimage) {
      supabase.from('payments').insert({
        endpoint: req.path,
        preimage: req.l402Preimage,
        amount_sats: req.l402AmountSats,
      }).then(() => {});
    }
    return original(body);
  };
  next();
});

Развёртывание на Cloudflare Workers

l402-kit API работает на Cloudflare Workers (изоляты V8, без холодного старта). Хранилище повторного воспроизведения в памяти сбрасывается для каждого изолята — для высоконагруженных API используйте Cloudflare KV или Durable Objects для защиты от повторного воспроизведения.
// api/data.ts
import { l402 } from 'l402-kit';
import { BlinkProvider } from 'l402-kit';

const blink = new BlinkProvider(
  process.env.BLINK_API_KEY!,
  process.env.BLINK_WALLET_ID!,
);

export default async function handler(req, res) {
  await new Promise<void>((resolve, reject) => {
    l402({ priceSats: 10, lightning: blink })(req, res, (err) => {
      if (err) reject(err); else resolve();
    });
  });
  res.json({ data: 'premium content' });
}

Docker

FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY dist ./dist
ENV NODE_ENV=production
EXPOSE 3000
CMD ["node", "dist/server.js"]
# docker-compose.yml
services:
  api:
    build: .
    environment:
      BLINK_API_KEY: ${BLINK_API_KEY}
      BLINK_WALLET_ID: ${BLINK_WALLET_ID}
    ports:
      - "3000:3000"

Удаление пользовательских данных

Предоставьте эндпоинт удаления, чтобы пользователи могли безвозвратно удалить свои данные. Бэкенд l402-kit включает /api/delete-data из коробки:
POST /api/delete-data
Content-Type: application/json

{ "lightningAddress": "user@blink.sv" }
// 200 OK
{ "deleted": { "payments": 42, "proAccess": true } }
Расширение VS Code отображает это как панель Danger Zone в нижней части панели управления — пользователи должны ввести свой Lightning-адрес для подтверждения, после чего вся история платежей и доступ Pro удаляются. Операция использует сервисный ключ Supabase на стороне сервера; anon-ключ не имеет прав на DELETE.

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

Перехватывайте ошибки провайдера до того, как они проявятся как неформатированные 500:
import { l402, L402Error } from 'l402-kit';

app.get('/api/data', l402({ priceSats: 10, lightning }), async (req, res) => {
  try {
    res.json({ data: await fetchData() });
  } catch (err) {
    if (err instanceof L402Error) {
      // Токен недействителен, истёк или уже использован — позвольте middleware обработать это
      return res.status(err.status).json({ error: err.code });
    }
    console.error('[api/data]', err);
    res.status(503).json({ error: 'Service temporarily unavailable' });
  }
});
Практические правила:
  • Никогда не возвращайте клиентам трассировку стека — записывайте её на стороне сервера.
  • Таймауты провайдера (503) являются временными — безопасно повторять с задержкой.
  • Ошибки токена (401) никогда не являются временными — не повторяйте запрос автоматически, требуйте новый инвойс.
  • Ошибки ограничения частоты (429) — передавайте поле retryAfter вызывающей стороне.
Полный список структурированных кодов ошибок см. в справочнике ошибок.

Ограничение частоты запросов

Каждый неаутентифицированный запрос вызывает createInvoice() у вашего Lightning-провайдера. Без ограничения частоты любой может перегрузить ваш эндпоинт и бесплатно исчерпать квоту API провайдера — не заплатив ни одного sat. Добавьте express-rate-limit перед вашими L402-маршрутами:
npm install express-rate-limit
import rateLimit from "express-rate-limit";
import { l402 } from "l402-kit";

// Ограничение создания инвойсов: 30 неаутентифицированных запросов/минуту на IP
const invoiceLimit = rateLimit({
  windowMs: 60_000,
  max: 30,
  // Применяется только к запросам без действующего заголовка L402
  skip: (req) => req.headers.authorization?.startsWith("L402 ") ?? false,
  message: { error: "Too many requests — slow down" },
});

app.get("/api/premium", invoiceLimit, l402({ priceSats: 10, lightning }), handler);
Клиенты, уже совершившие оплату, пропускаются функцией skip — ограничение применяется только к неаутентифицированным вызовам, инициирующим создание нового инвойса. Законопослушные плательщики никогда не получают ограничений.
Для FastAPI:
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded

limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)

@app.get("/api/premium")
@limiter.limit("30/minute")
@l402_required(price_sats=10, lightning=lightning)
async def premium(request: Request):
    return {"data": "paid content"}

Эндпоинт проверки работоспособности

Всегда добавляйте бесплатную проверку работоспособности, чтобы инструменты мониторинга не инициировали создание инвойсов:
app.get('/health', (req, res) => res.json({ ok: true, ts: Date.now() }));

// Платный эндпоинт
app.get('/api/data', l402({ priceSats: 10, lightning: blink }), handler);

Мониторинг

Ключевые метрики для отслеживания:
  • Частота ответов 402 — здоровый базовый уровень высокий (большинство вызывающих должны платить)
  • Доля верифицированных платежей — соотношение оплаченных и неоплаченных вызовов
  • Задержка провайдераblink.createInvoice() должен выполняться менее чем за 500 мс
  • Попытки повторного воспроизведения — всплеск указывает на атаки с повторным использованием токена
app.use((req, res, next) => {
  const start = Date.now();
  res.on('finish', () => {
    console.log(JSON.stringify({
      method: req.method,
      path: req.path,
      status: res.statusCode,
      ms: Date.now() - start,
      paid: res.statusCode !== 402,
    }));
  });
  next();
});

Производительность

  • Верификация токена выполняется за O(1) — чистая криптография, без БД, без сети
  • Создание инвойса (путь 402) обращается к API вашего Lightning-провайдера — добавьте кэш, если ожидаете повторные обращения к одному эндпоинту до совершения оплаты
  • Хранилище повторного воспроизведения — Set в памяти — для многоэкземплярных развёртываний замените на Redis
// Пример хранилища повторного воспроизведения на Redis
import { Redis } from 'ioredis';
const redis = new Redis(process.env.REDIS_URL!);

// Передаём пользовательскую функцию повторного воспроизведения в middleware
app.get('/api', l402({
  priceSats: 10,
  lightning: blink,
  checkReplay: async (preimage) => {
    const set = await redis.set(`l402:${preimage}`, '1', 'NX', 'EX', 86400);
    return set === 'OK';
  },
}), handler);