قائمة التحقق قبل الإطلاق
راجع هذه القائمة قبل الانتقال إلى بيئة الإنتاج. كل بند يحيل إلى القسم المقابل أدناه.
تكوين حماية إعادة التشغيل للنسخ المتعددة
المحوّل الافتراضي يعمل في الذاكرة فقط. إذا كنت تشغّل أكثر من عملية واحدة (عمال Gunicorn، PM2 cluster، Kubernetes)، فاضبط SUPABASE_URL + SUPABASE_ANON_KEY أو استخدم RedisReplayAdapter. التفاصيل → متغيرات البيئة في مدير الأسرار
لا تُدرج مفاتيح API مباشرةً في الكود. استخدم .env محلياً ومدير الأسرار في الإنتاج. التفاصيل → وجود نقطة نهاية للتحقق من الصحة
يجب ألا تُطلق عمليات مراقبة النظام إنشاء فواتير. أضف مسار /health قبل مساراتك المدفوعة. التفاصيل → استجابات الأخطاء لا تكشف التفاصيل الداخلية
التقط أخطاء المزود وأعد استجابة 503 نظيفة — لا تعيد تتبع المكدس. التفاصيل → السعر مقصود
يجب أن يعكس priceSats قيمة حقيقية. بسعر 1 sat ≈ 0.0006،فإن100sats≈0.06 لنقطة نهاية مميزة أمر معقول. لا تضبطه على 0 بالخطأ. مراقبة وقت التشغيل على نقطة نهاية الفاتورة
راقب استجابة 402 (وهي استجابة طبيعية وليست خطأً). تدعم أدوات مثل UptimeRobot توقعات رموز الحالة المخصصة.
تحديد معدل إنشاء الفواتير
كل طلب غير مصادق عليه يُنشئ فاتورة Lightning. بدون تحديد المعدل، يمكن لمهاجم استنفاد حصة الفواتير لدى مزودك مجاناً. أضف express-rate-limit قبل النشر العلني. التفاصيل →
هام: حماية إعادة التشغيل في الإنتاج
المحوّل الافتراضي لحماية إعادة التشغيل يعمل في الذاكرة فقط — يُعاد تعيينه عند كل إعادة تشغيل للعملية ولا يعمل عبر نسخ متعددة من الخادم. في بيئة الإنتاج مع أكثر من عملية واحدة (عمال Gunicorn، حاويات Kubernetes، PM2 cluster)، يمكن قبول نفس preimage مرتين.الحل: اضبط SUPABASE_URL + SUPABASE_ANON_KEY في بيئتك. سيستخدم الوسيط تلقائياً Supabase كمخزن لإعادة التشغيل، وهو مشترك عبر جميع النسخ.لـ Redis: مرر RedisReplayAdapter صراحةً (راجع TypeScript SDK أو Python SDK).
متغيرات البيئة
لا تُدرج المفاتيح مباشرةً في الكود. استخدم دائماً متغيرات البيئة:
# .env (never commit this)
BLINK_API_KEY=blink_xxx
BLINK_WALLET_ID=your-wallet-id
SUPABASE_URL=https://xxx.supabase.co
SUPABASE_ANON_KEY=sb_publishable_xxx # safe to expose to clients
SUPABASE_SERVICE_KEY=sb_secret_xxx # server-side only — never expose to clients
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) => {
// Log successful L402 payments (after middleware passes)
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، بدون تأخير في البداية). يُعاد تعيين مخزن إعادة التشغيل في الذاكرة مع كل عازل — لواجهات برمجة التطبيقات ذات الحركة العالية، استخدم 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) {
// Token invalid, expired, or already used — let the middleware handle it
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";
// Limit invoice creation: 30 unauthenticated requests/minute per IP
const invoiceLimit = rateLimit({
windowMs: 60_000,
max: 30,
// Only apply to requests that don't already carry a valid L402 header
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() }));
// Paid endpoint
app.get('/api/data', l402({ priceSats: 10, lightning: blink }), handler);
المراقبة
المقاييس الرئيسية للتتبع:
- معدل استجابة 402 — المعدل الأساسي الصحي مرتفع (معظم المُستدعين بحاجة إلى الدفع)
- معدل التحقق من المدفوعات — نسبة المكالمات المدفوعة مقارنةً بغير المدفوعة
- زمن استجابة المزود — يجب أن يكون
blink.createInvoice() < 500ms
- محاولات إعادة التشغيل — الارتفاع المفاجئ يشير إلى هجمات إعادة استخدام الرمز
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) يستدعي واجهة برمجة تطبيقات مزود Lightning الخاص بك — أضف ذاكرة تخزين مؤقت إذا كنت تتوقع ضرب نفس نقطة النهاية بشكل متكرر قبل الدفع
- مخزن إعادة التشغيل هو
Set في الذاكرة — لعمليات النشر متعددة النسخ، استبدله بـ Redis
// Redis replay store example
import { Redis } from 'ioredis';
const redis = new Redis(process.env.REDIS_URL!);
// Pass custom replay function to 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);