# Review — Chunk 5 (EGT-33) implementasyonu: üç kimlik alanı — 27.07.2026

> **Review type:** impl-review
> **Previous review:** `review-omurga-plan-chunk5-2026-07-26.md` (plan APPROVED, 5 bulgu → bu turda kapanışları doğrulandı) · spec: `review-omurga-spec-2026-07-26-r2.md` (APPROVED)
> **Upstream document(s) read:** `omurga-plan_chunk5.md` **bu turda değişmiş hâliyle** dosyadan yeniden okundu (§5.1.0, §5.1.2 sertleştirme, A-5a, kriter 8/17/19/26a-b/27/28 revizyonları diff ile) · `docs/specs/omurga-spec.md` (A-5a, RT-7, kriter 38) · chunk2 §2.8 · chunk6/chunk8/chunk9/plan index senkronları
> **Kod doğrulaması:** 95 dosyalık diff (`80acb32~1..HEAD`); migrationlar, guard/oturum/kapı katmanı, aksiyonlar, rotalar ve testler **dosyadan okundu**; wire/orphan **reverse-grep gerçek koşumla** yapıldı; CI kanıtı **TC #744 SUCCESS, 344 test** (sıfırdan kurulmuş veritabanıyla — F-CI sonrası)
> **Stage locks in effect:** ⛔ spec v1.11 + chunk5 planı APPROVED — model yeniden tartışılmaz. ⛔ A-5a/RT-7 Sets kararıdır; sorgulanmaz, yalnız işlenişi denetlenir.

---

## Upstream Coverage Matrix (chunk5 §5.9, dosyadan)

