Entegrasyon dokümanı
Hook entegrasyonu
İşlem öncesinde kendi sisteminizden onay alın, işlem sonrasında bildirim gönderin. Bu rehber hook isteğini, yanıt biçimini, imza doğrulamasını ve hata davranışını açıklar.
İki hook tipi ve zincirdeki yerleri
Doğrulama hook’u işlemi kontrol eder. Bildirim hook’u gerçekleşen olayı iletir.
Doğrulama (validate)
Operasyon uygulanmadan önce sorulur ve durdurabilir. Yanıtınız planın akıbetini belirler.
- Anı: plan kurulurken, onaya basıldığı an, ya da ikisi.
- Yanıtınız:
allow/deny/elevate. - Ulaşılamazsa:
onFailurebelirler; varsayılandeny.
Bildirim (notify)
Operasyon uygulandıktan sonra haber verilir ve hiçbir şeyi durdurmaz. Kullanıcı sonucu çoktan almıştır.
- Anı: apply bittikten sonra, kalıcı bir kuyruktan.
- Yanıtınız: herhangi bir
2xx. Gövdeye bakılmaz. - Ulaşılamazsa: tekrar denenir, sonra ölü kuyruğa düşer.
Bir yazma isteğinin izlediği yol (hook'lar kalın):
istek → dry-run → guardrail → HOOK (stage: plan)
│
├─ deny → plan DENIED, onay kartı hiç doğmaz
├─ elevate → onay kartı "kaynağın adını yaz" ister
└─ allow → onay kartı açılır
│
kullanıcı onaya basar
│
HOOK (stage: approve)
│
┌───────────────┴─ deny → 409, plan BEKLEMEDE kalır
│
apply
│
HOOK (stage: notify) ← kuyruktan, asenkronGuardrail ile hook aynı şey değildir. Cevabı planın şeklinden tek başına bilinebilen kural guardrail'e aittir (“bu namespace'te silme yasak”) ve ağ turu bile atmaz. Hook, cevabını yalnızca dışarının bilebileceği sorular içindir: açık change ticket var mı, bakım penceresi mi, nöbetçi kim.
İstek: başlıklar ve gövde
Her zaman POST, her zaman JSON. Yönlendirme izlenmez;
3xx dönerseniz çağrı hata sayılır.
| Başlık | Her zaman | Değer |
|---|---|---|
content-type | Evet | application/json |
user-agent | Evet | yeke-core, sabit ve sürümsüz. Erişim günlüğünüzde isteğin YEKE'den geldiğini görebilesiniz diye. |
x-yeke-hook | Evet | Hook'un kimliği: sizin verdiğiniz ad. |
x-yeke-delivery | Evet | Teslim UUID'si. Idempotency anahtarınız budur; tekrar denemelerde aynı kalır. |
authorization | Hayır | Yalnız tanımladıysanız. Değer ham geçer — Bearer …, Basic …, ne yazdıysanız o. |
x-yeke-signature-256 | Hayır | Yalnız imza sırrı tanımladıysanız. Gövdenin HMAC-SHA256'sı, hex. |
Doğrulama gövdesi
stage alanı plan ya da approve taşır; gövdenin geri kalanı
ikisinde de aynıdır.
{
"schemaVersion": 1,
"hookId": "change-board",
"deliveryId": "6f1c9d84-2b77-4a51-9e0e-7a2f1c0d8b33",
"stage": "plan",
"plan": {
"id": "op-01KZGTWSG5WSSR8PDZ35",
"clusterId": "c-3",
"source": "ui",
"createdAt": "2026-08-08T14:02:09.144Z",
"actor": { "userId": "u-2", "username": "deniz" },
"identity": { "user": "deniz", "groups": ["platform"] },
"origins": [
{ "method": "PATCH", "path": "/apis/apps/v1/namespaces/uretim/deployments/api" }
],
"steps": [
{
"target": {
"schemaId": "apps/v1/Deployment", "group": "apps", "version": "v1",
"kind": "Deployment", "resource": "deployments",
"namespaced": true, "namespace": "uretim", "name": "api",
"subresource": null, "uid": "9c1…", "resourceVersion": "122835"
},
"classification": { "severity": "mutating", "flags": ["owner-managed"] },
"changedPaths": ["/spec/replicas"],
"dryRun": { "status": "ok" }
}
],
"policy": {
"verdict": "allow",
"approvalLevel": "standard",
"matches": [
{ "policyId": "uretim.replica-tavani", "effect": "allow", "message": null }
]
}
}
}| Alan | Değerler | Ne demek |
|---|---|---|
stage | plan · approve · test | test, yönetim ekranındaki Sına düğmesidir ve gövdesinde plan yoktur; kararı hiçbir şeye dönüşmez. |
source | ui · nl · api | İsteği doğuran yüzey. nl = sohbet. |
actor | nesne · null | YEKE kullanıcısı. Otomasyondan gelen bir istekte null olabilir. |
identity | nesne | Kubernetes kimliği: apiserver'a gerçekten bu kimlikle gidilir. |
origins[].method | POST · PATCH · PUT · DELETE | Ham HTTP niyeti. Fiil buradan okunur. |
classification.severity | mutating · destructive | Yıkıcılık sınıfı; nesnenin şeklinden türetilir. |
classification.flags | dizi | cluster-scoped, collection, finalizers, owner-managed, orphan-delete, dry-run-unsupported, irreversible, stream |
changedPaths | JSON Pointer dizisi | Hangi yolların değişeceği. Değerler yok, yalnız yollar. |
dryRun.status | ok · failed · unsupported · skipped · not-applicable | apiserver'ın dry-run'ı ne dedi. |
policy.verdict | allow · deny · elevate · error | YEKE'nin kendi guardrail kararı, sizinkinden önce alınmış. |
policy.approvalLevel | standard · elevated | Onay kartının bugünkü çıtası. |
Gövdede nesne DEĞERİ yoktur ve olmayacaktır. Manifest yok, önceki hâl yok,
dry-run sonucu yok, istek gövdesi yok. Hook “ne olacağını” bilmek zorunda, “hangi değerin ne
olacağını” değil. Bir Secret güncellenirken bile changedPaths
["/data/password"] der, parolayı değil. Bunu bir ayarla açamazsınız: gövdeyi üreten tek fonksiyon zaten başka bir şey
üretmiyor.
Bildirim gövdesi
stage her zaman notify. Adım şekli doğrulamayla aynı; farkı
event ve result.
{
"schemaVersion": 1,
"hookId": "itsm-koprusu",
"deliveryId": "b3d0…",
"stage": "notify",
"event": { "type": "plan.applied", "ts": "2026-08-08T14:02:14.881Z" },
"operation": {
"id": "op-01KZGTWSG5WSSR8PDZ35",
"clusterId": "c-3",
"source": "ui",
"revertOf": null,
"actor": { "userId": "u-2", "username": "deniz" },
"identity": { "user": "deniz", "groups": ["platform"] },
"origins": [ { "method": "PATCH", "path": "/apis/apps/v1/…" } ],
"steps": [ { "target": { … }, "classification": { … }, "changedPaths": ["/spec/replicas"] } ],
"result": {
"state": "APPLIED",
"stepsApplied": 1,
"appliedAt": "2026-08-08T14:02:14.702Z",
"cause": null,
"failure": null,
"reason": null
}
}
}event.type altı değerden birini taşır — üç terminal sonuç ve çift onayın üç
jesti; şemada donmuş bir üçlü değildir. Terminal üçlü:
plan.applied— bütün adımlar uygulandı.plan.partially_applied— bir kısmı uygulandı;stepsAppliedkaçını söyler.plan.failed— uygulanamadı.
Başarısız ya da kısmi sonuçta sebep kod olarak gelir: result.cause planın
neden durduğunu kod ve parametreleriyle söyler (ör. APPLY_STEP_FAILED ve düşen adımın
sırası), result.failure düşen adımın arıza kodunu ve parametrelerini taşır. result.reason yalnız apiserver'ın kendi cümlesini taşır, yoksa
null'dır; YEKE'nin kendi açıklaması bu alana yazılmaz (0.49.28).
Çift onayın üç jesti de (plan.consent_requested, plan.consented,
plan.consent_revoked) aynı gövde biçimiyle gelir: henüz apply anına gelmemiş bir planı
taşıdıkları için result.cause, result.failure ve result.reason
null, stepsApplied 0'dır. Hook'un abone olabileceği altı
olayın tam listesi: Hook'u tanımlama tablosundaki Olaylar satırı.
revertOf doluysa bu operasyon başka bir operasyonun geri alınmasıdır ve değeri
o operasyonun kimliğidir; korelasyonu kurabileceğiniz tek yer burasıdır.
Yanıt sözleşmesi
Doğrulama hook’unda yanıt gövdesi kararı belirler. Bildirim hook’unda gövde okunmaz.
HTTP/1.1 200 OK
content-type: application/json
{ "verdict": "deny", "reason": "CHG-0042 kapalı — açık change kaydı yok" }| verdict | stage: plan | stage: approve |
|---|---|---|
allow | Plan ilerler, onay kartı açılır. | Kapı açılır, apply koşar. |
deny | Plan DENIED. Onay kartı hiç doğmaz. Terminal. | 409. Plan AWAITING_APPROVAL'da bekler, kayıt değişmez. Geçicidir: pencere açılınca aynı karta tekrar basılır. |
elevate | Onay çıtası yükselir: kart kaynağın adını yazmanızı ister. | Ret + tazeleme yönlendirmesi. Donmuş bir planın çıtası değiştirilemez; boru hattı baştan koşar ve elevate doğru yerde söylenir. |
reasonzorunlu değil ama işe yarar. Kullanıcının ekranda gördüğü cümle odur; YEKE onu çevirmez, kendi cümlesiyle değiştirmez. 2048 karakterde kırpılır.- Tanımadığımız alanlar sorun değil. Yalnız
ticketokunur (aşağıda), gerisi yok sayılır. Yanıt sizin şemanız — alan eklediğiniz gün entegrasyonunuzun sessizce her planı engellemesi kabul edilebilir bir davranış olmazdı. ticketbloğu isteğe bağlı (0.25.0'dan itibaren):{ "id", "url", "status" }.idzorunlu, en çok 128 karakter;urlyalnızhttp(s), en çok 2048;statusITSM'inizin kendi sözcüğü, en çok 64, çevrilmez. Onay kartıid'yi gösterir,urlvarsa bağlantı yapar,status'u çip olarak çizer. Blok varsa kendi şemasından geçmek zorundadır; geçemezse yalnız blok değil yanıtın tamamı anlaşılmamış sayılır — sessizce düşürülmez.- Gövde tavanı 64 KiB. Aşarsa yanıt anlaşılmamış sayılır.
- Bildirimde gövdeye hiç bakılmaz.
204yeter;2xx= teslim edildi.
2xx ama karar yok ile ulaşılamadı ayrı arızalardır ve
ayrı görünürler. Birincisi (gövde JSON değil, verdict yok, tanınmayan değer) sizi
yanıtın biçimine yollar ve tekrar denenmez; deterministik bir uyumsuzluktur.
İkincisi (bağlantı, zaman aşımı, 5xx) servisin kendisine yollar ve bir kez
tekrar denenir.
Onay-anı hook’u ile bakım penceresi (change-freeze,
Enterprise) aynı kapı noktasını paylaşır: ikisi de 409 döner ve plan
AWAITING_APPROVAL'da bekler, kayıt değişmez. Ayrım cevaptaki
params.source alanındadır — bakım penceresi bunu "change-freeze"
olarak yazar, hook reddi bunun yerine hookId (ve varsa reason) taşır.
Kanca, ikinci onaycının rıza jestinde (E3 çift onay,
POST …/operations/:opId/consent) çağrılmaz — yalnız gerçek
apply anında koşar. Ayrıntı: Yönetişim.
İmza doğrulama
Bir imza sırrı tanımlarsanız her istek x-yeke-signature-256 taşır.
İmza, ham gövdenin HMAC-SHA256'sıdır, hex kodlu. Başlıklar imzaya dâhil değildir:
başlıkları da kapsayan kanonik bir dize, sizden bizim sıralama kuralımızı yeniden yazmanızı
isterdi ve o gerçeklemenin ayrıştığı gün imza sessizce tutmazdı. Tekrar oynatmaya karşı
koruma gövdenin içinde: deliveryId imzanın altındadır.
Gövdeyi ayrıştırmadan önce doğrulayın ve ham baytlar üzerinde çalışın: JSON'u ayrıştırıp yeniden serileştirmek anahtar sırasını ve boşlukları değiştirir, imza tutmaz.
// Node.js — express, ham gövde ile import { createHmac, timingSafeEqual } from "node:crypto"; app.post("/yeke/hook", express.raw({ type: "application/json" }), (req, res) => { const expected = createHmac("sha256", process.env.YEKE_HOOK_SECRET) .update(req.body) // Buffer — ayrıştırılmamış hâli .digest("hex"); const got = req.get("x-yeke-signature-256") ?? ""; // Uzunluk eşit değilse timingSafeEqual FIRLATIR; önce onu ele. if (got.length !== expected.length || !timingSafeEqual(Buffer.from(got), Buffer.from(expected))) { return res.status(401).json({ error: "bad signature" }); } const body = JSON.parse(req.body.toString("utf8")); res.json({ verdict: "allow" }); });
# Python — Flask import hmac, hashlib, os from flask import request, jsonify @app.post("/yeke/hook") def yeke_hook(): raw = request.get_data() # bytes, ayrıştırılmamış expected = hmac.new(os.environ["YEKE_HOOK_SECRET"].encode(), raw, hashlib.sha256).hexdigest() if not hmac.compare_digest(expected, request.headers.get("x-yeke-signature-256", "")): return jsonify(error="bad signature"), 401 body = request.get_json() return jsonify(verdict="allow")
İmza sırrı en az 8 karakter olmalıdır. Daha kısası reddedilir; sebep teşhis metinlerindeki maskedir — kısa bir dize her yerde eşleşir ve maskeyi işe yaramaz hâle getirirdi. Sır bir kez yazılır, bir daha okunamaz: ekran bir sırrın var olduğunu gösterebilir, ne olduğunu asla.
Zaman aşımı, tekrar, fail-closed
Doğrulama ve bildirim hook’ları farklı timeout ve retry kuralları kullanır.
| Doğrulama | Bildirim | |
|---|---|---|
| Zaman aşımı | timeoutMs — 500–10000 ms, varsayılan 5000. Her deneme için taze kurulur. | |
| Tekrar | En fazla 2 deneme (yani bir tekrar), yalnız geçici arızada. Sabit; ayarlanamaz. | maxAttempts — 1–10. 1 = hiç tekrarlama. |
| Bekleme | Kısa ve jitter'lı: onay bekleyen bir insanın penceresindeyiz. | Üstel: 60 sn tabanlı, ×2, %20 jitter, tavan 15 dk. |
| Neyin tekrarı | Ağ hatası, zaman aşımı ve 5xx. 4xx ve anlaşılmayan gövde tekrarlanmaz: deterministik yanlış yapılandırmadır; tekrarlamak yalnız gecikmeyi ve kuyruğu şişirir. | |
| Tükenince | onFailure: deny → plan durur · warn → plan ilerler, iz kalır. | Ölü kuyruğa düşer ve denetim izine hook.delivery_failed yazılır. |
Varsayılan fail-closed'dur ve bedeli açıktır: onFailure: deny
kurulu bir doğrulama hook'unun endpoint'i çökerse o cluster'da yazma durur. Dengeleyicileri şunlar:
10 sn'lik zaman aşımı tavanı, bilinçli bir taviz olarak warn modu, hook'u ekrandan
kapatan acil yol, ve hook hiç kurulmadığında riskin sıfır olması. Zaman aşımı ve ağ arızası da
operasyonu durdurur, yalnız sizin deny'ınız değil.
Idempotency
x-yeke-delivery tekrar denemelerde aynı kalır. Yan etkili bir endpoint
yazıyorsanız (ticket açmak, kanala mesaj düşmek) o değeri saklayın ve aynı teslim ikinci kez
geldiğinde işlemi tekrarlamayın. Doğrulama tarafında ek bir tuzak var: aynı plan
stage: plan ve stage: approve ile iki kez sorulur ve bunlar
ayrı teslimlerdir; aynı plana ait olduklarını plan.id söyler.
Hook'u tanımlama
Yönetim ekranından: Yönetim → Bildirimler → Hook'lar → Yeni hook. Yeniden başlatma gerekmez.
| Alan | Tip | Not |
|---|---|---|
| Kimlik | ikisi de | Denetim izinde, teslim kuyruğunda ve x-yeke-hook başlığında bu adla anılır. Sonradan değiştirilemez. |
| Adres | ikisi de | http ya da https URL — host'un izin listesinde olması şart (aşağıda). Bildirim hook'unda Taşıma için E-posta seçilince bu alan kaybolur, yerine gelen Alıcılar alanına hedef yazılır; teslim SMTP rölesinden gider ve izin listesi hiç sorulmaz. SMTP rölesinin kurulumu ve e-posta gövdesinin dili: Uyarılar. Adres, sırlar gibi tek yönlü saklanır: kaydettikten sonra ekran yalnız host'unu ve izini (son dört karakter ve kısa bir özet) gösterir; değiştirmek için yenisini yapıştırırsınız. |
| Zaman aşımı | ikisi de | 500–10000 ms. |
| Kapsam | ikisi de | Tüm cluster'lar ya da seçili cluster'lar. Eşleşme cluster kimliğiyledir, adıyla değil: ad değişebilir, kimlik değişmez. |
| Aşamalar | doğrulama | plan, approve ya da ikisi. Yazılmazsa plan. Yalnız bakım penceresi bekleten bir hook sadece approve seçebilir. |
| Ulaşılamazsa | doğrulama | deny (varsayılan, fail-closed) ya da warn. |
| Olaylar | bildirim | Üç seçenek: Yalnız uyarılar ve tarama raporları (varsayılan — hiçbir operasyon bildirimi gitmez), Tüm operasyon olayları (altısı birden; yoğun cluster'larda çok bildirim üretebilir) ya da Yalnızca seçili olaylar (üç terminal sonuç: uygulandı, kısmen uygulandı, başarısız; çift onayın üç jesti: istendi, verildi, geri çekildi). Uyarı bildirimleri (kapanış dahil) ve tarama raporu hiçbir seçenekte buradan gelmez; kanalları Yönlendirme ve Zamanlanmış tarama belirler. |
| Teslim denemesi | bildirim | 1–10. |
| Sırlar | ikisi de | authorization başlığı ve/veya imza sırrı. Tek yönlü saklanır. |
Adresin host'u YEKE_HOOK_ALLOWED_HOSTS içinde olmalıdır. Bu
liste boşken core, iç ağa (loopback, RFC1918, link-local, ULA) çözülen hiçbir hook adresine
çıkmaz — genel internetteki bir endpoint çalışmaya devam eder, kesilen tek şey hook'ların hiç işi olmayan
iç ağdır. Sebep SSRF: hook adresini core çağırır ve core cluster'larınıza erişebilen bir ağ
konumunda oturur; kutudan çıktığı hâliyle ürün, yönetici ekranından sürülebilen bir iç-ağ
tarayıcısı olmamalıdır. İç bir adrese bilerek hook kurmak meşru bir hamledir ve yolu açıktır.
Host'u listeye yazın:
YEKE_HOOK_ALLOWED_HOSTS=itsm.ic.ornek.com,change-board
Liste verildiğinde liste kazanır ve iç adres kontrolü hiç koşmaz. Kapı iki yerde birden
işler: kaydı yazarken ve her çağrıda; ikincisi olmasa listeyi daraltmanın hiçbir etkisi
olmaz, daha önce kaydedilmiş hook'lar sessizce çıkmaya devam ederdi. Eşleşme host dizesi
üzerindedir ve port dâhil değildir: ornek.com listedeyse o host'un her portu açıktır.
Eksikse arıza yanıltıcıdır. Ağ erişilebilir olsa bile her çağrı
HOOK_HOST_NOT_ALLOWED ile düşer ve fail-closed kurulumda bu, “hook çalışıyor ama hep
hayır diyor” gibi görünür. Teşhisin ucuz yolu core log'unda o kodu aramaktır.
Sohbet kanalları
Bildirim hook'unun üçüncü Taşıması — HTTP hook'un makine gövdesi değil, bir insanın okuyacağı biçimli mesaj: kalın başlık, durum işareti, tıklanır bağlantı.
Bir HTTP hook'un gövdesi YEKE'nin kendi zarfıdır (schemaVersion,
hookId, deliveryId, stage…) ve bunu doğrudan bir sohbet
uygulamasının webhook'una bağlamak çalışmaz: Google Chat, Slack ve Teams yalnız kendi şemalarını
okur, tanımadıkları her alanda isteği reddeder — Google Chat bunu 400 ile yapar.
Sohbet hook'u tam olarak bunun için ayrı bir Taşıma: gövdeyi e-postanınkiyle aynı üretici yazar
ve seçtiğiniz sağlayıcının kendi zarfına sarar.
- Listede görünen biçim adıdır, adres değil. Webhook adresi bir token taşır; Yönetim → Bildirimler → Hook'lar listesinin hedef sütununda bu yüzden adres değil ürün adı görünür: “Google Chat”, “Slack” ya da “Microsoft Teams”. Slack biçimini Mattermost ve Rocket.Chat de konuşur.
- Adres kaydedildikten sonra okunamaz. Sırlar gibi tek yönlü saklanır: düzenleme ekranı yalnız host'u ve izi gösterir, adresi değiştirmek için Değiştir'e basıp yenisini yapıştırırsınız. Google Chat'te host her kayıtta aynı olduğundan kaydı iz ayırt eder. Adres denetim kaydına da yazılmaz.
- Kimlik doğrulama başlığı ve imza sırrı yoktur. Bu iki alan yalnız HTTP hook'ta yazılabilir; üç sağlayıcı da bunları okumaz. (Daha önce e-posta hook'una da yazılabiliyordu ve hiçbir yerde okunmuyordu — 0.52'de kapatıldı.) Taşımayı HTTP'den başka bir şeye çevirdiğinizde kayıtlı sır silinir; form önceden uyarır, değişiklik denetim kaydına düşer.
- Mesaj dili kayıt başına seçilir — e-posta hook'unun dil seçimiyle aynı kural.
- Uzun mesaj kırpılır ve kırpıldığı gövdede işaretlenir.
- Sağlayıcının host'u
YEKE_HOOK_ALLOWED_HOSTSte olmalı — yukarıdaki izin listesi kuralı sohbet hook'u için de aynen geçerli. - Yalnız ekrandan kurulur. Yönetim → Bildirimler → Hook'lar → Yeni hook, Taşıma için “Sohbet” ve biçim seçin, webhook adresini yapıştırın; Sına düğmesi kanala bir deneme mesajı gönderir. YAML dosya hook'u sohbeti tanımaz, yalnız ekrandan kurulur.
4xx artık bizim gövdemize bakmanızı söylüyor. Bir sohbet uygulamasına HTTP gövdesi
gönderildiğinde görülen arıza tam olarak budur: Google Chat'in şema uyuşmazlığına verdiği 400 gibi bir 4xx'te
ekrandaki cümle eskiden her durumda “uç ayakta değil ya da isteği reddediyor” diyordu.
Artık 4xx'te bakılacak yerin gönderdiğimiz istek olduğunu söylüyor; 5xx ve ulaşılamama
durumunda cümle değişmedi, yine uca işaret ediyor.
Çalışan referans endpoint
Bağımlılıksız, tek dosya. İndirin ya da kopyalayın, çalıştırın, adresini hook'a yazın.
// node yeke-hook.mjs — :8787/hook (doğrulama) ve :8787/notify (bildirim) import { createServer } from "node:http"; import { createHmac, timingSafeEqual } from "node:crypto"; const SECRET = process.env.YEKE_HOOK_SECRET; const seen = new Set(); // idempotency: deliveryId const read = async (req) => { const chunks = []; for await (const c of req) chunks.push(c); return Buffer.concat(chunks); }; const signatureOk = (raw, header) => { if (!SECRET) return true; // sır yoksa imza da yok const expected = createHmac("sha256", SECRET).update(raw).digest("hex"); const got = header ?? ""; return got.length === expected.length && timingSafeEqual(Buffer.from(got), Buffer.from(expected)); }; // ← Burayı kendi ITSM'inizle değiştirin. Stub: her namespace'te açık change var sayar. const openChangeRecord = async (ns) => true; // ← Burayı kendi kanalınızla değiştirin. Stub: hiçbir şey yapmaz. const postToChannel = async (eventType, operation) => {}; createServer(async (req, res) => { const raw = await read(req); const json = (code, payload) => { res.writeHead(code, { "content-type": "application/json" }); res.end(JSON.stringify(payload)); }; try { if (!signatureOk(raw, req.headers["x-yeke-signature-256"])) { return json(401, { error: "bad signature" }); } const body = JSON.parse(raw.toString("utf8") || "{}"); // ── doğrulama ──────────────────────────────────────────────── if (req.url === "/hook") { // Sınama çağrısında plan YOK; ulaşılabilirlik ölçülüyor. if (body.stage === "test") return json(200, { verdict: "allow" }); // Karar YALNIZCA dışarının bildiği bir şeye dayanmalı. const ns = body.plan?.steps?.[0]?.target?.namespace; const ticket = await openChangeRecord(ns); return ticket ? json(200, { verdict: "allow" }) : json(200, { verdict: "deny", reason: `${ns} için açık change kaydı yok` }); } // ── bildirim ───────────────────────────────────────────────── if (req.url === "/notify") { // Aynı teslim iki kez gelebilir; yan etki bir kez koşmalı. if (!seen.has(body.deliveryId)) { await postToChannel(body.event.type, body.operation); seen.add(body.deliveryId); // yan etki BAŞARDIKTAN sonra } res.writeHead(204).end(); // 2xx = teslim alındı return; } json(404, { error: "not found" }); } catch (err) { // ITSM'inizin arızası `allow` olmamalı, süreci de düşürmemeli: // 503, core'un onFailure ayarını işletir (varsayılan deny). json(503, { error: String(err?.message ?? err) }); } }).listen(8787);
Dosya olduğu gibi çalışır: ITSM'e ve kanala giden iki fonksiyon stub'dır, her plana allow
der, bildirimi hiçbir yere yazmaz. Değiştireceğiniz satırlar o ikisi; gerisi imza, idempotency ve arıza
davranışı. Doldurulmuş bir örnek için ITSM'e bağlama.
Kurduktan sonra yönetim ekranındaki Sına düğmesi endpoint'in ayakta olduğunu ve kimliğinizi
kabul ettiğini tek çağrıda ölçer. O çağrının kararı hiçbir şeye dönüşmez: deny
dönseniz bile hiçbir plan bloklanmaz.
ITSM'e bağlama
Yukarıdaki referansın ITSM'e giden iki satırının doldurulmuş hâli. Bağlayıcı bir ürün parçası değil, bir örnektir: indirip kendi kurulumunuza uyarlarsınız; lisans kapısı yoktur.
1 · İlk adım: izin listesi
Kurumların ITSM'i iç ağdadır ve izin listesi boşken core iç ağa çıkmaz.
Hook'u tanımlamadan önce ITSM'e giden host'u YEKE_HOOK_ALLOWED_HOSTS'a yazın;
compose ve k8s dağıtımları bu anahtarı taşır. Atlanırsa ilk deneme daha kayıtta
HOOK_HOST_NOT_ALLOWED ile düşer ve arıza ITSM'de aranır.
2 · Hangi aşama hangi soruyu sorar
| Aşama | ITSM'e sorulan | deny ne yapar |
|---|---|---|
plan | Bu namespace için açık bir change kaydı var mı? | Plan DENIED; onay kartı hiç doğmaz. |
approve | O kayıt hâlâ onaylı mı? | 409; plan bekler. Change manager onaylayınca aynı karta yeniden basılır. |
notify | Soru yok. | Operasyon kimliği ticket'ın günlüğüne yazılır. |
Aynı plan iki ayrı teslimle sorulur; ikisini plan.id bağlar
(Idempotency). Hook'u tanımlarken aşama olarak ikisini seçin: yalnız
plan seçilirse kart açıldıktan sonra geri çekilen bir onay görülmez.
3 · Ticket'sız yıkıcı operasyon onaylanamaz
Yıkıcılık sınıfı gövdede (classification.severity); kararı siz
verirsiniz. Örnekteki kuralın tamamı:
const destructive = steps.some((s) => s.classification?.severity === "destructive");
const ticket = await openChangeFor(ns);
if (!ticket) {
return destructive
? json(200, { verdict: "deny", reason: `no open change record for ${ns}` })
: json(200, { verdict: "allow" });
}4 · Kartta kimlik ve durum
reason onay kartına olduğu gibi düşer; onaylayan kişi hangi change'in yetki verdiğini
orada okur. Örneğin kalıbı "<ref> (<status>) — <başlık>":
{ "verdict": "allow", "reason": "C-000123 (approved) — uretim namespace bakımı" }0.25.0'dan itibaren yanıta tipli bir ticket bloğu da koyabilirsiniz; kart kimliği
gösterir, adres varsa bağlantı yapar, durumu çip olarak çizer (yanıt sözleşmesi).
reason yine olduğu gibi düşer.
5 · Apply sonrası ticket'a operasyon kimliği
Bildirim gövdesi operation.id taşır; örnek onu ticket'ın günlüğüne yazar. Tekrar teslimde
deliveryId aynı kalır; örnek onu bir kümede tutar ve yazma başardıktan sonra ekler —
ITSM'in o an düşmesi kaydı kaybettirmez, tekrar deneme yeniden yazar. Ticket'ın durumunu YEKE
değiştirmez: kapatmak change manager'ın işidir.
6 · ITSM arızasında asla allow dönmeyin
ITSM'e ulaşılamıyor, kimlik reddedildi, yanıt beklenen biçimde değil — hepsi 503. Core bunu
“ulaşılamadı” sayar, bir kez tekrar dener, tükenince hook'un onFailure ayarını uygular
(varsayılan deny). Arızada allow dönen bir endpoint,
ticket kapısını tam gerektiği anda kaldırmış olur.
7 · Örnek: iTop
yeke-hook-itop.mjs — tek dosya, bağımlılıksız, Node 22+.
iTop'un REST/JSON arayüzüne karşı yazıldı (json_data form alanı, auth_token,
uygulama düzeyi code) ve yukarıdaki altı maddenin hepsini taşır. Yapılandırma env ile:
ITOP_URL=https://itsm.ic.ornek.com/itop ITOP_TOKEN=… # ya da ITOP_USER + ITOP_PASSWORD ITOP_CLASS=NormalChange # varsayılan ITOP_APPROVED_STATES=approved,implemented # varsayılan YEKE_HOOK_SECRET=… # hook'a yazdığınız imza sırrı node yeke-hook-itop.mjs
Ticket'ı hedeften bulan tek fonksiyon openChangeFor(ns): örnek, change başlığında namespace'in
yazılı olması geleneğine dayanır. Kendi eşlemenizi (CI bağı, özel alan, etiket) oraya yazarsınız; hook
gövdesinde ticket alanı yoktur.
Sıra: izin listesi → dosyayı indirip koşturun → Yönetim → Bildirimler → Hook'lar ekranında adresini yazın,
aşamalar plan ve approve → Sına (iTop'a core/check_credentials
gider) → bir yıkıcı plan kurup kartta reason'ı okuyun.
Bu örnek sahte bir iTop'a karşı ve 04.09.2026'da gerçek bir iTop 3.2.2'ye karşı ölçüldü; aynı gün YEKE üstünden uçtan uca canlı kabul geçti (yıkıcı plan → kartta change kaydı, onay anında henüz onaylanmamış change → 409, ITSM'de onay → aynı karta apply → nesne silindi, ITSM günlüğünde operasyon kimliği, ITSM kapalıyken 503 → ret) (beş senaryo: bağlantı sınaması, planda açık change → izin, onay anında henüz onaylanmamış change → ret, ticket'sız yıkıcı işlem → ret, yıkıcı olmayan → izin).
Ölçülen şey protokolün şekli ve on bir senaryodaki kararlar. OQL sözdizimi,
ref/status/title alan adları ve günlük alanı (private_log)
gerçek kurulumunuzda farklı olabilir; hangi satırların doğrulanmadığı dosyanın başında yazılı.
Hook'un yapamadıkları
Entegrasyonunuzu aşağıdaki sınırlara göre yapılandırın.
allowinsan onayını atlamaz. Siz “evet” derken bile onay kartı açılır ve bir insanın basması beklenir. Hook kapı ekler, kapı kaldırmaz.- Hook planı değiştiremez. Yanıt sözlüğü üç kelimedir. Replika sayısını düzeltmek, bir adım eklemek, hedefi değiştirmek — hiçbiri mümkün değil, alanı da yok.
- Denetim izinin tamamına abone olunamaz. Yalnız altı operasyon bildirimi (üç terminal
sonuç, çift onayın üç jesti) teslim edilir.
auth.logingibi bir olay yazmayı denerseniz kayıt reddedilir ve ret, sınırı adıyla söyler. İzin tamamının dışarı aktarımı ayrı bir yetenektir ve garantileri (sürekli akış, boşluk tespiti, imzalı biçim) bundan farklıdır. - Nesne değeri hiç görmezsiniz. Yukarıdaki gövde disiplini bir ayar değil sınırdır.
- Yönlendirme izlenmez.
3xxbir hatadır: bir302, imzalı gövdeyi veauthorizationbaşlığını operatörün yazmadığı bir host'a taşırdı. - Yanıtınız
2xxolmalı.200+ karar taşıyan gövde, ya da bildirimde herhangi bir2xx.
Kurmadan önce denemek ister misiniz?
Kendi sisteminizi bağlamadan önce zincirin nasıl davrandığını görmek için bir simülatör var: dış sistemi oynayan sahte bir endpoint ve gelen her isteğin ham gövdesini gösteren bir panel. Yukarıdaki “gövdede nesne değeri yoktur” iddiasını gözle doğrulayabileceğiniz yer orası.