Kurulum, abonelik, teslim sağlığı ve GDPR (customer.redact) yaşam döngüsü event'leri, packet.created ile AYNI webhook ucuna düşer. Bunlara abone OLMAZSIN — bağlı (connected) kuruluma otomatik gönderilir (customer.redact yalnız PII scope'lu + consent vermiş kurulumlara); ödeme şartı yoktur. İmzalıdır (aynı HMAC) ve başarısız teslimlerde retry edilir.
Subscribable event (packet.created…) | Lifecycle (app.installed…) | |
|---|---|---|
| Abonelik | events[] + events:subscribe gerekir | Yok — her zaman gönderilir |
| Ödeme şartı | — | Yok — connected yeter |
| Uç | webhookUrl | Aynı webhookUrl |
| İmza | install webhookSecret (HMAC) | Aynı |
| Retry | var | var |
| Kime | bağlı kurulum | Yalnız connected kurulum (secret sahibi) |
Tek uç hem subscribable event'leri hem lifecycle'ı alır; type ile ayırt edersin. Katalogda ayrı bir lifecycleEvents listesi vardır (GET /plugin-meta/catalog).
| type | Ne zaman | data | Geliştirici ne yapar |
|---|---|---|---|
| app.installed | OAuth connect tamamlandı (kurulum bağlandı) | { version, scopes[] } | Provision — tenant kaydını oluştur/etkinleştir |
| subscription.activated | Abonelik aktif/trial oldu (aktivasyon + her yenileme) ⚠️ birden çok kez | { interval, currentPeriodEnd } | Premium provision — idempotent (tenantId durumuna göre) |
| subscription.past_due | Ödeme başarısız (gecikmiş) | {} (boş) | Degrade — premium özellikleri kısıtla |
| subscription.canceled | Abonelik iptal edildi | { reason? } | Deprovision — premium yetkiyi kapat (reason=plugin_now_free ise ücretsiz sürdür) |
| app.uninstalledkritik | Kurulum kaldırıldı | {} (boş) | Veri sil (GDPR/temizlik) |
| customer.redactkritik | Tenant bir müşteriyi sildi (GDPR/KVKK) | { customerId, deletedAt } | Bu customerId'ye ait TÜM PII'yi sil (zorunlu) |
| delivery.degraded | Circuit breaker açıldı (endpoint art arda başarısız) — 6 saatte bir | { consecutiveDead, windowFail, windowOk, openedAt, hint } | Endpoint sağlığını düzelt — teslimler geçici atlanıyor |
| delivery.restored | Breaker kapandı / yeniden etkinleştirildi — saatte bir (debounce) | { restoredAt } | Bilgi — teslimler normale döndü |
| delivery.disabledkritik | Breaker 72 saat kesintisiz açık → webhook teslimi KALICI durduruldu | { openedAt, disabledAt, hint } | Endpoint'i düzelt + yeniden etkinleştir (portal / test-emit) |
| delivery.throttled | Teslim hacmi cap üstü, bazı teslimler düşürüldü — 6 saatte bir | { cls, count, eventType, hint } | Bilgi — hacmi düşür / topluyu sadeleştir |
Her tipin kendi detay sayfası var (gerçek payload + alanlar + en iyi pratik) — type'a tıkla.
app.uninstalled kritiktir: tenant kurulumu kaldırınca o tenant'a ait tüm veriyi sil (GDPR/temizlik). Bu event tek temizlik sinyalidir.customer.redact zorunludur (GDPR/KVKK): tenant bir müşteriyi silince o customerId'ye ait tüm PII'yi sil — eklenti o tenant'ta pasif/borçlu olsa bile gelir. Critical sınıf (breaker/cap'ten muaf).delivery.degraded/restored/disabled/throttled endpoint'inin teslim durumunu bildirir (circuit breaker / cap). Kurallar: Teslim Sağlığı & Politikası.subscription.activated birden çok kez gelir: aynı abonelik için checkout + subscription.created + subscription.updated + her yenileme — her biri farklı id ile (retry değil). id ile dedup bunları birleştirmez; bu yüzden provision'ı event sayısına değil tenantId durumuna göre idempotent yap.kur → OAuth connect ────────────────────────► [app.installed] → provision
abonelik aktif/trial (+ her yenileme) ───► [subscription.activated] → premium aç (idempotent)*
├─ ödeme başarısız ─────────────────► [subscription.past_due] → degrade (kıs)
└─ iptal ───────────────────────────► [subscription.canceled] → deprovision
olay (paket) — yalnız aktif + ödenmiş ───► packet.created → işle
kaldır ─────────────────────────────────────► [app.uninstalled] → veri SİL (GDPR)
* activated aynı abonelik için birden çok kez (farklı id) gelir → tenantId durumuna göre idempotentDurum makinesi: app.installed başlangıç; ödeme durumu activated ⇄ past_duearasında gidip gelebilir; canceled premium'u kapatır ama kurulum durur;app.uninstalled terminal (veri silinir).
webhookUrl'e düşer. switch (type) ile ayır; tanımadığın type'a güvenli (no-op + 200) yanıt ver.id'leri kalıcı sakla (örn. TTL'li tablo). Retry'da aynı id tekrar gelir → ilk satırda yut. Not: subscription.activated aynı abonelik için farklı id'lerle birden çok kez gelir — id dedup'u bunu engellemez; o aksiyonu ayrıca durum-temelli (tenantId) idempotent kur.installed → active ⇄ past_due → canceled → uninstalled durumu tut; her event durumu ilerletsin. Geçişleri tersine çevrilebilir kur (past_due→active).interval'a tek başına güvenme.past_due/canceled veriyi KORUR (geri dönüş olabilir); app.uninstalled tek silme sinyali.POST {webhookUrl} // packet.created ile AYNI uç
X-Restomenum-Signature: t=<unixSec>,v1=<HMAC_SHA256(webhookSecret,"<t>.<rawBody>")>
{
"id": "evt_…", // idempotency anahtarı
"type": "app.installed", // lifecycle tipi (aşağıdaki tablo) — event'ten BUNUNLA ayırt et
"version": "1",
"tenantId": "…",
"occurredAt": 1780000000000,
"data": { } // tipe göre (çoğu boş; app.installed & subscription.activated dolu)
}// /webhook — TEK uç hem event hem lifecycle alır. type ile dallan.
app.post('/webhook', express.raw({ type: '*/*' }), async (req, res) => {
if (!verifySignature(webhookSecret, req.body, req.get('X-Restomenum-Signature'))) return res.sendStatus(401);
const e = JSON.parse(req.body.toString('utf8'));
if (await seen(e.id)) return res.sendStatus(200); // idempotency: aynı id'yi yut
await enqueue(e); // ağır işi kuyruğa al, HEMEN 200 dön (yoksa retry tetiklenir)
res.sendStatus(200);
});
// worker (async) — type'a göre durum makinesini ilerlet
async function handleLifecycle(e) {
switch (e.type) {
case 'app.installed': await provision(e.tenantId, e.data.scopes, e.data.version); break;
case 'subscription.activated': await enablePremium(e.tenantId, e.data.currentPeriodEnd); break; // idempotent — birden çok kez (farklı id) gelir
case 'subscription.past_due': await degrade(e.tenantId); break; // sil DEĞİL, kıs
case 'subscription.canceled': await deprovision(e.tenantId); break; // yetki kapat
case 'app.uninstalled': await purgeTenant(e.tenantId); break; // GDPR — tenant verisi sil
case 'customer.redact': await redactCustomer(e.tenantId, e.data.customerId); break; // GDPR — müşteri PII sil (ZORUNLU)
case 'delivery.disabled': await alertOps(e); break; // teslim kalıcı durdu → endpoint'i düzelt
default: await handleEvent(e); break; // packet.created vb. (delivery.degraded/restored/throttled bilgi)
}
}webhookSecret, ±5 dk pencere; geçersizse 401 dön, işleme.tenantId'yi doğrula — gelen veriyi yalnız o tenant'ın kaydına uygula, başka tenant'a sızdırma.id gelir → dedup; özellikle provision/deprovision/purge'ü tekrarlama.webhookSecret yalnız doğrulama için; gövdeyi loglarken PII/secret maskele.