| Kriter | Durum | Kanıt (dosya/test — bağımsız doğrulandı) |
|---|---|---|
| 1, 20 | ✓ | `2026_07_27_000100/000200` migrationları: üç aile + `sessions` DROP (`dropFrameworkSessionTable`); `IdentityDomainTest` *keeps the three identity domains on separate tables* |
| 2, 3 | ✓ | `citext` e-posta, unique `(tenant_id, email)` (000200:153); testler *lets the same address exist…* / *unique per tenant rather than globally* |
| 4, 5, 6 | ✓ | `ParticipantStatus` + `ParticipantAreaController` (`/katilimci` rotası bağlı); `ParticipantCapabilityTest` üç test |
| 7, 11 | — | chunk6'ya taşınmış (plan turunda onaylıydı; chunk6 7b/7c yerinde) |
| 8 | — → **D-1 geçerli** | chunk6 kriter 15'e taşındığı doğrulandı (chunk6:169, U-3/U-4/U-5 içerikli); gerekçe orphan-üretmeme ilkesi, emsal kriter 7/11. Kabul |
| 9, 10, 25 | ✓ | `AuthSettings` + `SessionSeparationTest` *settles each identity domain independently* / *leaves an open session alone…* |
| 12, 13, 27 | ✓ | `magic_links` hash'li şema (000200), `MagicLinkTest` 10 test — ikinci kullanım, süre, teyit kapatılamazlığı, istek ucu (`POST /baglanti` rotası bağlı), postada firma adı yok |
| 14, D-3 | ✓ | Kapı migration'ı (000300): dört kolon imzada sabit; test `pg_proc.proargnames`'ten okuyor — koddan değil, veritabanından |
| 15, 16 | ✓ | `CustomerAuthenticator` çoklu-eşleşme reddi; `AuthenticationAuditTest` |
| 17 (A-5a) | ✓ | `TerminatesSessions` (`static::updated`, kuyruk yok, satır silme) üç modelde de (`grep -l`: User, BackofficeUser, CustomerUser); `EnsureAccountIsActive` `web` grubunda (bootstrap/app.php:50) **ve** Filament panel middleware'inde; `SessionSeparationTest` üç test (iki cihaz, üç alan, başkasının oturumuna dokunmama) |
| 18, 21, 22 | ✓ | `config/auth.php` `defaults.guard = null` + üç guard; `IdentityDomainTest` *…no default*; `SessionSeparationTest` *keeps every pair of identity domains out of each other* |
| 19 | ✓ | `BackofficePanelProvider`: `authGuard(backoffice)` + `canAccessPanel` (aktif kayıt + panel id — Filament'in local-only müsamahası kapatılmış); negatif+pozitif test |
| 23 (I-5) | ✓ | Şemada bağlayıcı kolon/eşleştirme tablosu taraması (`links no record in one identity domain to another`) |
| 24 | ✓ | Filament `multiFactorAuthentication(isRequired: true)` kod kuralı + `IdentityDomain::requiresMfaForEveryone()`; konfigürasyon + davranış testi |
| 26, 26a, 26b | ✓ | `customer_password_reset_tokens` **kişi anahtarlı** (000200); `AnonymousRequestLimiter` + `config('security.password_reset_throttle')`; 26a testi posta **gövdesini** sınıyor (firma adı/tenant izi yok — assert içeriğini okudum) |
| 28 | ✓ | `ActorType::BackofficeUser` eklendi; `visibleToOffice()` = `[OfficeUser, System]` — backoffice dışarıda; `InheritedCriteriaTest` |
| D-2 | ✓ (yarısı **geçerli olarak düşürüldü**) | `magic_link_lookup` altı kolon (000300 + test); `app.all_customer_ids` kodda **hiç yok** (grep boş) — chunk3 §3.6.1 o yaklaşımı zaten reddetmişti; ölü cümlenin düşürülmesi doğru |
| D-4, D-5 | ✓ | `pg_indexes` taraması; R-12/R-14/R-16 karşılıkları |
| D-6, D-7 | ✓ | `CustomerUser::casts()` `TenantEncrypted` (phone, national_id, two_factor_secret) + `national_id_hash` HMAC + `key_version`; kolonlar `text`; testler şifreli saklama + tip taraması. ⚠ `config('crypto.encrypted_columns')` listesi **henüz yok** — S-16/S-18 chunk9'da doğduğunda yaratılacak; bugün kırık bir şey yok ama chunk9'un ilk işi bu listeyi kurmak |

**Plan-review (heyet 143) bulgu kapanışları:** F-1 → chunk9 **S-19** (allowlist'li panel taraması) + kriter 33 devir kuralı plan index'te (omurga-plan.md:78) ✓ · F-2 → chunk6 **kriter 2c** (Y-4/Y-5 backoffice bacağı) ✓ · F-3 → dizge kontrolü tek yerde (chunk6 2a; chunk5 19'da atıf) ✓ · F-4 → §5.1.2'ye throttle + posta içeriği kuralları ve kriter 26a/26b ✓ · F-5 → iki eski-model kod yorumu temizlenmiş (grep: yalnız yeni negatif assert'te geçiyor) ✓.

**Bilinçli orphan:** `CreateCustomer` — reverse-grep **0 caller** (doğrulandı; diğer 19 yeni sembolün tamamı ≥1). Kapanış chunk6 kriter 15a'da kayıtlı (chunk6:170), emsal `OfficeAuditFeed`. Kabul.

**Spec/plan değişiklikleri (bu turda):** A-5a + kriter 38 + RT-7 spec'te; chunk8 §8.2.0 (CH-1…CH-5) + §8.2.1 (Reverb `terminate_connections` / `userAuthentication` tuzağı, negatif kontrol şartlı kriter 3b) yazılı. Kriter 38'in "anlık bağlantısı da kapanıyor" ayağının chunk8'e borç yazıldığı hem A-5a satırında hem chunk8 3b'de kayıtlı — borç adresli, sahipsiz değil.

## ✅ What works

- **Kapı disiplini örnek seviyede:** üç bootstrap fonksiyonu tek migration'da, kolon listesi imzada sabit, `REVOKE ALL FROM PUBLIC` + role-bazlı `GRANT`, A/G sözleşme ayrımı yorumda; testler kolon sayısını `pg_proc`'tan okuyor — kendi kodunun beyanına değil kataloğa güveniyor.
- **`TenantContextGuard`'ın üçüncü istisnası deseni bozmamış:** üçü de aynı şablonda (bileşene tanınır, komut + tablo olarak iki kez daraltılmış) ve tek dosyada simetrik duruyor.
- **A-5a implementasyonu doğru katmanda:** oturum satırının silinmesi = kimliğin kendisinin düşmesi; middleware ikinci katman ve `web` grubu + panel'de global. "Kuyruklanmaz" gerekçesi kodda.
- **`CurrentIdentityDomain`'in `get()`/`require()` ayrımı** — altyapı için en az yetkili alana düşme, erişim veren akışlar için tahmin reddi — tam doğru güvenlik içgüdüsü.
- **F-CI şeffaflığı:** lokalde geçip CI'da düşen migrate-edilmemiş-DB durumu gizlenmemiş, kökü teşhis edilmiş, doğrulama sıfırdan kurulumla tekrarlanmış.

## ⚠️ Findings & Risks (in-scope for this review type)

**F-1 (orta) — Bağlamsız KESİN liste planla senkronsuz.** `customer_user_sessions` ve `customer_auth_settings` implementasyonda bağlamsız yapıldı (`config/tenancy.php:106`, muaf sayısı 32) ve migration gerekçeleri sağlam (oturum satırı kimlik doğrulamadan ÖNCE okunur — RLS dairesel olurdu; ayar ekseni alan, tenant değil — spec §5.1 zaten müşteri firmaya yöntem seçtirmez). **Ama:** (a) chunk2 §2.8'in kesin listesinde bu iki tablo yok ve liste *"burada olmayan her tablo tenant-kapsamlıdır"* diyor — hüküm şu an iki tablo için yanlış; (b) chunk5 §5.1 hâlâ ikisini **"Müşteri tarafı (tenant-kapsamlı, RLS'li)"** başlığı altında listeliyor; (c) `TenancyTest`'in adı *"keeps the exempt list in step with the plan"* ama plan güncellenmemiş — test yorumu gerekçeyi taşıyor, gerekçenin evi olan doküman taşımıyor. Kesin-liste disiplini tam bu senkronu şart koşar (kendi kuralı: *"eklemek plan değişikliğidir"*). Doc-only düzeltme.

**F-2 (orta) — `customer_users.email` düz metin; upstream şifreleme listesiyle kayıtsız sapma.** chunk4 §4.1 şifrelenecek alanları sayarken **"kişisel e-posta"** açıkça listede; spec §8.1 "iletişim bilgileri … saklanırken şifrelenir" der. İmplementasyon telefonu şifreliyor, e-postayı `citext` düz metin saklıyor — ve hiçbir yerde (migration yorumu, plan, spec) bu sapmanın kaydı yok. Mimari zorunluluk savunulabilir: e-posta giriş kimliğidir, bootstrap kapısı `WHERE email =` ile tenant-öncesi eşleşmek zorundadır (tenant anahtarlı hash tanım gereği çalışamaz), `unique (tenant_id, email)` karşılaştırılabilir değer ister. Muhtemel doğru sonuç "giriş kimliği e-postası şifreleme kapsamının adlandırılmış istisnasıdır" kararıdır — ama o karar **verilmemiş, varsayılmış** durumda. Karar Sets'indir: (a) adlandırılmış istisna olarak spec §8.1/chunk4 §4.1'e işlenir, ya da (b) farklı bir çözüm istenir (prod verisi yokken şema değişikliği ucuz). Aynı soru `users.email`/`backoffice_users.email` için de geçerli ancak oradaki veri tenant verisi değil; asıl karar müşteri tarafı için gerekli.

**F-3 (minör) — Migration yorumlarında iki hayalet sınıf adı.** `000100:138` "see `App\Support\Auth\BackofficeMfa`" — böyle bir sınıf yok (zorlama Filament `isRequired: true` + `IdentityDomain::requiresMfaForEveryone()`); `000100:209` "`App\Support\Auth\AuthSetting`" — gerçek ad `AuthSettings`. Yorumdan kod izleyen ilk kişi çıkmaza girer; heyet.md'nin "yanlış model anlatan yorum" sınıfının hafif hâli.

**F-4 (minör) — Auth ayar tablosunun şekli plana işlenmemiş sapma.** Plan §5.3 `(method, is_enabled)` çifti diyor; implementasyon `key/jsonb value` yaptı ve gerekçesi migration'da sağlam (A-6 süre ayarları aynı tabloda taşınmalı, iki kolon süre taşıyamaz). Sapma doğru; kaydı eksik — plan §5.3 hâlâ eski şekli tarif ediyor.

## ❓ Open Questions (to be answered before next stage)

Sunulan beş soruya değerlendirme:

1. **`CreateCustomer`'ın `RETURNING`'siz insert'i doğru çözüm; işaret değil.** Tuhaflık, "yazılan satır kapsamın kendisidir" olgusunun kaçınılmaz sonucu ve sisteme bir kez, tek Action'da, bekçi istisnasının arkasında giriyor. İki reddedilen alternatifin analizi doğru: SELECT politikasını gevşetmek her bağlamsız okumayı sessiz-boş yapar (yüksek maliyet), `SECURITY DEFINER` yazıcı W sözleşmesinin açık yasağı. `customers`'ın id-bazlı RLS modeli yeniden düşünülmemeli — o model tenant tablosunun kendisi için T-1/§4.1'i sağlayan şey. Örnek-bazlı `incrementing` düğmesi yerelleştirilmiş ve yorumlu; kabul.
2. **Üçüncü bekçi istisnası deseni aşındırmıyor.** Desenin özü sayı değil şekil: muafiyet bileşene (tabloya değil), komut ve tablo olarak iki kez daraltılmış, `finally` ile geri alınan bayrak. Üçü de bu şablonda ve tek dosyada; aşınma sinyali, şablonu bozan ilk istisna olur (tabloya muafiyet, daraltılmamış bayrak, guard dışında bir kaçış). O gün gelirse bulgudur; bugün değil.
3. **`resolveUsersUsing` seam'i doğru katman.** "Her `$request->user()` çağıranı adlandırılmış guard'a çevirmek" framework-içi çağrılar için mümkün değil — `Gate`, `SetLocale` ve Filament nitelikesiz sorar; oraları yamalamak framework'ü çatallamaktır. Seam tek kaynaktan (`CurrentIdentityDomain`, host'tan bir kez) çözüyor ve `get()`'in en-az-yetkili fallback'i + `require()`'ın tahmin reddi yanlış-alan riskini doğru yöne deviriyor. CH-2'nin kanal tarafında adlandırılmış guard seçmesi çelişki değil, oradaki kodun bize ait olmasının sonucu — daha sıkı olan seçilebiliyorsa seçilmeli.
4. **Oturum yazma yükü: varsayım kabul edilebilir, ölçüm chunk9'a ait.** Laravel'in database oturum sürücüsü zaten istek başına bir UPDATE yapar; `domain` sürücüsü buna yalnız tablo seçimi ekler ve PgBouncer transaction pooling kısa yazmalarla uyumludur. Ama varsayım **ölçümsüz kapanmasın**: chunk9 yük testi kriterlerine "oturum yazmasının istek gecikmesine katkısı" satırı eklenmeli — A-2'nin bedeli görünür olsun.
5. **Altı yön fazla değil; "üç yönlü"nün doğru okunuşu.** Üç çiftin her birinde iki yön vardır ve asimetrik bir kusur (örn. çerez adı/etki alanı yapılandırmasının tek yönde sızdırması) ancak iki yön de sınanınca yakalanır. Kriter metni bir dahaki revizyonda "üç alan çifti, her iki yönde" diye netleştirilebilir; davranış doğru.

## Verdict: ❌ REJECTED

Reddin ağırlığı dar: kod davranışında kusur **bulunmadı** — 28 kriterin ve 7 devralınanın kanıtları gerçek şema/katalog/testlerden doğrulandı, wire/orphan taraması temiz, F-CI dersi doğru kapatılmış, iki kriter düşürmesi (D-1, D-2) geçerli. Red, iki **orta** bulgudan geliyor ve ikisi de bu projenin en sert iki disiplinine dokunuyor: F-1 kesin-liste/plan senkronu ("liste ya plandadır ya tablo tenant-kapsamlıdır" hükmü şu an iki tablo için yanlış söylüyor), F-2 ise AD-05 alanında **kayıtsız ve karara bağlanmamış** bir sapma — e-posta düz metni muhtemelen doğru mühendislik, ama onu "doğru" yapacak şey Sets'in vereceği adlandırılmış istisna kararıdır, implementasyonun sessiz varsayımı değil. F-2 kararı + F-1/F-3/F-4 doc düzeltmeleri işlendiğinde yeniden tur kısa olur; kod tarafında değişiklik beklenmiyor (F-2 kararı (b) çıkmadıkça). Son karar kullanıcınındır.
