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: onFailure belirler; varsayılan deny.

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, asenkron

Guardrail 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ıkHer zamanDeğer
content-typeEvetapplication/json
user-agentEvetyeke-core, sabit ve sürümsüz. Erişim günlüğünüzde isteğin YEKE'den geldiğini görebilesiniz diye.
x-yeke-hookEvetHook'un kimliği: sizin verdiğiniz ad.
x-yeke-deliveryEvetTeslim UUID'si. Idempotency anahtarınız budur; tekrar denemelerde aynı kalır.
authorizationHayırYalnız tanımladıysanız. Değer ham geçer — Bearer …, Basic …, ne yazdıysanız o.
x-yeke-signature-256HayırYalnı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 }
      ]
    }
  }
}
AlanDeğerlerNe demek
stageplan · approve · testtest, yönetim ekranındaki Sına düğmesidir ve gövdesinde plan yoktur; kararı hiçbir şeye dönüşmez.
sourceui · nl · apiİsteği doğuran yüzey. nl = sohbet.
actornesne · nullYEKE kullanıcısı. Otomasyondan gelen bir istekte null olabilir.
identitynesneKubernetes kimliği: apiserver'a gerçekten bu kimlikle gidilir.
origins[].methodPOST · PATCH · PUT · DELETEHam HTTP niyeti. Fiil buradan okunur.
classification.severitymutating · destructiveYıkıcılık sınıfı; nesnenin şeklinden türetilir.
classification.flagsdizicluster-scoped, collection, finalizers, owner-managed, orphan-delete, dry-run-unsupported, irreversible, stream
changedPathsJSON Pointer dizisiHangi yolların değişeceği. Değerler yok, yalnız yollar.
dryRun.statusok · failed · unsupported · skipped · not-applicableapiserver'ın dry-run'ı ne dedi.
policy.verdictallow · deny · elevate · errorYEKE'nin kendi guardrail kararı, sizinkinden önce alınmış.
policy.approvalLevelstandard · elevatedOnay 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ı; stepsApplied kaçı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" }
verdictstage: planstage: approve
allowPlan ilerler, onay kartı açılır.Kapı açılır, apply koşar.
denyPlan 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.
elevateOnay çı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.
  • reason zorunlu 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 ticket okunur (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ı.
  • ticket bloğu isteğe bağlı (0.25.0'dan itibaren): { "id", "url", "status" }. id zorunlu, en çok 128 karakter; url yalnız http(s), en çok 2048; status ITSM'inizin kendi sözcüğü, en çok 64, çevrilmez. Onay kartı id'yi gösterir, url varsa 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. 204 yeter; 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ğrulamaBildirim
Zaman aşımıtimeoutMs — 500–10000 ms, varsayılan 5000. Her deneme için taze kurulur.
TekrarEn fazla 2 deneme (yani bir tekrar), yalnız geçici arızada. Sabit; ayarlanamaz.maxAttempts — 1–10. 1 = hiç tekrarlama.
BeklemeKı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ükeninceonFailure: 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.

AlanTipNot
Kimlikikisi deDenetim izinde, teslim kuyruğunda ve x-yeke-hook başlığında bu adla anılır. Sonradan değiştirilemez.
Adresikisi dehttp 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 de500–10000 ms.
Kapsamikisi deTü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şamalardoğrulamaplan, approve ya da ikisi. Yazılmazsa plan. Yalnız bakım penceresi bekleten bir hook sadece approve seçebilir.
Ulaşılamazsadoğrulamadeny (varsayılan, fail-closed) ya da warn.
OlaylarbildirimÜç 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 denemesibildirim1–10.
Sırlarikisi deauthorization 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şamaITSM'e sorulandeny ne yapar
planBu namespace için açık bir change kaydı var mı?Plan DENIED; onay kartı hiç doğmaz.
approveO kayıt hâlâ onaylı mı?409; plan bekler. Change manager onaylayınca aynı karta yeniden basılır.
notifySoru 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.

  • allow insan 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.login gibi 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. 3xx bir hatadır: bir 302, imzalı gövdeyi ve authorization başlığını operatörün yazmadığı bir host'a taşırdı.
  • Yanıtınız 2xx olmalı. 200 + karar taşıyan gövde, ya da bildirimde herhangi bir 2xx.

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ı.