# @devoplus/one-sdk

**Devoplus One** uygulamalarının (Devoplus Portal içinde iframe ile gösterilen kullanıcı uygulamaları) platform
servislerine **ince istemcisi**. SDK sır taşımaz, kiracı seçmez ve
belirteç üretmez: kimliği platform kapısının yazdığı başlıklardan **okur**, backend'e kullanıcının uygulama
belirteciyle gider, dosyayı kuruluma bağlı dosya deposuna yazar.

Platformun tamamı: `devoplus/devoplus-one` (mimari `docs/architecture.md`, örnek uygulama `apps/hello`).

## Kurulum

```bash
pnpm add @devoplus/one-sdk        # kararlı sürüm (latest)
pnpm add @devoplus/one-sdk@next   # ön sürüm (test kanalı)
```

Kararlı sürümle (`latest`) kurun; `@next` yalnız test kanalındaki ön sürümü denemek içindir. İki kanal Sürümleme bölümünde
anlatılır.

Paket yalnız **ESM** dağıtılır (`dist/index.js` + `dist/index.d.ts`); Node ≥ 22. Çalışma ortamı tipleri (`OneDatabase`,
`OneFilesBinding`, `OneCacheBinding`, `OneApp` vb.) paketin içindedir; ek bir tip paketi gerekmez. `tsconfig` içinde
`"lib": ["ES2022", "WebWorker"]` ve `"types": []` yeterlidir (standart Web API'leri `WebWorker` kütüphanesinden gelir).

### Yerel geliştirme: `one` komutu

Paket `one` komutunu da getirir; uygulamanın kök dizininde çalışır ve `one.manifest.json`'u okur:

| Komut | Ne yapar |
|---|---|
| `one dev [--as kişi] [--port n] [--api-base url]` | Uygulamayı **yerel Portal** içinde açar (`http://127.0.0.1:8787/`): kimlik bağlamı seçilen kişiden üretilir, e-posta, SMS ve mobil bildirim posta kutusuna yazılır, DEXA ve organizasyon şeması `one.dev.json` örnek verisinden gelir ([Yerel Portal ve servis taklidi](#yerel-portal-ve-servis-taklidi-one-dev)). `--api-base` (ya da `ONE_API_BASE`) servis taklidini kapatır ve servis isteklerini o adrese yönlendirir (kendi taklit sunucunuz için); yerel oturum belirteci gerçek Devoplus servislerinde geçmez. |
| `one migrate` | `migrations/` altındaki SQL dosyalarını **yerel** veritabanına uygular (`one dev` aynı veriyi görür). |
| `one build` | Yükleme provası: kodu paketler, hiçbir yere göndermez (çıktı `.one/dist/`). |
| `one data generate [--check]` | `one.data.json` veri modelinden yeni migration + `src/data/types.ts` üretir; yalnız ekleyen değişiklik ([Veri modeli](#veri-modeli-ve-kayıt-işlemleri)). |
| `one upgrade [--no-install] [--json]` | SDK'yı izlediği kanalın (`next` ya da `latest`) son sürümüne **tam sürümle** sabitler, kurulu sürümü `one.manifest.json` `sdkVersion` alanına ve yükseltme durumunu `one.platform-state.json`'a yazar, ardından yeni SDK'nın `one agents sync` ve `one doctor` komutlarını çalıştırır ([SDK sürümü beyanı ve yükseltme](#sdk-sürümü-beyanı-ve-yükseltme-one-upgrade)). |
| `one upgrade --complete --from <sdk>@<rev> --catalog-revision <n> [--sdk-version <v>] [--env test\|uat\|prod] [--json]` | Platform güncellemeleri özeti işlenip düzeltmeler bitince yükseltme durumundaki işlenmiş değeri ilerletir ([Güncelleme döngüsü](#güncelleme-döngüsü-ve---complete)). |
| `one doctor [--json]` | Uygulamayı kurulu SDK'nın taşıdığı platform kuralları kataloğuna karşı tarar; yayın hattının kataloğa bağlı kapılarıyla aynı denetimdir, dosyaya yazmaz, zorunlu bulguda çıkış kodu 1'dir ([Platform kuralları](#platform-kuralları-one-doctor-ve-agentsmd-bölümü)). |
| `one agents sync [--json]` | `AGENTS.md`'nin platform kuralları bölümünü kurulu SDK'nın kataloğuyla yeniler; bölümün dışına dokunmaz ([Platform kuralları](#platform-kuralları-one-doctor-ve-agentsmd-bölümü)). |

Yerel yapılandırma ve veri `.one/` altındadır, depoya girmez ve her komutta manifest'ten yeniden üretilir. Gerçek
yükleme Devoplus GitLab hattıyla yapılır; geliştirici makinesinden yükleme yoktur. Komutun ekrana yazdığı ayrıntılar
(araç adı, sürüm satırı) yerel çalışma ortamına aittir ve gizlenmez.

#### Güncelleme uyarısı

`one`, her komuttan önce kurulu SDK sürümünü paket deposundaki kanal etiketiyle karşılaştırır: ön sürüm (ör.
`0.2.0-next.150`) `next` etiketiyle, kararlı sürüm `latest` etiketiyle. Kurulu sürüm eskiyse hata akışına (stderr) şu
uyarıyı yazar; komut yine çalışır ve çıkış kodu değişmez:

```text
one: Devoplus One SDK'nın daha yeni bir sürümü var: kurulu 0.2.0-next.150, güncel 0.2.0-next.183 (ön sürüm kanalı "next").
one: Güncellemek için uygulamanın kök dizininde şunu çalıştırın: pnpm add @devoplus/one-sdk@next
one: Ardından npx one upgrade çalıştırın: sürümü tam sabitler ve one.manifest.json sdkVersion alanına yazar.
one: Bu denetimi kapatmak için ONE_NO_UPDATE_CHECK=1 tanımlayın.
```

| Konu | Davranış |
|---|---|
| Ne zaman sorulur | Yanıt 24 saat önbellekte tutulur; önbellek tazeyse ağa çıkılmaz. Bayatsa komut en çok 1,5 saniye bekler. Yanıt alınamayan deneme de kaydedilir ve 24 saat yinelenmez. |
| Önbellek | `$XDG_CACHE_HOME/devoplus-one/update-check.json`; değişken yoksa Linux `~/.cache/devoplus-one/`, macOS `~/Library/Caches/devoplus-one/`, Windows `%LOCALAPPDATA%\devoplus-one\Cache\`. Önbellek yazılamıyorsa denetim ağa çıkmaz. |
| Paket deposu | Varsayılan `https://registry.npmjs.org/`. Komut paket yöneticisi betiğiyle (`npm run`, `npm exec`, `pnpm run`) çalışıyorsa ve yapılandırmada başka bir depo tanımlıysa (`npm_config_registry`) o depoya sorulur. |
| Güncelleme komutu | Uygulamanın kilit dosyasına göre `pnpm add`, `yarn add` ya da `npm install`; komut uygulamanın değil genel (global) kurulumun SDK'sıyla çalışıyorsa `npm install -g`. |
| Kapatma | `CI` tanımlıyken (`0` ve `false` dışında) ya da `ONE_NO_UPDATE_CHECK=1` iken denetim hiç çalışmaz. Çıktının uçbirime gitmemesi denetimi kapatmaz. |
| Sessizlik | Her hata (çevrimdışı, ulaşılamayan depo, bozuk yanıt, yazılamayan önbellek) sessizdir. Uyarı yalnız stderr'e yazılır; `--json` çıktısı bozulmaz. |

Uyarıyı görürseniz başka bir işe geçmeden SDK'yı güncelleyin ve `npx one upgrade` çalıştırın, ardından
`pnpm type-check && pnpm test && pnpm build` çalıştırın ve `package.json`, kilit dosyası ile `one.manifest.json`'u kodla
birlikte yükleyin. Yayın hattının SDK sürüm denetimi [Sürümleme](#sürümleme) bölümündedir.

#### SDK sürümü beyanı ve yükseltme (`one upgrade`)

`one.manifest.json`'daki `sdkVersion` alanı uygulamanın derlendiği SDK sürümüdür ve **elle yazılmaz**. Yayın hattı onu
kurulu SDK sürümüyle karşılaştırır: farklıysa yayın durur; SDK 0.3.0 ve sonrasında alan eksikse de durur. Yazan iki yol
vardır: iskelet üretici (`create-devoplus-one-app`, güncel sürümü paket deposundan okur) ve `npx one upgrade`.

Yükseltme iki adımdır; ilk adım eski SDK'nın komutunu gerektirmez:

```bash
pnpm add @devoplus/one-sdk@latest # 1. SDK'yı uygulamanın paket yöneticisiyle ve izlediği kanalla güncelleyin (ön sürümde @next)
npx one upgrade                   # 2. yeni SDK'nın komutu: tam sürümle sabitler, sdkVersion'ı yazar
```

| Konu | Davranış |
|---|---|
| Paket yöneticisi | Kilit dosyasından: `pnpm-lock.yaml` ise pnpm, `package-lock.json` ise npm, `yarn.lock` ise yarn. İkinci bir kilit dosyası varsa ya da hiç yoksa komut **hiçbir şeye dokunmadan** durur (başka bir yöneticiyle kurmak ikinci kilit dosyası üretir ve yayın hattı durur). |
| Kanal | Kurulu sürüm ön sürümse `next`, kararlıysa `latest` izlenir (komut kanalı değiştirmez). Kanalın güncel sürümü paket deposundan okunur ve `package.json`'a tam yazılır (`--save-exact`). Paket deposuna ulaşılamazsa komut **hiçbir şeye dokunmadan** durur (kanal etiketi kurulmaz); SDK'yı kendiniz tam sürümle kurup `--no-install` ile çalıştırabilirsiniz. Kurulumun çıktısı stderr'e gider (`--json` çıktısı bozulmaz). |
| Eski sürüme inmez | Taban, kurulu sürüm, `package.json`'daki tam sürüm, manifestteki `sdkVersion` ve `one.platform-state.json`'daki işlenmiş/bekleyen SDK sürümünün (`legacy` hariç) en büyüğüdür. Kanaldaki sürüm tabandan eskiyse kurulum atlanır. Kurulu sürüm tabandan eskiyse (`git pull` sonrası `node_modules` güncel değil) komut durur ve önce `pnpm install` ister; taban yalnız `one.platform-state.json`'dan geliyorsa (SDK hiç kurulu değilse de) bağımlılıkları yeniden kurmak yetmez: komut SDK'yı bağımlılık tanımınızda o sürüme getirmenizi ya da dosyayı düzeltmenizi ister; paket yöneticisine özgü komut önermez. Kurulum istenen sürümü kurmadıysa manifest ve yükseltme durumu yazılmaz. |
| Çalışma alanı | `workspace:` ya da yerel dosya tanımında kurulum yapılmaz; kurulu sürüm yazılır. Kurulum yapılmadığında kilit dosyası aranmaz (çalışma alanı üyesinin kilit dosyası kökte durur). |
| `--no-install` | Kurmadan, kurulu sürümü yazar (SDK'yı az önce kendiniz kurduysanız). |
| Yükseltme durumu | Uygulama kökündeki `one.platform-state.json`: `processed` (değişiklik özeti işlenmiş son durum) ve `pending` (kurulan SDK sürümü ve o SDK'nın taşıdığı kural kataloğu revizyonu). `processed` bu komutla **ilerlemez**; yalnız `--complete` ilerletir. Dosya yoksa (yeni üretilen uygulamada da) önceki durum `legacy` sayılır ve ilk yükseltmenin özeti bütün geçmişi listeler: fazla bilgi verir, hiçbir şeyi atlamaz. Dosya var ama okunamıyorsa (ör. birleştirme çatışması) komut **hiçbir şeye dokunmadan** durur; işlenmiş durum ezilmez. Dosya **depoya girer**: ekip ve farklı makinelerdeki ajanlar aynı noktadan devam eder. `one.manifest.json`, `one.platform-state.json`, `package.json` ve kilit dosyası birlikte commit'lenir. |
| Kurallar ve denetim | Manifest ve yükseltme durumu yazıldıktan sonra **yeni kurulan** SDK'nın `one agents sync` ve `one doctor` komutları çalıştırılır: `AGENTS.md`'nin platform kuralları bölümü yeni katalogla yenilenir ve uygulama yeni kurallara karşı taranır. İkisinden biri düşerse yükseltme yine tamamlanmış sayılır; uyarı yazılır ve komutu elle çalıştırmanız istenir. Zorunlu bulgu yükseltmeyi durdurmaz, ama düzelmeden yayın hattı durur. |
| Çıktı | Önceki ve bekleyen durum, `AGENTS.md` bölümünün yenilenip yenilenmediği ve `one doctor` özeti; `--json` ile aynı bilgi JSON olarak (`agents`, `doctor`). Uyarılar stderr'e yazılır. |

Komut düşerse (kurulum hatası, okunamayan kurulu sürüm) manifest ve yükseltme durumu değiştirilmez. `AGENTS.md`, manifest ve
yükseltme durumuyla birlikte commit'lenir.

#### Güncelleme döngüsü ve `--complete`

Yükseltmeden sonra yapay zeka aracınız Devoplus One sunucusundan platform güncellemeleri özetini, `one upgrade` raporundaki
önceki işlenmiş durumla (`previous`) ister. Özet işlenmiş durumdan bu yana yeni ve metni değişen kuralları, bugün
zorunlu düzeydeki bütün kuralları ve o tarihten bu yana çıkan SDK sürümlerini (sürüm ve başlık; ayrıntılı notlar
uygulamanın kurulu SDK'sındaki `node_modules/@devoplus/one-sdk/CHANGELOG.md` dosyasında) verir. Özetin ilk satırı her zaman aynı uyarıdır: liste sunucunun
bildiği katalogdan üretilir ve yayın hattınınkinden eski olabilir, yayında esas olan hattın doğrulama çıktısıdır. Özet
hiçbir uygulamanın kurallara uyduğunu söylemez.

Bulgular düzeltilince özetin sonundaki komut değiştirilmeden çalıştırılır:

```bash
npx one upgrade --complete --from legacy@0 --catalog-revision 2 --sdk-version 0.3.0 --env test
```

| Konu | Davranış |
|---|---|
| Özetin durumu | `--from`, özetin hesaplandığı işlenmiş durumdur (`<sdk>@<revizyon>`). Dosyadaki `processed` ile birebir aynı değilse hiçbir şey yazılmaz: özet başka bir durumdan istendiyse arada kalan kurallar ve sürümler hiç gösterilmemiştir. |
| Doğrulama | `--catalog-revision` ve `--sdk-version` aynı ortamın Devoplus One sunucusundaki kural kataloğuyla karşılaştırılır (`--env`, varsayılan `test`; `ONE_ENV` ya da `ONE_AGENT_BASE` ile de verilir). Revizyon sunucunun yayımladığından büyükse, sürüm sunucunun sürüm notlarının kapsadığından yeniyse ya da sunucuya ulaşılamazsa hiçbir şey yazılmaz. |
| Katalog revizyonu | `processed.catalogRevision` verilen revizyon olur; daha küçükse değişmez. |
| SDK sürümü | `processed.sdkVersion`, verilen sürüm ile kurulu (`pending`) sürümün küçüğü olur; işlenmiş sürümden eskiyse değişmez. Sunucu geride kaldıysa kalan sürüm notlarını bir sonraki çağrı listeler. `--sdk-version` verilmezse değişmez. |
| Ön koşul | `one.platform-state.json` ve içinde `pending` olmalıdır (önce `npx one upgrade`). Kurulu SDK'nın kural kataloğu okunamadıysa `pending.catalogRevision` boştur; `--complete` yine çalışır. |
| Eski SDK | `--complete`'i tanımayan bir SDK komutu tam bir yükseltme olarak çalıştırır ve işlenmiş durumu ilerletmez. Çıktıda "İşlenmiş durum:" satırı yoksa önce SDK'yı güncelleyin. |
| Geri gitmez | İşlenmiş durum hiçbir değerle geri alınmaz. |

Döngü yarıda kesilirse işlenmiş durum değişmez ve sonraki yükseltme aynı yerden başlar. `one.platform-state.json`
dosyasını depoya ekleyin.

#### Platform kuralları: `one doctor` ve `AGENTS.md` bölümü

Platformun uygulamalardan beklediği kurallar bir **kural kataloğunda** tutulur. Her kuralın bir kimliği (`ONE-RULE-008`
gibi), düzeyi (zorunlu, uyarı, öneri), hangi SDK sürümünden itibaren geçerli olduğu ve bir düzeltme yönergesi vardır.
Katalog bir **revizyon** numarası taşır; her SDK sürümü yayımlandığı anki kataloğun bir kopyasını getirir. Yayın hattı
aynı kataloğu kullanır ve doğrulama çıktısında uygulanan revizyonu yazar.

| Komut | Davranış |
|---|---|
| `npx one doctor` | Uygulamayı kurulu SDK'nın katalog kopyasına karşı tarar. Denetim, yayın hattının kataloğa bağlı kapılarıyla **aynı koddur**: manifest doğrulaması, veri modeli, arayüz ve SDK beyanı. Hattın diğer kapıları (belge ve ajan rehberi kapıları, giriş dosyası denetimi, SDK asgari sürüm denetimi, sır taraması, bağımlılık zafiyet taraması, kod güvenlik taraması, migration sürekliliği) yalnız hatta çalışır; `one doctor` onları çalıştırmaz. Uymayan her kural kimliği, düzeyi, konumu ve düzeltme yönergesiyle yazılır (hattın çıktısıyla aynı bilgi, daha kısa biçimde); kuralın metni bir kez yazılır. Çıktı `--json` ile bile tek seferde ve eksiksiz yazılır, başka bir komuta aktarılabilir. Dosyaya yazmaz. Zorunlu bulgu ya da manifest sorunu varsa çıkış kodu 1, yoksa 0'dır. `--json` raporu (`catalogRevision`, `sdkVersion`, `manifestProblems`, `findings`, `summary`, `passes`) JSON olarak yazar. |
| `npx one agents sync` | `AGENTS.md`'nin platform kuralları bölümünü kurulu SDK'nın katalog kopyasıyla yeniler. Bölüm iki işaret satırının arasındadır (`<!-- devoplus-one:platform-rules:start -->` ve `<!-- devoplus-one:platform-rules:end -->`); yalnız aralarındaki metin değişir. İşaret yoksa bölüm dosyanın **sonuna** eklenir. İşaretlerden biri eksikse, birden çok varsa ya da sırası tersse hiçbir şey yazılmaz ve komut düşer. İşaret yalnız tek başına bir satırda ve kod bloğunun dışında sayılır: belgede alıntılanan işaret bölüm sanılmaz. Kapanmamış bir kod bloğu varsa hiçbir şey yazılmaz. `AGENTS.md` yoksa komut düşer (dosyayı uydurmaz). Satır sonu dosyanınkiyle aynıdır. Değişiklik yoksa dosyaya dokunulmaz. |

Bölümü elle düzenlemeyin; bir sonraki eşitlemede yeniden yazılır. Uygulamaya özgü notlarınızı bölümün dışına yazın.
Yeni üretilen uygulamanın `AGENTS.md`'si bölümü baştan taşır.

`one doctor` uygulamanın "uyumlu" olduğunu söylemez: kurulu SDK'nın getirdiği katalog kopyası, yayın hattının kullandığı
güncel katalogdan eski olabilir. Yayında esas olan hattın doğrulama çıktısıdır. Güncel kuralları görmek için SDK'yı
`npx one upgrade` ile yükseltin.

#### SDK sürüm başlığı

SDK kendi sürümünü `SDK_VERSION` sabitinde taşır (yayımlanan pakette paketin tam sürümü, ör. `0.3.0-next.214`) ve
**yalnız kendi ürettiği** yanıtlara (`problemResponse`, kayıt işlemleri, dışa aktarma, bileşen dosyaları, iş ve ajan
araçlarının yanıtları) ve Devoplus servislerine giden kendi isteklerine `X-One-Sdk-Version` başlığını yazar.
Uygulamanın kendi kurduğu yanıta dokunulmaz; aynı başlığı taşımasını istediğiniz yanıtı `sdkResponse(gövde, seçenekler)`
ile kurun. Platform bu başlıktan yalnız uyumluluk seçimleri yapabilir; güvenlik kuralları başlığa bağlanmaz.

### Yerel Portal ve servis taklidi (`one dev`)

`one dev` üç yerel adres açar ve üçü de yalnız `127.0.0.1`'de dinler (aynı ağdaki başka bir bilgisayar erişemez). Yerel
Portal ve uygulamanın kapısı yalnız `127.0.0.1` ya da `localhost` adıyla gelen isteği kabul eder; çalışma ortamının iç
adresi bu ad denetimini YAPMAZ (aşağıdaki tablo):

| Adres | Ne |
|---|---|
| `http://127.0.0.1:8787/` (`--port`) | Yerel Portal: uygulamayı Portal gibi çerçevede açar ve köprünün Portal tarafını oynar (`one:token`, `one:context`, menü sekmeleri, bildirim, onay penceresi, indirme, dış bağlantı, karartma, kaydedilmemiş değişiklik). Sayfanın yolu uygulamanın yoludur (`/bridge` uygulamanın `/bridge`'ini açar). Kişi, oturum türü (kendi oturumu, Vekalet, Yerine Görüntüle), dil ve tema sayfadan seçilir. Devoplus API taklidi de buradadır (`/api/v1/...`); uygulamanın `API_BASE`'i yerelde bu adrestir. |
| `http://127.0.0.1:8788/` (`--port` artı bir) | Uygulamanın kapısı: platform kapısının kurallarını uygular. İstemcinin yazdığı `X-One-*` başlıklarını siler, kimlik bağlamını yerel belirteçten yazar (belirteç yoksa anonim), belirteçsiz yazmayı `ONE-DSP-E-007`, geçersiz belirteci `ONE-DSP-E-003`, `/__one/` yolunu `ONE-DSP-E-011`, başkası adına oturumda yazmayı (`actingSessions: "write"` değilse) `ONE-DSP-E-009`, başka bir portalın kişisine verilmiş belirteci `ONE-DSP-E-005` ile reddeder; yanıta platformun içerik güvenliği politikasını (satır içi betik kapalı), aygıt izinlerini ve güvenlik başlıklarını ekler. Platformda her kurulumun kendi adresi olduğu gibi yerelde de her portalın kendi uygulama adresi vardır: açılış kişisinin portalı bu porttadır, `portals` listesindeki diğer portallar boş bir portta dinler ve Portal kabuğu seçilen kişinin portalının adresini kendisi açar (belirteçsiz isteğin portalı adrestir; bir sekmede seçilen kişi başka sekmeyi etkilemez). Yerel belirteç 12 saat geçerlidir; süresi dolunca kapı ve taklit 401 döner, kabuk sayfasını yenilemek yeni belirteç verir. |
| `http://127.0.0.1:8789/` (`--port` artı iki) | Yerel çalışma ortamının iç adresi. Ad (Host) denetimi YOKTUR ve gelen `X-One-*` başlıkları doğrulanmadan uygulamaya ulaşır: bu adrese istek gönderebilen her sayfa, kimliği istediği gibi yazarak uygulamanın yerel verisini okuyup değiştirebilir. Yalnız iş işleyicisini komut satırından elle denemek içindir ([Zamanlanmış görev ve kuyruk](#zamanlanmış-görev-ve-kuyruk)); tarayıcıda açmayın. `one dev` açılışta bunu konsola uyarı olarak yazar. |

Kişiler ve örnek veri uygulamanın kökündeki `one.dev.json` dosyasındadır (yoksa iki örnek kişi: `gelistirici` ve
yöneticisi `mudur`). Dosya sır taşımaz ve depoya eklenebilir; bozuk bir alan açık bir hatayla reddedilir.

| Alan | Anlamı |
|---|---|
| `portals` | `[{ "key": "yerel", "name": "Yerel Portal", "id": "<24 onaltılık, isteğe bağlı>" }]`. Varsayılan tek portal `yerel`. Yerel veritabanı, dosya ve önbellek portallar arasında ORTAKTIR (platformda her kurulumun kendisinindir). |
| `defaultPerson` | `--as` verilmezse oturumu açan kişi (varsayılan ilk kişi). |
| `people.<anahtar>` | Kişi (anahtar küçük harf, rakam ve tire). `name` zorunlu; `email`, `phone`, `portal`, `title` (`titleEn`), `department`, `position`, `workplace`, `userCode`, `costCenterCode` (DEXA bağlam kodu, aşağıda), `manager` ve `operationalManager` (kişi anahtarı, aynı portalda), `permissions` (yoksa uygulamanın bütün izinleri; ad alanı dışındaki kod platformdaki gibi iletilmez), `scopes` (yoksa manifest'in bütün yetki alanları; süreli onay gerektirenleri çıkararak onay verilmemiş oturumu deneyin), `admin` (portal yöneticisi), `globalAdmin`, `active` (`false` = ayrılmış kişi), `push` (`false` = bildirim alamaz), `sub` ve `orgUserId` (24 onaltılık; verilmezse kişiden kararlı biçimde üretilir). İki kimlik platformdaki gibi AYRIDIR: `sub` hesap kimliği, `orgUserId` organizasyon şeması kimliğidir. |
| `groups.<anahtar>` | Kullanıcı grubu: `{ "name": "Satış Onay", "members": ["ayse"], "portal": "yerel" }`. |
| `channels` | `{ "sms": "open" \| "closed" \| "disabled", "push": "open" \| "closed" }`, Devoplus backend kodunun varsayılanlarıyla. SMS varsayılanı `open`: kanal açık ortamdaki gibi ileti yerelde posta kutusuna yazılır; `disabled` SMS kanalı ortamda kapalıymış gibi 500 `ONE-APP-E-110` döner; `closed` kanal açık ama portalın ve sistemin SMS sağlayıcısı tanımlı değilmiş gibi 500 `ONE-APP-E-021` döner. Mobil bildirim varsayılanı `closed`: backend kanal ortamda ya da portalda açık değilse kuyruğa almadan 500 `ONE-APP-E-029` döner ve kanalın varsayılan ayarı kapalıdır; `open` iletiyi posta kutusuna yazar (açık kanalı denemek için). |
| `dexa.queries.<anahtar>` | Sorgunun örnek yanıtı: `parameters` (sorgunun bildirdiği parametre adları; bildirilmeyen ad `ONE-APP-E-026`), `columns` (yoksa ilk satırın alanları), `rows`, `cases` (`[{ "when": { "Yil": "2025" }, "rows": [] }]`, parametreye göre yanıt), `unavailable` (`true` = "şu anda getirilemiyor"), `truncated`, `appContext` (`true` = kurulumda "kodları uygulama belirler" onayı var; yoksa `UserCode`, `CompanyCode`, `CostCenterCode` gönderimi `ONE-APP-E-026`). Sorgu bir bağlam kodu bildiriyor ve uygulama onu göndermiyorsa değer oturumdaki kişiden gelir: `UserCode` kişinin `userCode`'u, `CompanyCode` portalın şirket kodu (organizasyon şemasındaki `companyCode`, büyük harfle portal anahtarı), `CostCenterCode` kişinin `costCenterCode`'u; değer yoksa ya da kişi pasifse 403 `ONE-APP-E-027`. Gönderilen kod backend kuralıyla denetlenir: kırpıldıktan sonra boş, 64 karakterden uzun ya da kontrol karakterli değer 400 `ONE-APP-E-076`. Kodun portalda tanımlı olup olmadığı (`ONE-APP-E-077`) yerelde denetlenmez. |
| `dexa.commands.<anahtar>` | İşlemin örnek sonucu: `outcome` (`succeeded`, `rejected`, `inDoubt`, `pending` = 409 `ERPCMD-APP-E-016` + `Retry-After`, `notSent` = 424 `ERPCMD-APP-E-013`) ve `erp` (`headerId`, `documentNumber`, `exceptionMessage`, `lines`, `raw`, `shape`, `isSuccessful`). Aynı `Idempotency-Key` aynı içerikle yeniden gönderimdir (`replayed: true`), farklı içerikle 409 `ERPCMD-APP-E-015`. ERP'ye hiçbir şey yazılmaz. |

Taklit edilen Devoplus uçları ve kuralları (Devoplus backend sözleşmesiyle aynı; belirteç yoksa ya da tanınmıyorsa 401):

| Uç | Yerelde |
|---|---|
| `GET Users/me` | Oturumdaki kişi ve `orgContext` (org kimliği, kullanıcı kodu, departman, unvan). |
| `GET OrganizationManagement/organization-schema`, `users[?hideInactiveDocuments=false]`, `user-groups/{id}/users`, `users/{id}/user-groups` | Kişilerden ve gruplardan türetilir; yalnız oturumdaki kişinin portalı, şemada yalnız aktif kayıtlar. Yetki alanı `orgschema.read` yoksa 403 `ONE-APP-E-022`. |
| `POST One/services/mail`, `sms`, `push` | Mobil bildirim varsayılan olarak kapalıdır (500 `ONE-APP-E-029`, `channels` satırı). Doğrulama (400 `GEN-VAL-E-001`: en az bir, en çok 50 alıcı; konu 200, e-posta metni 10.000, SMS 300, bildirim başlığı 100 ve metni 300 karakter; bildirim `path`'i Portal yol kuralı), yetki alanı (403 `ONE-APP-E-017`), Vekalet / Yerine Görüntüle oturumunda 403 `IDN-APP-E-080`, alıcı kuralları (`invalid-subject`, `invalid-org-user`, `not-in-portal`, `inactive`, `no-address`, `no-phone`, `not-deliverable`; aynı kişi bir kez). Gönderilmez: posta kutusuna yazılır. |
| `POST One/services/audit` | Doğrulanır ve konsola yazılır (`{ "recorded": true }`). |
| `POST One/services/dexa/query` | Örnek veriden; tanımsız anahtar 403 `ONE-APP-E-024`, `LangCode` ve onaysız bağlam kodu 400 `ONE-APP-E-026`, biçimce geçersiz bağlam kodu 400 `ONE-APP-E-076`, oturumdan çözülemeyen bağlam kodu 403 `ONE-APP-E-027`, yetki alanı `dexa.read`. |
| `POST One/services/dexa/command`, `GET One/services/dexa/command/{anahtar}` | Örnek veriden; tanımsız anahtar 403 `ONE-APP-E-030`, `Idempotency-Key` eksik ya da bozuk 400, yetki alanı `dexa.write`, Vekalet / Yerine Görüntüle oturumunda 403 `IDN-APP-E-080`. Kayıtlar süreç kapanınca silinir. |
| `POST One/services/ai` | Yerelde yoktur: 503 `ONE-APP-E-050`. |
| Diğer uçlar | 404 `ONE-DEV-E-001`. |

Hata gövdesi Devoplus backend'inin biçimindedir: `type` ve `title` HTTP durumundan gelir (ör. 400 için
`https://tools.ietf.org/html/rfc9110#section-15.5.1` ve `Bad Request`), ayrım `code` alanındadır; doğrulama hatasında
`errors` dizisinin her satırı `code` taşır (kuralın kodu, yoksa doğrulayıcının adı: `NotEmptyValidator`,
`MaximumLengthValidator`, `PredicateValidator`). `traceId` yerelde bu sürecin opak bir kimliğidir; biçimine
güvenmeyin. Uygulamanın kapısının hataları (`ONE-DSP-E-*`) platform kapısının biçimindedir (`type: "about:blank"`).

Posta kutusu: `http://127.0.0.1:8787/__one-dev/posta-kutusu` (beş saniyede bir yenilenir; JSON biçimi `posta-kutusu.json`).
İletiler `.one/mailbox/messages.jsonl` dosyasındadır; e-postanın alıcıya giden gövdesi Devoplus'ın şablonuyla gösterilir.
Komut satırından denemek için oturum belirteci `GET http://127.0.0.1:8787/__one-dev/session?as=<kişi>` yanıtının
`token` alanındadır (`&kind=delegation|impersonation&actor=<kişi>` ile başkası adına oturum); uygulamaya
`Authorization: Bearer <token>` ile gönderin. Yerel belirteç yalnız bu süreçte geçerlidir.

Yerelde taklit EDİLMEYENLER: gönderim hız sınırları, kullanıcının süreli onay ekranı, kurulum onayları, arka plan işinin
ve ajan aracının Devoplus servis çağrıları (belirteçsiz servis çağrısı 401 alır), gönderilen DEXA bağlam kodunun portalda
tanımlı olup olmadığı (`ONE-APP-E-077`), Devoplus yapay zekası, gerçek ERP verisi. Bunlar platformda (test ortamı)
denenir. `Idempotency-Key` yerelde yalnız tireli UUID biçiminde ve sıfır olmayan değerle kabul edilir (SDK'nın
`newIdempotencyKey()` ürettiği biçim); backend daha fazla yazımı kabul eder.

### Erişim talebi: `one login`, `one request`, `one requests`

Uygulamanız bir kuruluma, DEXA sorgusuna, uygulama izni atamasına ya da yetki alanına ihtiyaç duyduğunda izni
yöneticiden komut satırından isteyebilirsiniz. Talep izni kendiliğinden VERMEZ; yönetici Devoplus Console'da onaylar ve
sonuç size e-postayla bildirilir.

| Komut | Ne yapar |
|---|---|
| `one login [--env test\|uat\|prod]` | Tarayıcıda kod onayıyla giriş (portal orada seçilir). İlk girişte komut bu bilgisayarı kimlik sağlayıcıya kendisi kaydeder (yalnız erişim talebi, yayın ve kullanım panosu yetkisiyle) ve kimliği `~/.config/devoplus-one/clients.json`'da saklar; oturum `~/.config/devoplus-one/session.json`'da, ikisi de yalnız sizin okuyabileceğiniz izinle durur. Sabit bir istemci kullanmak için `--client-id` / `ONE_CLIENT_ID`. |
| `one request <tür> ...` | Talep gönderir; türler: `registration`, `installation`, `dexa-query`, `dexa-command`, `app-permission`, `scope`, `new-query`. `one request` yazınca türlerin alanlarını listeler. Kısa ad `one.manifest.json`'dan okunur. |
| `one requests [kimlik]` | Taleplerinizi ve durumlarını listeler. |
| `one context [--json]` | Oturumdaki portalın adını, kimliğini ve var olan kısa adını (portal anahtarı; yoksa yok der) gösterir. Kurulum talebinde portal anahtarı seçilmez; sunucu onu portalın adından belirler. |
| `one dexa queries [--search <metin>] [--json]` | Oturumdaki portalda Devoplus One uygulamalarına açılmış DEXA sorgularını listeler: anahtar, yön (okuma ya da ERP'ye yazma), bugün kullanılıp kullanılamadığı ve nedeni, açıklama, giriş parametreleri (tip adı; platformun oturumdan doldurduğu bağlam kodları işaretli: okumada `LangCode`, `UserCode`, `CompanyCode`, `CostCenterCode`, yazmada `LangCode`, `CompanyCode`) ve dönen sütunlar. `--search` anahtar, açıklama, parametre ya da sütun adında arar. Liste izin vermez: sorgu `dexa-query` talebiyle istenir. SQL metni ve test değerleri gösterilmez. |
| `one logout` | Oturumu kimlik sağlayıcıda iptal eder ve dosyayı siler. |

Uygulama içi izinler kayıt talebinde istenir: `one request registration --title "<ad>" --permission
One.Hello.Manage="Yönetir"` (tekrar edilebilir, en çok 16; İngilizce ad `--permission-en <kod>="<ad>"`). Kayıt talebini
portalın şirket yöneticisi, yetki alanı talebini platform yöneticisi onaylar. Kayıt talebi dışındaki türleri yalnız uygulamanın sahibi gönderebilir. Vekalet ya da Yerine Görüntüle oturumuyla talep
gönderilemez. Aynı istemci Devoplus One MCP sunucusunda da kullanılır (`@devoplus/one-sdk/access-request`).
Kayıttan sonra izin eklemenin talep türü yoktur: izni uygulamanın ana portalının şirket yöneticisi Devoplus Console'dan
ekler (ana portalı belirlenemeyen uygulamada platform yöneticisi).

Talep reddedildiğinde dönen hatalar:

| Durum | Ne olur |
|---|---|
| Talebi gönderen uygulamanın sahibi değil (kayıt hariç) | 403 `ONE-APP-E-039`; talebi uygulamanın sahibi gönderir |
| Kısa ad zaten kayıtlı (kayıt talebi) | 409 `ONE-APP-E-038`; başka bir kısa ad seçin |
| Kısa ad pasife alınmış bir uygulamaya ait | 409 `ONE-APP-E-107`; bu kısa ad yeniden kaydedilemez, başka bir kısa ad seçin |
| Aynı talep zaten bekliyor | 409 `ONE-APP-E-040`; yeniden göndermeyin, `one requests` ile izleyin |
| Kişi başına saatte 20 talep aşıldı | 429 `ONE-APP-E-041` |
| İzin kodu uygulamanın ad alanında değil | 400 `ONE-APP-E-047`; kod `One.<Uygulama>.<Eylem>` biçiminde olmalı |
| Ek izin biçimsiz, ad alanı dışında, ana kodla aynı ya da yinelenen; 16'dan fazla | 400; listeyi düzeltip yeniden gönderin |
| `app-permission` kodu uygulamanın kaydında yok | 400; izni önce uygulama kaydına ekletin (ana portalın şirket yöneticisi, Devoplus Console) |
| Oturumda portal yok | 403 `ONE-APP-E-037`; yeniden giriş yapıp portal seçin |
| Vekalet ya da Yerine Görüntüle oturumu | Talep gönderilmez; kendi oturumunuzla bağlanın |

### Yayın ve kullanım panosu: `one publish`, `one pipeline`, `one promote`, `one rollback`, `one deployments`, `one status`

Kod bu bilgisayardan yayımlanmaz: `one publish` kaynağı **Devoplus API'si üzerinden** uygulamanın deposuna (test dalı)
yazar ve bir **yayın isteği** bırakır; Devoplus isteği kaydeder ve yayın hattını kendisi başlatır, hattın kapılarının
hiçbiri atlanmaz. Komutlar `one login` oturumuyla çalışır; uygulamanın
kısa adı `one.manifest.json`'dan okunur (`--app` ile değişir), `--json` ham yanıtı basar.

| Komut | Ne yapar |
|---|---|
| `one publish [--portal <anahtar>] [--message <açıklama>] [--no-upload]` | Uygulamanın kök dizininden projenin tam görüntüsünü yükler ve test ortamına yayınlar. Dizin bir git deposuysa dosyalar git'in kurallarıyla seçilir (izlenen ve yok sayılmayan dosyalar); değilse dizin gezilir ve kökteki `.gitignore` yalnız basit kalıplarla uygulanır (`ad`, `ad/`, `/ad`, başta ya da sonda tek `*`), desteklenmeyen kalıpta komut durur. Git dizini okuyamazsa (bozuk dizin, güvenilmeyen sahiplik), çalışma ağacı seyrekse (sparse checkout) ya da çözülmemiş birleştirme çakışması varsa komut durur ve yükleme yapılmaz: yok sayma kuralları uygulanmadan sır dosyaları görüntüye girebilir ya da dizinde olmayan izlenen dosyalar silinmiş sayılırdı. Bağımlılık ve yerel durum klasörleri, `.gitlab-ci.yml` ve sır dosyaları yüklenmez ve hangi dosyanın atlandığı yazılır. Tam görüntüde listede olmayan dosya depodan silinir. Sınırlar: 2.000 dosya, dosya başına 2 MB, toplam 10 MB, uygulama başına saatte 60 yükleme; yalnız test ortamında. `--no-upload` yüklemeden depodaki kodu yeniden yayınlar. Hat bitince uygulama test Portal'ında açılır; adresi (`https://<kısa-ad>-<anahtar>.test-one.devoplus.dev`) paylaşılabilir önizlemedir. |
| `one pipeline [--portal <anahtar>] [--deployment <kimlik>] [--job <iş>]` | Yayın isteğinin hattını (işler, durumlar, süreler) ve bir işin günlüğünü gösterir. `--deployment` yoksa en son istek, `--job` yoksa son başarısız iş, o da yoksa en son iş. Günlük sunucuda süzülür (yalnız betik bölümü ve son kısmı, sırlar maskeli). |
| `one promote [--portal <anahtar>]` | Sürümü bir sonraki ortama taşır: komut hedef ortamın oturumuyla çalışır (`one login --env uat` sonrası test'ten uat'a). Kod birleştirmesini ve `v<sürüm>` etiketini Devoplus yapar; yalnız kaynak ortamda başarıyla yayınlanmış kod taşınır. Canlıya terfi bu komutla yapılamaz (`ONE-APP-E-099`): uygulamanın sahibi Portal'daki "Devoplus One Uygulamalarım" ekranından, şirket yöneticisi Devoplus Console'dan başlatır. |
| `one rollback [--portal <anahtar>] [--to <x.y.z>] --yes` | Önceki sürüme (ya da verilen sürüme) döner (test ve uat; canlıda sahip Portal'daki "Devoplus One Uygulamalarım" ekranından, şirket yöneticisi Devoplus Console'dan). Kod `v<x.y.z>` etiketinden alınır; etiketi Devoplus her terfide yazar, hiç terfi etmemiş sürüme dönülemez. `--to` yoksa dalın ucundaki sürümden küçük en büyük etiket. Veritabanı da o sürümün yapısına dönmek zorundaysa dönülen noktadan sonra kaydedilen veriler **kaybolur**; komut `--yes` olmadan hiçbir istek göndermez. |
| `one deployments [--portal <anahtar>]` | Yayın isteklerinizi ve durumlarını listeler. |
| `one status [--portal <anahtar>] [--days 7]` | Kullanım ve hata özeti: istek ve hata sayısı, işlemci süresi, günlük dağılım, son hata kayıtları (`logError`), başarısız arka plan işleri. Kişisel veri ve ileti gövdesi döndürülmez. |

- Kodu yalnız uygulamanın sahibi yükleyebilir; yalnız sahip ya da o portalın şirket yöneticisi yayın isteyebilir ve
  panoyu görebilir; kararı sunucu verir.
- `--portal` verilmezse istek portal anahtarı taşımaz ve kurulumu sunucu seçer. Kısa ad, portal anahtarı, sürüm ve gün
  sayısı istek gönderilmeden denetlenir; tanınmayan bir seçenek (`--portl`) açık hatadır.
- Yanıt *"Yayın hattı bu ortamda yapılandırılmamış"* ya da *"Kullanım ve hata panosu bu ortamda yapılandırılmamış"*
  ise o ortamda bu özellik henüz kullanılamıyor demektir.
- Aynı istemci Devoplus One MCP sunucusunun yayın ve kullanım panosu araçlarında da kullanılır
  (`@devoplus/one-sdk/app-ops`).

### Bağımlılık taraması ve yama: `one deps patch`

Devoplus her gün uygulamanın bağımlılıklarını bilinen güvenlik açıklarına karşı tarar (test dalı; uat ve main varsa
onlar da). Açık bulunursa ve düzeltilmiş sürüm varsa platform bir yama önerir (`package.json` ve `pnpm-lock.yaml`).
Yama **kendiliğinden uygulanmaz** ve depoda birleştirme isteği açılmaz: kodun doğru kaynağı yerel kopyanızdır, depoya
kendiliğinden yazılan bir yama bir sonraki `one publish` ile geri alınırdı. Yamayı yerelde uygular, testleri çalıştırır
ve her zamanki gibi yüklersiniz.

| Komut | Ne yapar |
|---|---|
| `one deps patch [--ref test\|uat\|main] [--json]` | Son taramanın bulgularını (paket, önem, kayıt, düzeltildiği sürüm), sunucunun notlarını ve varsa önerilen yamayı gösterir. Hiçbir dosya yazılmaz. `--ref` verilmezse test. Yama yalnız test için önerilir; uat ve main yalnız taranır, oradaki açık test'te düzeltilip terfi edilince kapanır. |
| `one deps patch --apply [--ref test\|uat\|main]` | Yamayı bu dizine uygular. Yalnız kök dizindeki `package.json` ve `pnpm-lock.yaml` değişir; commit atılmaz, hiçbir şey yüklenmez. Şu durumlarda hiçbir dosyaya dokunulmaz ve komut hata verir: yama başka bir dosyaya dokunuyor ya da dosya ekliyor, siliyor, adını değiştiriyor; git bulunamıyor ya da dizin bir git deposu değil; bu iki dosyada kaydedilmemiş değişiklik var; yama yerel koda uymuyor (`git apply --check`). |

- Uyguladıktan sonra bağımlılıkları kurun (`pnpm install`), `pnpm test` ve `one build` çalıştırın, sonra `one publish`
  ile yükleyin. Yükleme yayını başlatır ve hattın kapıları (bağımlılık kapısı dahil) yine çalışır.
- Yama uymuyorsa ya da hiç yoksa bulgulardaki paketleri düzeltildiği sürüme elle yükseltin. Düzeltilmiş sürümü olmayan
  paket için yama önerilmez.
- Sonuç yoksa (`available: false`) notlar nedeni söyler (ör. henüz tarama yapılmadı); bulgu ya da yama gösterilmez.
- Yalnız uygulamanın sahibi ya da platform yöneticisi görebilir; başkası `403` alır. Tarama hattının adresi yalnız
  platform yöneticisine gösterilir.
- Aynı istemci (`getDependencyPatch`, `describeDependencyPatch`) Devoplus One MCP sunucusunda da kullanılır.

## Hızlı başlangıç

```ts
import {
  BackendClient, OneFiles, problemResponse, readContext, requireScope, requireUser, sendMail,
  type OneApp, type OneDatabase, type OneFilesBinding,
} from '@devoplus/one-sdk'

interface Env { DB: OneDatabase; FILES: OneFilesBinding; API_BASE: string }

export default {
  async fetch(request, env): Promise<Response> {
    try {
      const ctx = readContext(request.headers)          // platform kapısının yazdığı X-One-* başlıkları
      const url = new URL(request.url)

      if (url.pathname === '/api/me') {
        requireUser(ctx)                                 // oturum yoksa 401
        const api = new BackendClient({ baseUrl: env.API_BASE, authorization: request.headers.get('authorization') })
        return Response.json(await api.get('Users/me'))
      }

      if (url.pathname === '/api/notify' && request.method === 'POST') {
        const user = requireScope(ctx, 'mail.execute')   // scope manifest'te ve belirteçte yoksa 403
        const api = new BackendClient({ baseUrl: env.API_BASE, authorization: request.headers.get('authorization') })
        return Response.json(await sendMail(api, { toSubjects: [user.sub], subject: 'Merhaba', text: 'Deneme' }))
      }

      if (url.pathname === '/api/files' && request.method === 'POST') {
        requireUser(ctx)
        const files = new OneFiles(env.FILES)
        const info = await files.put(`uploads/${crypto.randomUUID()}`, await request.arrayBuffer(), {
          contentType: request.headers.get('content-type') ?? 'application/octet-stream',
        })
        return Response.json(info, { status: 201 })
      }

      return new Response('Bulunamadı', { status: 404 })
    } catch (error) {
      return problemResponse(error)                      // OneError RFC 9457 yanıtına çevrilir; bilinmeyen hata 500
    }
  },
} satisfies OneApp<Env>
```

## Kimlik bağlamı

| Üye | Açıklama |
|---|---|
| `readContext(headers): OneContext` | `X-One-App`, `X-One-Portal-Key`, `X-One-Portal`, (`X-One-Auth=user` ya da kuyruk işinde `job` + `X-One-Sub`, `X-One-Scope`, `X-One-Global-Admin`, `X-One-Permissions`, `X-One-Admin`). Üç kurulum başlığından biri yoksa `ONE-SDK-E-001` (500): istek dispatch üzerinden gelmemiştir. |
| `OneContext` | `{ app, portalKey, portalId, user: OneUser \| null, actor?: OneActor \| null, mode?, admin?, jobServices? }`. `portalId` kiracıdır (24-hex); veri her zaman buna bağlıdır. `mode`: `'user'` (kullanıcının isteği), `'job'` (kuyruk / zamanlanmış iş), `'agent'` (Devoplus ajanının araç çağrısı). `admin` = `user.admin` (oturumsuzda ve işte `false`). `jobServices`: işte manifest `jobs.services` beyanı, diğer yollarda boş. |
| `OneActor`, `isActingSession(ctx)` | Vekalet / Yerine Görüntüle oturumu (`X-One-Actor-Sub` + `X-One-Session-Kind`): `{ sub, kind: 'delegation' \| 'impersonation' }`. `user.sub` adına çalışılan kişidir (veri onundur), `actor.sub` oturumu kullanan kişidir; "kim yaptı" kaydına bu yazılır. Kendi oturumunda `null`. ⛔ Bu oturumlar varsayılan **salt-okunurdur**: platform GET/HEAD dışındaki isteği uygulamaya ulaşmadan **403 `ONE-DSP-E-009`** ile reddeder; uygulama manifest'te `"actingSessions": "write"` beyan ederse yazma isteği geçer. |
| `OneUser` | `{ sub, scope: readonly string[], globalAdmin, permissions: readonly string[], admin }`. `sub` devoplus-auth kimlik uzayıdır (`UserAccount._id`), Organization Schema `User._id` DEĞİLDİR. `permissions` yalnız bu uygulamanın ad alanındaki (`One.<Uygulama>.*`) izinlerdir; `admin` = şirket ya da küresel yönetici rolü. |
| `requireUser(ctx): OneUser` | anonimse `ONE-SDK-E-002` (401). |
| `requireScope(ctx, scope): OneUser` | belirteçte scope yoksa `ONE-SDK-E-003` (403). Onaya bağlı scope'ta metin kullanıcıya izni nereden vereceğini de söyler. |
| `requireServiceScope(ctx, scope)` | Devoplus servisinin yerel kapısı, üç yolu ayırır (bkz. "Devoplus servisleri arka plan işinde ve ajan aracında"): kullanıcıda `requireScope`; ajan aracında süreli onay gerektiren scope'lar hiç kullanılamaz; işte kullanıcı gerekmez, scope `jobs.services`'te beyanlı olmalı. İhlal `ONE-SDK-E-003` (403); asıl karar sunucudadır. |
| `CONSENT_SCOPES`, `awaitsUserConsent(ctx, scope): boolean` | Kullanıcının **ayrıca ve süreli** onay vermesi gereken scope'lar: `dexa.write`, `mail.execute`, `sms.execute` ve `push.execute`. `awaitsUserConsent` scope onaya bağlıysa ve belirteçte yoksa `true` döner; ilgili düğmeyi kapatıp kullanıcıya "portaldaki İzinleri incele düğmesiyle izin verin" demek için. ⚠️ Manifest'te hiç beyan edilmemiş scope için de `true` döner (beyan uygulamanın bilgisidir). |
| `hasPermission(ctx, code): boolean` | uygulama izni: kullanıcıda kod varsa ya da `admin` ise `true` (backend'in yönetici baypasıyla aynı); anonimde `false`. Kod manifest `permission`/`permissions` içinde beyan edilir, atamayı portal yöneticisi Yetki Yönetimi'nde kullanıcı gruplarına (org düğümlerine) yapar. ⚠️ Ad alanı süzgeci bir kolaylıktır, gizlilik sınırı değildir: `Authorization` belirteci kullanıcının bütün izinlerini taşır. |
| `requirePermission(ctx, code): OneUser` | oturum yoksa 401, izin yoksa `ONE-SDK-E-004` (403). |
| `OneError` | `status`, `code`, `message` (kullanıcıya görünen Türkçe metin). |

Uygulama kodu `X-One-*` başlıklarını **asla kendisi yazmaz**; dispatch istemciden gelenleri siler ve belirteçten
yeniden kurar.

**Kullanıcı onayı.** ERP'ye veri yazma (`dexa.write`) ve kullanıcı adına e-posta/SMS/mobil bildirim
(`mail.execute`, `sms.execute`, `push.execute`) manifest'te beyan edilmiş ve kullanıcının yetkisi olsa bile, kullanıcı bu uygulamaya
**o portalda** ayrıca onay vermeden belirtece girmez. Onay bir kez verilir, **30 gün** geçerlidir; portal uygulamanın
üstünde bir uyarı ve "İzinleri incele" düğmesi gösterir, onay devoplus-auth ekranında verilir ve oradaki "Yetkili
uygulamalarım" sayfasından kaldırılır. Uygulama onay beklerken de açılır ve onay gerektirmeyen işlevleri çalışır; bu
yüzden bu işleri yapan rotalar `requireScope` ile korunmalı, arayüz `awaitsUserConsent` ile ilgili düğmeyi kapatıp
yönlendirme yazabilir. Onay kaldırılınca sonraki her belirteç hassas scope'u taşımaz (devoplus-auth onayı güçlü tutarlı
bir Durable Object'te tutar); daha önce verilmiş belirteç iptal edilmez, en çok ömrü kadar (10 dk) geçerli kalır.

## Portal köprüsü ve görsel dil

Uygulama sayfası Portal'da iframe içinde açılır. Sayfa üç dosyayı **kendi** adresinden yükler; satır içi betik
platformda kapalıdır (`script-src 'self'`) ve `one-ci validate` satır içi ya da harici (`https://...`) betiği yayından önce
durdurur:

```html
<link rel="stylesheet" href="/one-ui.css">    <!-- görsel dil -->
...
<script src="/one-bridge.js"></script>          <!-- window.one -->
<script src="/shell.js"></script>               <!-- sayfanın kendi betiği -->
```

```ts
import manifest from '../one.manifest.json' with { type: 'json' }
import { clientAssetResponse, navigationFromManifest, scriptResponse } from '@devoplus/one-sdk'

const NAVIGATION = navigationFromManifest(manifest)          // Portal'ın çizeceği sekmeler
// fetch içinde:
const asset = clientAssetResponse(url.pathname, request)      // /one-bridge.js ve /one-ui.css
if (asset) return asset
if (url.pathname === '/shell.js') return scriptResponse(renderShellScript(NAVIGATION))
```

| Sunucu üyesi | Açıklama |
|---|---|
| `clientAssetResponse(pathname, request?): Response \| null` | `/one-bridge.js` (`ONE_BRIDGE_SCRIPT`, `text/javascript`), `/one-charts.js` (`CHARTS_SCRIPT_PATH`; portaldaki grafiklerle aynı grafik kütüphanesi; tarayıcı saklar ama her açılışta yeniden doğrular, `request` verilirse değişmediğinde gövdesiz 304 döner, SDK güncellenince yeni kütüphane hemen iner), `/one-components.js` (`ONE_COMPONENTS_SCRIPT`, hazır bileşenler) ve `/one-ui.css` (`ONE_UI_CSS`, `text/css`); başka yolda `null`. |
| `scriptResponse(body)` | Sayfa betiği için doğru içerik türüyle yanıt. |
| `navigationFromManifest(manifest)` | Manifest `navigation` alanı: bozuk/yinelenen öğe atlanır, en çok `NAVIGATION_MAX_ITEMS` (12). Yol kuralı `ROUTE_PATH_RE` / `isRoutePath` (manifest şemasıyla aynı). Sekme izni (`permission`) korunur; biçimi bozuk izinli öğe atlanır. |
| `visibleNavigation(items, access)` | Kişinin görebildiği sekmeler (`access`: `{ permissions, admin }` ya da `null` = yalnız izinsiz sekmeler); dönen öğeler izin kodu taşımaz. |

Tarayıcıdaki `window.one` (tip: `OneBridge`):

| Üye | İleti | Açıklama |
|---|---|---|
| `one.start({ navigation })` | `one:ready` + `one:navigation` | Sayfa hazır olunca **bir kez**; yükseklik bildirimini de başlatır (`one:resize`). Menü sözleşmeye uymuyorsa fırlatır. İzinli sekmeler (`permission`) erişim bildirilene kadar gönderilmez. |
| `one.setNavigationAccess({ permissions, admin })` | `one:navigation` | Oturumdaki kişinin uygulama izinleri: görebildiği izinli sekmeler menüye eklenir (menü değiştiyse ileti yeniden gönderilir; Portal son iletiyi geçerli sayar). Yönetici her sekmeyi görür. Oturum bilgisini okuyunca çağırın (`GET /api/session`). |
| `one.token`, `one.onToken(fn)`, `one.fetch(yol, init)` | `one:token` | Belirteç yalnız Portal'dan gelir; `one.fetch` `Authorization: Bearer ...` ekler. |
| `one.context`, `one.onContext(fn)`, `one.actor` | `one:context` | `{ theme, lang, autoHeight, actor: { kind } \| null, design }`. Köprü `data-theme`'i köke yazar, tasarım değerlerini (`primary`, `primaryContrast`, `fontFamily`, `radius`; biçimi tutmayan değer yazılmaz) `--one-primary` ve benzeri değişkenlere geçirir, `autoHeight` gelince sayfanın kendi dikey taşmasını gizler (yatay taşma kaydırılabilir kalır; yatay kaydırma çubuğu bildirilen yüksekliğe eklenir). `one.actor` yalnız gösterim içindir; yazma kapısı sunucudadır. |
| `one.onNavigate(fn)`, `one.path` | `one:navigate` | Portal sekmesine tıklandı; görünümü değiştirin. Adres geçmiş kaydı eklenmeden güncellenir; geri bildirim gönderilmez. |
| `one.setRoute(yol)` | `one:route` | Uygulama kendi içinde görünüm değiştirdi; Portal adresi güncellenir. Yol `/` ile başlar, sorgu/`#` yok. |
| `one.setTitle(metin \| null)` | `one:title` | Alt sayfa başlığı (en çok `TITLE_MAX_LENGTH` = 120, uzunu kesilir); `null` kaldırır. |
| `one.toast({ severity, summary, detail? })` | `one:toast` | `success`, `info`, `warn`, `error`; Portal içindeyken Portal çizer. Düz metin. Portal dışında hazır bileşenler yüklüyse sayfanın köşesinde gösterilir, değilse konsola yazılır. |
| `await one.confirm({ message, title?, severity?, acceptLabel?, rejectLabel? })` | `one:confirm`, yanıt `one:confirm-result` | `true`/`false`. `severity`: `danger`, `warn`, `info`. Portal origin'i yoksa `false`. ⚠️ Portal bu iletiyi tanımıyorsa söz çözülmez. |
| `await one.remove(adres, { message, title?, acceptLabel?, rejectLabel?, headers? })` | `one:confirm` (önem `danger`), kabulde `DELETE` | Onaylı silme: kabulde yanıtı (`Response`), vazgeçilince `null` döner ve istek GİTMEZ. one-ci onaysız `DELETE` isteği için yayında uyarı verir (yayın sürer). |
| `await one.download({ name, mime, data })` | `one:download` | `data` ArrayBuffer (Uint8Array, Blob ve metin çevrilir); en çok `DOWNLOAD_MAX_BYTES` (25 MB). iframe'de indirme kapalıdır; Portal kaydeder. |
| `one.openExternal(url)` | `one:open-external` | Yalnız `https://`; Portal kullanıcıya sorup yeni sekmede açar. |
| `one.setDirty(bool)` | `one:dirty` | Kaydedilmemiş değişiklik; Portal ayrılmadan önce sorar. Yalnız değişince gönderilir. |
| `one.viewport.current`, `one.viewport.onChange(fn)` | `one:viewport` | iframe'in ekranda görünen kısmı `{ top, height }` (iframe CSS pikseli). Kendi penceresini görünen alana yerleştirmek için kullanılır. Köprü aynı değeri `--one-viewport-top` / `--one-viewport-height` CSS değişkenlerine de yazar (`<one-dialog>` bunlarla yerleşir; betik konum yazmaz). |
| `one.setModal(bool)` | `one:modal { open }` | Uygulama içinde pencere açık mı? `true` Portal'a kendi ekranını, çerçevenin görünen kısmı DIŞINDA aynı renkle karartmasını söyler; `false` karartmayı kaldırır. Yalnız değişince gönderilir; `one.start()`'tan önce açılmış pencere `one:ready`'nin ardından yeniden bildirilir. Sayfa giderken (tam sayfa gezinme, yeniden yükleme) açık pencere Portal'a kapalı bildirilir; sayfa geri/ileri önbelleğinden dönerse ve pencere açık kalmışsa yeniden açık bildirilir. `<one-dialog>` bunu kendisi çağırır; elle yalnız kendi penceresini çizen uygulama kullanır. |
| (köprü kendisi) | `one:modal-escape` | Pencere açıkken odak iframe'de olduğu için Escape Portal'a ulaşmaz. Uygulama tuşu işlemezse köprü onu Portal'a iletir ve Portal bunu vazgeçme sayar (`one:modal-dismiss` döner). İletilmediği durumlar: tuşa basıldığı anda sayfada kipli bir `<dialog>` vardı (tarayıcı Escape'i yalnız kipli `<dialog>`'da, `showModal()` ile açılmışsa, `cancel` olarak kendisi işler; `<one-dialog>` da öyle; pencereyi `close` olayında kapatmanız yeterlidir), Escape `stopPropagation()` ya da `preventDefault()` ile işlendi ya da olay bittiğinde pencere kapanmıştı. Escape'i zaman uyumsuz işliyorsanız `preventDefault()` çağırın; yoksa Portal da vazgeçer. |
| `one.onModalDismiss(fn)` | `one:modal-dismiss` | Kullanıcı Portal'ın karartılmış alanına tıkladı ya da Portal'da Escape'e bastı: Portal karartmayı **beklemeden kendisi** kaldırmıştır; açık pencereyi "vazgeçildi" diye kapatın. Uygulama Portal'ı kilitleyemez (karartma ayrıca uygulama/kimlik değişince, Portal gezintisinde, sayfa yeniden yüklenince kalkar; açılış hız sınırlıdır). |

Bütün iletiler yalnız iframe `src`'sindeki `parent` origin'lerine gönderilir (joker yok); gelen ileti origin ve üst
pencere doğrulanarak alınır. Portal dışında (yerel geliştirme) ileti gönderilmez: `confirm` tarayıcının kendi sorusuna,
`download` tarayıcı indirmesine, `openExternal` yeni pencereye düşer; `toast` hazır bileşenler yüklüyse sayfada, değilse
konsolda görünür.

**Görsel dil (`/one-ui.css`):** açık/koyu değişkenler (`--one-bg`, `--one-fg`, `--one-muted`, `--one-surface`,
`--one-border`, `--one-primary`, `--one-primary-contrast`, `--one-font-family`, `--one-radius`, durum renkleri) ve
sınıflar: `one-btn` (`-primary`, `-danger`, `-text`), `one-field` + `one-label` + `one-input` / `one-select` /
`one-textarea`, `one-table` (+ `one-table-wrap`, `one-num`), `one-card`, `one-message` (`-success`, `-info`, `-warn`,
`-error`), içerik sekmeleri `one-tabs` + `one-tab` (`aria-selected`), `one-row`, `one-stack`, `one-muted`, `one-empty`,
`one-loading`, `one-sr-only`. ⚠️ `one-tabs` içerik içi gruplama içindir; uygulama menüsünü Portal çizer.

**Ek sınıflar** (renkler tema değişkenlerinden; hareket azaltma tercihine uyulur):

| Sınıf | İşaretleme | Not |
|---|---|---|
| `one-badge` (+ `-success`, `-info`, `-warn`, `-error`, `-primary`) | `<span class="one-badge one-badge-success">Onaylandı</span>` | Durum rozeti. Durumu yalnız renkle anlatmayın; metin taşır. |
| `one-switch` | `<label class="one-check"><input type="checkbox" role="switch" class="one-switch"> Bildirimler</label>` | Açık/kapalı anahtarı; tarayıcının onay kutusudur (klavye ve ekran okuyucu hazır). |
| `one-radio-group` (+ `-inline`) | `<fieldset class="one-radio-group" role="radiogroup"><legend class="one-label">Öncelik</legend><label class="one-check"><input type="radio" name="p"> Düşük</label>...</fieldset>` | Ok tuşları tarayıcının kendi radyo davranışıdır. |
| `one-progress` | `<progress class="one-progress" max="100" value="60">%60</progress>` | `value` yoksa belirsiz (akan) kip. Etiketi `<label for>` ya da `aria-label` ile verin. |
| `one-spinner` (+ `-lg`), `one-loading-inline` | `<span class="one-loading-inline" role="status"><span class="one-spinner" aria-hidden="true"></span> Yükleniyor</span>` | Yalnız döner simge kullanılacaksa `role="status"` + `aria-label` verin. |

## Hazır arayüz bileşenleri

Liste, form, süzgeç, kişi seçimi, dosya yükleme ve grafik için hazır bileşenler `/one-components.js`'tedir; açılır
liste, etiket girişi, pencere, ipucu, avatar, eylem menüsü, sekmeler ve akordeon da oradadır (aşağıda). Bileşenler
bağımlılıksızdır, uygulamanın **kendi** adresinden yüklenir (satır içi betik gerekmez), `/one-ui.css` görsel dilini
kullanır, Portal'ın tema ve dil değişimini (`one:context`) kendiliğinden izler ve bütün metinleri Türkçe + İngilizce
taşır. Tarih `gg.aa.yyyy`, sayı ve para `tr-TR` biçimindedir. İstekler yalnız uygulamanın kendi adresine gider ve
belirteci köprü ekler (`one.fetch`); başka adrese istek gönderilmez.

```html
<link rel="stylesheet" href="/one-ui.css">
...
<one-filter for="talepler"></one-filter>
<one-table id="talepler" src="/api/requests" export="xlsx csv" selectable clickable caption="Talepler"></one-table>
...
<script src="/one-bridge.js"></script>
<script src="/one-components.js"></script>   <!-- köprüden SONRA -->
<script src="/shell.js"></script>
```

```js
// /shell.js: kolon ve alan tanımları JavaScript özelliğidir (öznitelik değil).
var table = document.getElementById('talepler')
table.columns = [
  { key: 'title', label: { tr: 'Başlık', en: 'Title' } },
  { key: 'amount', label: { tr: 'Tutar', en: 'Amount' }, type: 'money' },
  { key: 'createdAt', label: { tr: 'Tarih', en: 'Date' }, type: 'date' },
  { key: 'status', label: { tr: 'Durum', en: 'Status' }, type: 'choice',
    options: [{ value: 'open', label: { tr: 'Açık', en: 'Open' } }, { value: 'closed', label: { tr: 'Kapalı', en: 'Closed' } }] },
]
document.querySelector('one-filter').fields = [
  { key: 'q', label: { tr: 'Ara', en: 'Search' }, type: 'text' },
  { key: 'status', label: { tr: 'Durum', en: 'Status' }, type: 'choice', options: table.columns[3].options },
  { key: 'createdAt', label: { tr: 'Oluşturma', en: 'Created' }, type: 'dateRange' },
]
table.addEventListener('rowclick', function (e) { openRequest(e.detail.row.id) })
```

Sunucu tarafında bir şey eklemeniz gerekmez: `clientAssetResponse(pathname, request)` `/one-components.js`'i ve grafik kütüphanesi
`/one-charts.js`'i de döndürür. Sayfa sırası: köprü, `/one-charts.js`, `/one-components.js`, sayfa betiği.
Kişi seçici için `peopleSearchHandler` ucunu sunun (aşağıda).

| Bileşen | Özellikler (JS) | Öznitelikler | Olaylar |
|---|---|---|---|
| `<one-table>` | `columns` (`OneTableColumn[]`), `rows` (yerel veri), `filters` (ek sorgu parametreleri), `emptyText`, `selection` (salt okunur), `reload()`, `applyFilter(detail)`, `clearSelection()` | `src` (uzak liste), `page-size` (varsayılan 20, en çok 100), `paging="page"`, `selectable` (`single` ya da çoklu), `clickable`, `export="xlsx csv"`, `export-name`, `caption`, `row-key` (varsayılan `id`), `no-search`, `lang` | `rowclick` `{ row, index }`; `selectionchange` `{ rows, keys }`; `load` `{ items, nextCursor }` |
| `<one-form>` | `fields` (`OneFormField[]`), `value` (atamak "kaydedilmiş" hal), `submitLabel`, `dirty`, `validate()`, `setErrors(errors, detail?)`, `reset()` | `action` (aynı adres; JSON gönderir), `method` (`POST`/`PUT`/`PATCH`), `no-cancel`, `acting-write`, `lang` | `save` `{ value }` (iptal edilebilir); `saved` `{ value, response }`; `savefailed` `{ status, detail }`; `cancel`; `dirtychange` `{ dirty }` |
| `<one-filter>` | `fields` (`OneFilterField[]`), `value`, `clear()` | `for` (tablo kimliği), `lang` | `change` `{ values, params }` |
| `<one-person-picker>` | `value` (`OnePerson` / dizi / `null`), `label` | `src` (varsayılan `/api/one/people`), `multiple`, `required`, `disabled`, `label`, `lang` | `change` `{ value }` |
| `<one-file-upload>` | `value` (`OneUploadedFile` / dizi / `null`), `label` | `action` (varsayılan `/api/files`), `accept`, `max-size` (bayt, varsayılan 5 MB), `multiple`, `required`, `disabled`, `name-header` (varsayılan `x-file-name`), `label`, `lang` | `uploaded` `{ file }`; `change` `{ value }` |
| `<one-chart>` | `data` (`{ labels, series: [{ name, values }] }` ya da `[{ label, value }]`), `label` | `type` (`bar`, `line`, `area`, `pie`, `doughnut`), `stacked`, `horizontal`, `size` (`sm`, `lg`), `label`, `unit`, `lang` | Yok |
| `<one-stat>` | `label`, `value`, `caption`, `deltaLabel` | `label`, `value`, `unit`, `format` (`number`, `money`, `percent`, `text`), `currency`, `decimals`, `delta`, `delta-format` (`percent`, `number`), `delta-label`, `delta-polarity` (`neutral`, `up-good`, `down-good`), `tone` (`neutral`, `primary`, `success`, `warn`, `danger`, `info`), `caption`, `loading`, `clickable`, `lang` | `select` (`clickable`) |

`<one-form>` pasife alma onayı: yöntem `POST` değilken `active` ya da `is_active` adlı `boolean` alan açıktan
kapalıya çevrilince form kaydetmeden önce onay sorar (vazgeçilirse `save` olayı bile oluşmaz). Başka bir alan için
`confirmOff: { tr, en }` yazın; `confirmOff: false` soruyu kapatır.

**Alan türleri** (`OneFieldType`, veri modeliyle aynı küme): `text`, `longText`, `number`, `money`, `date`,
`datetime`, `boolean`, `choice`, `person`, `file`. Etiketler ve seçenek adları `{ tr, en }` ya da düz metin olur;
hepsi DÜZ METİNDİR (HTML yorumlanmaz). Kolonun `format(value, row, lang)` işlevi de düz metin döndürür.

**Uzak liste sözleşmesi** (`<one-table src>`): `GET <src>?limit=&cursor=&sort=&order=asc|desc&q=&<alan>=...` isteğine
`{ items: [...], nextCursor: "..." | null }` biçiminde yanıt verilir. Sonraki sayfa `nextCursor`'la istenir, önceki sayfaya bileşen kendi
tuttuğu imleçle döner. `paging="page"` ile istek `page=` taşır ve yanıttaki `total` sayfa sayısını gösterir.
Süzgeç değerleri `<one-filter>`'dan parametre olarak gelir: metin için `q`, seçim için alan adı, tarih aralığı için
`<alan>From` / `<alan>To` (başka ad gerekiyorsa alanda `fromParam` / `toParam`). **Dışa aktarma** aynı adresi
`format=xlsx` ya da `format=csv` ile, sayfalama parametresi OLMADAN çağırır ve gelen dosyayı köprünün
`one.download`'ıyla indirir (ad `Content-Disposition`'dan, yoksa `export-name`).

**Form:** istemci doğrulaması (zorunlu, en az/en çok karakter, sayı aralığı, biçim, tarih, seçenek) Türkçe ve İngilizce
iletilerle; hatalı alan `aria-invalid` alır ve odak ilk hataya gider. Sunucu 4xx yanıtındaki RFC 9457 `errors` alanı
(`{ "alan": "ileti" | ["ileti"] }` ya da `[{ "field" | "pointer": "#/alan", "detail": "..." }]`) alanlara bağlanır;
eşleşmeyen iletiler ve `detail` formun üstünde gösterilir. Kaydedilmemiş değişiklik köprüye bildirilir
(`one.setDirty`; sayfada birden çok form varsa herhangi biri kirli olduğu sürece Portal sorar, sayfadan ayrılan form
sayılmaz, yeniden eklenen kirli form yeniden sayılır); Vazgeç değişiklik varsa Portal'da onay sorar. Başkası adına açılmış oturumda (Vekalet / Yerine
Görüntüle) Kaydet kapalıdır; manifest `actingSessions: "write"` beyan eden uygulama forma `acting-write` yazar.

**Kişi araması:** bileşen uygulamanın kendi ucuna gider; ucu SDK hazırlar:

```ts
import { PEOPLE_SEARCH_PATH, peopleSearchHandler } from '@devoplus/one-sdk'
// fetch içinde (manifest scopes: "orgschema.read"):
if (pathname === PEOPLE_SEARCH_PATH && method === 'GET') return peopleSearchHandler(request, env, ctx)
```

`GET /api/one/people?q=&lang=` yanıtı `{ items: [{ id, name, title, department }] }` biçimindedir: yalnız aktif kişiler, en az 2 en çok
100 karakterlik arama, en çok 20 sonuç, Türkçe harf duyarsız. Yanıt **yalnız** ad, unvan ve departman taşır; e-posta,
telefon, kullanıcı kodu ve fotoğraf DÖNMEZ (veri en azaltma). ⚠️ `id` Organization Schema kimliğidir (`sub` DEĞİL);
bildirim gönderirken `toOrgUserIds` alanına yazın. Şema her aramada okunur; `options.loadSchema` ile kendi önbelleğinizi
(`OneCache`) bağlayabilirsiniz.

**Dosya yükleme** iskeletin dosya ucuyla uyumludur (`POST /api/files`, gövde dosyanın kendisi, ad `x-file-name`
başlığında; başlık yalnız ASCII taşıdığı için Türkçe harfler karşılığına çevrilir). Tür ve boyut sınırı yüklemeden önce
tarayıcıda denetlenir; sunucu yine kendi sınırını uygular. Kaldır düğmesi yalnız değeri değiştirir, dosyayı silmez.

**Grafik** sayfa `/one-charts.js`'i yüklediyse portaldaki grafiklerle aynı kütüphaneyle (Chart.js) tuvale çizilir, yüklemediyse
SVG'dir (`area` çizgi, `doughnut` pasta olarak). Pasta ve halka iki çizimde de yalnız ilk seriyi gösterir (gizli tablo da);
birden çok seri için `bar` ya da `line` kullanılır. İkisinde de `role="img"` + `aria-label` ve ekran okuyucu için gizli veri
tablosu vardır; renkler `--one-chart-1` ile `--one-chart-6` arasındaki değişkenlerden (açık/koyu), yazı tipi ve ızgara
`--one-font-family`, `--one-muted`, `--one-border`'dan okunur. Yeniden çizimde ve sayfadan ayrılınca eski grafik yok edilir.

**Gösterge kartı** (`<one-stat>`): sayı ve durum özetleri cümle değil karttır. Birden çok kart `<div class="one-stat-grid">`
içinde dizilir (dar ekranda alt alta). Değer `tr-TR` biçimindedir (`money` TRY varsayılanıyla); `delta` oklu SVG simge ve
ekran okuyucu metniyle ("artış", "azalış") çizilir ve `delta-polarity` verilmedikçe nötr renktedir. `loading` yükleniyor
göstergesi, değer yokken "-" çizilir. Metinler yalnız `textContent` ile yazılır.

### Açılır liste, pencere ve gezinme bileşenleri

| Bileşen | Özellikler (JS) | Öznitelikler | Olaylar |
|---|---|---|---|
| `<one-select>` | `options` (`[{ value, label, description?, disabled? }]`), `value` (tekli: değer ya da `null`; `multiple`: dizi), `label` | `label`, `multiple`, `required`, `disabled`, `placeholder`, `lang` | `change` `{ value }` |
| `<one-tag-input>` | `value` (`string[]`), `suggestions` (`string[]`, isteğe bağlı), `label` | `label`, `max` (adet, varsayılan 50), `max-length` (karakter, varsayılan 50), `required`, `disabled`, `placeholder`, `lang` | `change` `{ value }` |
| `<one-dialog>` | `label`, `body` (gövde öğesi), `open` (salt okunur), `show()` (`Promise<string>` döner), `close(değer?)` | `label` (başlık), `size` (`sm`, `lg`), `accept-label` / `reject-label` (onay penceresi), `severity` (`danger`, `warn`, `info`), `alert` (`role="alertdialog"`), `open` (eklenince açılır, kaldırılınca kapanır), `keep-open` (içteki form kaydedince açık kalır), `lang` | `open`; `cancel` `{ reason }` (iptal edilebilir; `escape`, `close`, `backdrop`, `portal`); `close` `{ returnValue }` |
| `<one-tooltip>` | `text` (`{ tr, en }` ya da metin) | `text`, `text-en`, `placement="bottom"`, `lang` | Yok |
| `<one-menu>` | `items` (`[{ id, label, danger?, disabled? }]`, ayraç `{ separator: true }`), `label` | `label`, `compact` (yalnız `⋯` düğmesi; `label` erişilebilir ad olur), `disabled`, `lang` | `select` `{ id }` |
| `<one-avatar>` | Yok | `name`, `src` (kendi adresi, `https:`, `data:image/`, `blob:`), `size` (`sm`, `lg`), `lang` | Yok |
| `<one-tabs>` | `selected` (sekme anahtarı) | `label` (sekme şeridinin erişilebilir adı), `selected`, `lang` | `change` `{ tab }` |
| `<one-accordion>` | Yok | `multiple`, `heading-level` (2-6, varsayılan 3), `lang` | `toggle` `{ index, open, key }` |

**Açılır liste** (`<one-select>`): APG "combobox + listbox" kalıbı. Yazınca Türkçe harf duyarsız süzer (`ı`/`i`, `ş`/`s`
gibi), ok tuşları etkin seçeneği taşır (devre dışı seçenek atlanır), Enter seçer, Escape önce listeyi kapatır (olay dışarı,
ör. pencereye, taşmaz), Alt+Aşağı listeyi açar. Çoklu seçimde liste açık kalır, seçilenler etiket olarak görünür ve boş
alanda Geri son seçimi kaldırır. En çok 200 seçenek gösterilir (fazlası için "aramayı daraltın" yazar). Liste giriş alanının
altında akış içindedir (Portal iframe'i içerik boyunda çizer; akış dışındaki liste kesilirdi). Değer seçeneğin `value`'sudur
(sayı ve mantıksal değer korunur). Seçimi yalnız `null` (ya da `undefined`) atamak temizler. `''` yalnız `''` değerli bir
seçenek varsa seçilir: önce `options`, sonra `value` atayın; seçenekler değişince artık seçenek olmayan `''` seçimden düşer.

**Etiket girişi** (`<one-tag-input>`): Enter ya da virgül etiket ekler; yapıştırılan virgüllü/çok satırlı liste ayrı
etiketlere bölünür; alandan ayrılınca yazılı kalan metin etikete dönüşür; aynı etiket (büyük/küçük harf ve Türkçe harf
duyarsız) ikinci kez eklenmez. Uzunluk, adet ve yinelenme nedenini durum satırı söyler. `suggestions` verilirse kutu
combobox olur ve öneriler listelenir.

**Pencere** (`<one-dialog>`): tarayıcının kipli penceresidir (`<dialog>.showModal()`): odak pencerede kalır, sayfanın geri
kalanı etkisizdir. Alt öğeler gövdeye taşınır; `data-one-dialog-footer` taşıyan öğe alt bilgiye gider. `data-one-close="değer"`
taşıyan her düğme pencereyi o değerle kapatır (`<form method="dialog">` da çalışır; değersiz düğme vazgeçmedir). Pencerenin içindeki `<one-form>`
kaydedilince pencere `'saved'` ile, formda Vazgeç'e basılınca `''` ile kapanır (`keep-open` özniteliği kapatmaz; kayıt
sürerken yapılmış ve gönderilmemiş bir değişiklik varsa da kapanmaz); formda
kaydedilmemiş değişiklik varken vazgeçme önce "değişiklikler kaybolacak" onayı sorar ve kabulde form son kaydedilen
haline döner. İlk odak sırası: `autofocus`, onay
penceresinde "vazgeç", gövdedeki ilk odaklanabilir öğe, kapat düğmesi; kapanınca odak açan öğeye döner. Escape, kapat
düğmesi ve karartmaya tıklama iptal edilebilir `cancel` olayıdır (`preventDefault` pencereyi açık tutar). Kaydedilmemiş
`<one-form>` varken `cancel` ancak kullanıcı değişiklikleri atmayı kabul ettikten sonra gelir; o olayda ikinci bir soru
sormayın ve `preventDefault` çağırmayın. `<form method="dialog">` gönderimini `preventDefault` ile durdurursanız pencere
açık kalır.

- Pencere iframe'in **görünen** dilimine yerleşir (uzun sayfada kullanıcının baktığı yerde açılır).
- Açılınca köprü `one.setModal(true)` ile Portal'a bildirir; Portal kendi ekranını aynı renkle karartır ve iki karartma
  kenarda birleşir. Kullanıcı Portal'ın karartılmış alanına tıklarsa ya da Portal'da Escape'e basarsa Portal karartmayı
  kendisi kaldırır ve pencere `cancel` (`reason: "portal"`) ile kapanır. `cancel` iptal edilirse pencere açık kalır ama
  Portal yeniden karartılmaz.
- **Onay penceresi** (`accept-label` / `reject-label`): sonuç `'accept'` ya da `'reject'`. ⛔ Portal içindeyken pencere
  iframe'de ÇİZİLMEZ, Portal'ın onay penceresiyle (`one.confirm`) sorulur: başlık `label`, ileti gövdenin DÜZ
  METNİ (HTML ve biçim taşınmaz), düğmeler `accept-label` / `reject-label`, `severity`. Portal'ın onay penceresi
  uygulamadan kapatılamaz (`close()` etkisizdir). Portal dışında (yerel geliştirme) aynı pencere sayfada çizilir.
- Zengin içerikli onay (biçimli metin, alan) gerekiyorsa onay öznitelikleri olmadan genel pencere kullanın ve düğmeleri
  `data-one-close` ile kendiniz koyun; o pencere Portal içinde de iframe'de çizilir.

**Bildirim** için ayrı bileşen yoktur: `one.toast(...)` Portal içindeyken bildirimi Portal'a verir (tek görünüm); Portal
dışında hazır bileşenler yüklüyse sayfanın köşesinde gösterir (hata 8, diğerleri 5 saniye; kapatılabilir).

**İpucu** (`<one-tooltip>`): ilk alt öğe tetikleyicidir ve **odaklanabilir** olmalıdır (düğme, bağlantı). Fareyle üzerine
gelince ve klavye odağıyla görünür, Escape ile kapanır, fare ipucunun üzerine geçebilir (WCAG 1.4.13). Tetikleyiciye
`aria-describedby` bağlanır. İpucuna önemli bilgi koymayın; ipucu dokunmatik ekranda görünmeyebilir.

**Eylem menüsü** (`<one-menu>`): APG "menu button". Düğme `aria-haspopup="menu"`; açınca ilk öğe odaklanır (Yukarı ok ile
son öğe), oklar/Home/End gezinir, harf yazınca o harfle başlayan öğeye atlar, Enter/Boşluk seçer, Escape kapatıp odağı
düğmeye döndürür. Devre dışı öğe odaklanır ama seçilmez. Menü aşağıda yer yoksa yukarı açılır.

**Avatar** (`<one-avatar>`): baş harfler Türkçe büyük harfle (`şule yıldız` için `ŞY`), renk addan kararlı biçimde seçilir;
erişilebilir ad kişinin adıdır. Görsel yüklenemezse baş harflere döner.

**Sekmeler** (`<one-tabs>`): alt öğeler paneldir: `data-label` (+ `data-label-en`), isteğe bağlı `data-tab` (anahtar) ve
`data-disabled`. APG "tabs": oklar devre dışı sekmeyi atlayarak gezer ve seçer, Home/End uçlara gider. Uygulama menüsü
DEĞİLDİR (menüyü Portal çizer); içerik içi gruplama içindir. Seçimin tek kaynağı `selected` özniteliğidir: `selected`
özelliği başlangıçta da gösterilen sekmeyi döndürür, kullanıcının seçimi özniteliğe yazılır, uygulama özniteliği sonradan
da değiştirebilir. `change` yalnız seçim gerçekten değişince yayılır. Panel sonradan eklenir ya da `data-label`,
`data-tab`, `data-disabled` değişirse şerit yenilenir.

**Akordeon** (`<one-accordion>`): alt öğeler bölümdür: `data-label` (+ `data-label-en`), başlangıçta açık olan için
`data-open`. Başlık düğmesi `aria-expanded` taşır, oklar başlıklar arasında gezer; `multiple` yoksa aynı anda tek bölüm açıktır.
`data-open` iki yönlüdür: kullanıcı bölüm açınca öznitelik eklenir, kapatınca kalkar; uygulama özniteliği ekleyip
kaldırarak bölümü açıp kapatabilir.

**Sayfadan ayrılıp yeniden eklenen bileşen:** ayrıyken değişen öznitelikler yeniden eklenince uygulanır (ör. `src`
değişen tablo bir kez yeniden yüklenir, `open` alan pencere açılır, `selected` alan sekmeler o sekmeyi gösterir); ayrıyken
değişip eski değerine dönen öznitelik yok sayılır (tablo bulunduğu sayfada kalır). Ayrılan açık pencere `''` ile kapanır
ve Portal karartması kalkar; yeniden eklenince kapalı görünür, `show()` ya da `open` ile yeniden açılır (Portal'ın onay
penceresi hariç: o yanıt gelince sonuçlanır). Açık liste, menü ve ipucu kapanır.

**Açılır öğeler ve yükseklik:** menü ve ipucu kapsayıcılarına göre mutlak konumludur; açıkken alt kenarları otomatik
yükseklik ölçütüne katılır (`data-one-popup`), sayfanın altındaki menü iframe'in alt kenarında kesilmez.

| Sabit | Değer |
|---|---|
| `COMPONENTS_SCRIPT_PATH` | `/one-components.js` (`ONE_COMPONENTS_SCRIPT`) |
| `ONE_COMPONENT_TAGS` | on beş etiket adı |
| `ONE_COMPONENT_EVENTS` | etiket başına yayılan olay adları (`OneComponentEvents` ile aynı küme) |
| `PEOPLE_SEARCH_PATH`, `PEOPLE_SEARCH_MIN_QUERY`, `PEOPLE_SEARCH_MAX_QUERY`, `PEOPLE_SEARCH_MAX_RESULTS` | `/api/one/people`, 2, 100, 20 |
| `FILE_UPLOAD_DEFAULT_ACTION`, `FILE_UPLOAD_DEFAULT_MAX_BYTES` | `/api/files`, 5 MB |
| `TABLE_DEFAULT_PAGE_SIZE`, `TABLE_MAX_PAGE_SIZE` | 20, 100 |

Tipler: `OneTableColumn`, `OneTableElement`, `OneListResponse`, `OneFormField`, `OneFormElement`, `OneFormErrors`,
`OneFilterField`, `OneFilterElement`, `OneFilterChange`, `OnePerson`, `OnePersonPickerElement`, `OneUploadedFile`,
`OneFileUploadElement`, `OneChartData`, `OneChartSeries`, `OneChartElement`, `OneChoiceOption`, `OneLocalizedText`,
`OneFieldType`, `OneSelectElement`, `OneSelectOption`, `OneSelectValue`, `OneTagInputElement`, `OneDialogElement`,
`OneDialogCancelReason`, `OneTooltipElement`, `OneMenuElement`, `OneMenuItem`, `OneTabsElement`. Olay yükleri bileşen
başına `OneComponentEvents[etiket][olay]` (ör. `OneComponentEvents['one-dialog']['cancel']` = `{ reason }`) ya da
`OneComponentEvent<etiket, olay>` (`CustomEvent`) ile yazılır; eski birleşik `OneComponentEventMap` yükü daraltamaz
ve kullanımdan kalkacaktır. Sunucu: `peopleSearchHandler`, `searchPeople` (saf), `foldPersonText`,
`PeopleSearchItem`, `PeopleSearchEnv`, `PeopleSearchOptions`.

## Backend istemcisi

`new BackendClient({ baseUrl, authorization })`: `baseUrl` ortamın API tabanıdır (uygulama `vars`'ından gelir ve
outbound allowlist'inde olmak zorundadır); `authorization` tarayıcıdan gelen `Authorization: Bearer <app_token>`
başlığıdır ve olduğu gibi iletilir.

| Metot | Davranış |
|---|---|
| `request<T>(path, init?)` | `api/v1/<path>`; `accept: application/json`; gövde varsa `content-type: application/json`; `init.headers` olduğu gibi eklenir (outbound yalnız `X-One-*` başlıklarını siler); `204` yanıtında `undefined` döner; `!ok` olursa `OneError(status, problem.code ?? HTTP-<status>, problem.detail ?? title)` fırlatır. Yanıtta `Retry-After` (saniye) ya da gövdede `retryAfterSeconds` varsa `OneError.retryAfterSeconds` olarak taşınır. |
| `get<T>(path)`, `post<T>(path, body)` | kısayollar. |

Backend "kullanıcı yetkisi ∩ scope" kuralını kendisi uygular; SDK ek bir yetki vermez.

## Dosya servisi

Manifest'te `services.files: true` ise platform kurulum başına bir dosya deposu açar ve `env.FILES`'a bağlar.
İzolasyon depo sınırındadır: anahtara `portalId` eklenmez, başka kurulumun deposu görünmez.

| Üye | Açıklama |
|---|---|
| `new OneFiles(env.FILES)` | |
| `put(key, body, { contentType?, maxBytes?, metadata? })` | `FileInfo`; gövde `ArrayBuffer \| ArrayBufferView \| string \| Blob` (akış KABUL EDİLMEZ); tavan `FILE_MAX_BYTES` (25 MiB), aşımda `ONE-SDK-E-014` (413). `maxBytes` sayı değilse, `NaN` ya da negatifse depoya yazılmadan `TypeError` atılır. |
| `get(key)` | `OneStoredFileBody \| null`; dosyayı uygulamanın kendisi sunar (`writeHttpMetadata(headers)` + `httpEtag`). |
| `head(key)`, `delete(key)`, `list({ prefix?, limit?, cursor? })` | `limit` 1-1000. |
| `normalizeFileKey(raw)` | 1-512 karakter **ve** UTF-8 ile en çok 1.024 bayt (depo sınırı, `FILE_KEY_MAX_BYTES`), baştaki `/` atılır; boş / `.` / `..` parçası ve kontrol karakteri reddedilir (`ONE-SDK-E-010...012`). |
| `sanitizeFileName(raw, maxLength = 120)` | kullanıcı dosya adını tek parçaya indirger (`ONE-SDK-E-013`). |
| `readBoundedBody(request, maxBytes)` | HTTP isteğinin gövdesini **sınırlı** okur: `Content-Length` varsa tek bayt okunmadan sınanır, yoksa (parçalı aktarım) akış parça parça okunur ve sınır aşıldığı an kesilir (`ONE-SDK-E-014`, 413); bozuk `Content-Length` için hata `ONE-SDK-E-016` (400) olur. `maxBytes` sayı değilse, `NaN` ya da negatifse gövdeden tek bayt okunmadan `TypeError` atılır (`Infinity` tavana kırpılır). `request.arrayBuffer()` gövdenin tamamını belleğe aldıktan SONRA sınar, bu da uygulamanın belleğini taşırabilir; yükleme ucunda bunu kullanın. |

## Önbellek servisi

Manifest'te `services.cache: true` ise platform kurulum başına bir önbellek açar ve `env.CACHE`'e bağlar. İzolasyon
önbellek sınırındadır. Bu bir **önbellektir**: nihai tutarlıdır (yazma başka bölgelerde 60 sn'ye kadar gecikebilir).
Sayaç, kilit ya da kalıcı kayıt için kullanılmaz; kalıcı veri veritabanındadır.

| Üye | Açıklama |
|---|---|
| `new OneCache(env.CACHE)` | |
| `get<T>(key)` | `T \| null`; değer yazıldığı JSON şeklinde döner (bozuk kayıt fırlatmaz, `null`). |
| `put(key, value, { ttlSeconds })` | TTL **zorunlu**, tam sayı, `CACHE_MIN_TTL_SECONDS` (60) ile `CACHE_MAX_TTL_SECONDS` (30 gün) arasında; sınır dışı değer `ONE-SDK-E-022`, sessizce kırpılmaz. Değer `JSON.stringify` ile yazılır; `undefined`, üst düzey `null` (okumada "anahtar yok" ile ayırt edilemezdi) ve çevrilemeyen değer `ONE-SDK-E-023`; `CACHE_VALUE_MAX_BYTES` (1 MiB) üstü `ONE-SDK-E-024` (413). |
| `delete(key)` | |
| `normalizeCacheKey(raw)` | kırpılır; boş / 512 bayttan uzun / tam olarak `.` ya da `..` (önbelleğin ayırdığı anahtarlar; `ONE-SDK-E-020`) ya da kontrol karakterli (`ONE-SDK-E-021`) anahtar reddedilir. |

## İletişim servisi (e-posta / SMS / mobil bildirim)

`sendMail(api, { toSubjects, subject, text })` `POST One/services/mail` ucunu, `sendSms(api, { toSubjects, text })`
`POST One/services/sms` ucunu, `sendPush(api, { toSubjects, title, text })` `POST One/services/push` ucunu çağırır (aşağıda). Alıcılar devoplus-auth `sub` değerleridir; adres/telefon **sunucuda** ve yalnız aynı portalın
aktif üyeleri için çözülür; serbest adres/numara kabul edilmez. Scope `mail.execute` / `sms.execute` hem kimlik
sağlayıcı kaydında hem platform kayıt defterinde tanımlı olmalıdır. Yanıt `NotifyResult { queued, skipped[{ sub,
reason }] }`; `reason` değerleri: `invalid-subject`, `not-in-portal`, `inactive`, `no-address`, `no-phone`,
`preference`, `rate-limited`, `invalid-org-user`, `not-deliverable` (yalnız push). Kurulum başına saatlik tavan vardır (varsayılan e-posta 50, SMS 20, push 100). Backend **429**
yalnız hiçbir ileti kuyruğa girmez **ve** atlananların tamamı `rate-limited` ise döner; başka gerekçeyle hiç kuyruğa
girmeyen istek **200** + `queued: 0` alır. Sonucu `skipped[].reason` ile denetleyin (backend `OneServiceHandlers`,
okundu 15.10.2026).

**Org kimliğiyle alıcı:** `toOrgUserIds` alanı org şemasındaki kullanıcı kimliğini (legacy `User._id`, 24-hex) kabul eder. Yönetici zinciri ve grup üyeleri bu uzayda geldiği için onlara `sub` bulmadan bildirim gönderilebilir. `toSubjects` ile birlikte verilebilir; toplam alıcı en çok 50'dir, aynı kişi iki kez sayılmaz. Biçimi bozuk org kimliği `invalid-org-user`, portalın aktif kullanıcısı olmayan kimlik `not-in-portal` ile atlanır. ⛔ İki kimlik uzayını karıştırmayın: `ctx.user.sub` org kimliği DEĞİLDİR (ikisi de 24-hex; karışırsa hata vermez, kişi `not-in-portal` ile sessizce atlanır).

### Mobil bildirim

`sendPush(api, { toSubjects, toOrgUserIds?, title, text, path? })`, `POST One/services/push` ucunu çağırır; scope `push.execute`.
Devoplus Portal mobil uygulamasına başlık + metin gider (başlık 1-100, metin 1-300 karakter; bu sınırı sunucu uygular, SDK
`sendPush` istemci denetimi yapmaz, hazır senaryolar `toPush` ile kırpar). İsteğe bağlı `path` bildirime dokununca açılacak
uygulama içi konumdur (`/siparis/42`, istenirse `?sekme=ozet`); yoksa uygulamanın ana sayfası. Kural Portal'ınkiyle
aynıdır: `/` ile başlar, en çok 512 karakter, yalnız harf, rakam ve `- . _ ~ /`; `//`, `.`/`..` bölümü ve `#` yok.
Sorgu `ad=değer` çiftleridir ve yalnız harf, rakam ile `- . _ ~ = & + %` içerir; diğer karakterler (Türkçe harfler, `/`,
boşluk, `! ' ( ) *`) yüzde kodlanır. Yolu `pushPath('/siparis/42', { sekme: 'özet' })` ile kurun: `encodeURIComponent`
`! ' ( ) *` karakterlerini kodlamaz ve sunucu onları reddeder. Portal'a ayrılmış sorgu anahtarları (`parent`, `lang`, `theme`, `delegate`,
`isMobileApp`), boş, değersiz, uzun ya da yinelenen anahtar ve 20'den fazla anahtar `400` alır. Bildirimin hangi uygulamaya ve portala ait olduğunu sunucu yazar. Alıcı
kuralları e-posta/SMS ile aynıdır; yeni atlama gerekçesi `not-deliverable` = kişinin bildirim tercihi kapalı **ya da**
mobil kimliği çözülemedi (sunucu ikisini ayırt edemez). Kanal ortamda ya da portalda kapalıysa uç kuyruğa almadan
`OneError` `ONE-APP-E-029` ile reddeder (kapalı kanal sessiz başarı gibi görünmesin); SMS kanalı ortamda kapalıysa
`sendSms` `ONE-APP-E-110` alır. Kodunuz bu hatalarda çalışmayı sürdürmeli; `notify` kanalları bağımsız gönderir.

### Hazır bildirim senaryoları

`approvalRequest`, `approvalResult`, `reminder`, `systemNotice`, `operationResult`, `verificationCode` aynı girdiden e-posta konusu + gövdesi ve SMS metni üretir (`ScenarioMessage { subject, text, smsText }`; Türkçe resmi "siz" ya da `lang: 'en'`). Metinler backend sınırlarına göre kırpılır (konu ≤ 200, SMS ≤ 300; e-posta gövdesi 10.000'i aşarsa `ONE-SDK-E-062`). Bağlantı yalnız `https` olabilir ve kullanıcı adı/parola taşıyamaz (`normalizeLink`, `ONE-SDK-E-061`). `notify(api, message, { toSubjects?, toOrgUserIds?, channels })` kanalları (`mail`, `sms`, `push`) **bağımsız** gönderir: SMS ya da push düşse de e-posta gider; sonuç kanal adıyla döner, düşen kanal `{ error }` taşır. `toPush(message, to)` başlığı e-posta konusundan (≤ 100, `PUSH_TITLE_MAX_LENGTH`), metni SMS metninden (≤ 300, `PUSH_TEXT_MAX_LENGTH`) kurar; kırpma bir emojiyi ortasından bölmez.

```ts
import { approvalChain, approvalRequest, getMyOrgContext, getOrgUsersWithManagers, notify } from '@devoplus/one-sdk'

const me = await getMyOrgContext(api)            // null: kişinin bu portalda org kaydı yok
if (me) {
  const users = await getOrgUsersWithManagers(api, { includeInactive: true })   // pasifler de gelsin ki atlanabilsin
  const [first] = approvalChain(users, me.orgUserId)   // işlevsel yönetici zinciri, pasifler atlanır
  if (first) {
    const message = approvalRequest({ appName: 'İzin Talepleri', item: '3 günlük yıllık izin', requester: me.fullName ?? '', link })
    await notify(api, message, { toOrgUserIds: [first], channels: ['mail'] })
  }
}
```

### Doğrulama kodu

`generateVerificationCode(digits = 6)` (4-10 hane, yanlılıksız); `generateSalt()`; `hashVerificationCode({ code, salt, purpose, sub })`; `verificationCodeMatches({ ..., hash })` (sabit süreli karşılaştırma). Özet **amaca ve kişiye** bağlıdır: başka bir işlem ya da başka bir kişi için üretilmiş kod kabul edilmez. Kodun kendisini değil `salt + hash + son kullanma + deneme sayısı`nı uygulamanın **veritabanında** saklayın (önbellek nihai tutarlıdır; deneme sayacı ve tek kullanım orada güvenilir değildir). Süreyi ve deneme tavanını (ör. 5) uygulama uygular; doğru koddan sonra kaydı silin.

## Organizasyon şeması

Scope `orgschema.read` (salt okuma) + kullanıcının kendi `Portal.Generic.Read` izni. Veri aktif portalın aktif kayıtlarıdır.

| İşlev | Uç | Not |
|---|---|---|
| `getOrgSchema(api)` | `GET OrganizationManagement/organization-schema` | şirket, iş yeri, departman, pozisyon, ünvan, çalışan türü, kullanıcı grubu, kullanıcı |
| `getOrgUsersWithManagers(api, { includeInactive? })` | `GET OrganizationManagement/users` (`includeInactive` karşılığı `?hideInactiveDocuments=false`) | yönetici alanlarını (`functionalManager` / `operationalManager`) taşıyan TEK uç; boş metin = yönetici yok. Uç varsayılan olarak yalnız aktifleri döner; onay zinciri kurarken `includeInactive: true` verin, yoksa zincir pasif yöneticide kesilir. ⚠️ Yanıt iletişim bilgisi de taşır, saklamayın |
| `getMyOrgContext(api)` | `GET Users/me` | oturumdaki kişinin org kimliği (`orgUserId`); org kaydı yoksa `null` |
| `getUserGroupMembers(api, groupId)` | `GET OrganizationManagement/user-groups/{id}/users` | üyelik SUNUCUDA çözülür (yedi boyutun VEYA'sı + iç içe gruplar); istemcide türetmeyin |
| `getUserGroupsOfUser(api, orgUserId)` | `GET OrganizationManagement/users/{id}/user-groups` | |

Saf yardımcılar (ağ yok): `managerOf(users, orgUserId, kind?)`; `approvalChain(users, orgUserId, { kind?, maxDepth?, skipInactive? })` (döngüye ve kendine yöneticiye karşı güvenli; pasif yönetici varsayılan olarak atlanır ve zincir onun yöneticisinden sürer; derinlik varsayılan 5, en çok 20); `approvalChainReport(...)` (aynı zincir ve durma nedeni: `top`, `depth`, `unknown` ya da `cycle`; boş zinciri "onaycı yok" diye okumadan önce bakın); `orgUsersMatching(schema, criteria)` (verilen boyutların hepsine uyan aktif kullanıcılar; ölçütsüz çağrı `ONE-SDK-E-051`); `localizedText(text, lang?)`. Şemada departman/pozisyon hiyerarşisi alanı **yoktur**; zincir yalnız kişi kayıtlarındaki yönetici alanlarından kurulur.

## DEXA (ERP okuma) servisi

`queryDexa(api, key, parameters?)`, `POST One/services/dexa/query` ucunu çağırır. Uygulama ERP verisini **yalnız önceden tanımlı
QueryStore anahtarlarıyla** okur: serbest SQL, ERP'ye yazan anahtar ya
da desteklenmeyen sorgu türü çalıştırılamaz. Hangi anahtarın çalışabileceğinin **tek kaynağı platform kayıt
defteridir**: anahtarları Devoplus Console'da uygulamanın kaydına yönetici ekler; kayıtta olmayan anahtar 403
`ONE-APP-E-024` alır. Manifest `services.dexa.queryKeys` yalnız **beyandır** (çalışma anında okunmaz) ve `scopes`
içinde `dexa.read` ile birlikte yazılır; biri eksikse manifest doğrulaması düşer. Belirteçte `dexa.read` yoksa
backend 403 `ONE-APP-E-017` döner.

```ts
import { BackendClient, queryDexa, requireScope } from '@devoplus/one-sdk'

requireScope(ctx, 'dexa.read')
const api = new BackendClient({ baseUrl: env.API_BASE, authorization: request.headers.get('authorization') })
// Anahtar ve parametre adları sorgu tanımından (QueryStore) gelir; buradakiler yalnız örnektir.
const result = await queryDexa(api, 'Ornek.Rapor', { StartDate: '2026-10-01' })

if (result.unavailable) {
  // Sorgu ÇALIŞAMADI (DEXA tüneli kurulmamış / ERP'ye ulaşılamıyor / sorgu düştü): boş liste "kayıt yok" DEĞİLDİR.
  return Response.json({ status: 'unavailable', message: 'Veriler şu anda getirilemiyor; lütfen daha sonra deneyin.' }, { status: 503 })
}
return Response.json({
  columns: result.columns,
  rows: result.rows,                           // boşsa gerçekten kayıt yoktur
  truncated: result.truncated,                 // true: sunucu sonucu 5.000 satırda kesti, süzgeci daraltın
})
```

| Alan | Anlam |
|---|---|
| `columns: string[]` | Kolon adları (sıra korunur). |
| `rows: Array<Record<string, unknown>>` | Satırlar (kolon adı: değer). |
| `unavailable: boolean` | `true` = sorgu çalışamadı; kullanıcıya "şu anda getirilemiyor" gösterin, "kayıt yok" değil. Sunucu **200** döner, SDK **fırlatmaz**. Yanıt gövdesi hiç nesne değilse (boş/bozuk) SDK da `true` sayar. |
| `truncated: boolean` | `true` = sunucu sonucu 5.000 satırda kesti; liste eksiktir. |

Eksik dizi `[]`, eksik bayrak `false` olarak okunur (`normalizeDexaQueryResult`). İstemci tarafı kurallar sunucuyla
birebirdir ve istek **gönderilmeden** fırlatır (`buildDexaQueryRequest`; programlama hatası):

| Kural | Sabit | Hata |
|---|---|---|
| Anahtar `^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$`, harf duyarlı | `DEXA_KEY_RE` | `ONE-SDK-E-040` |
| Parametreler `{ ad: metin }` nesnesi, en çok 32 | `DEXA_MAX_PARAMETERS` | `ONE-SDK-E-041` |
| Ad `^[A-Za-z_][A-Za-z0-9_]{0,63}$` | `DEXA_PARAMETER_NAME_RE` | `ONE-SDK-E-042` |
| `LangCode` gönderilemez: platformun bağlam parametresidir, sunucu doldurur; denetim **harf duyarsızdır** (`langcode` da reddedilir) | `DEXA_RESERVED_PARAMETERS` | `ONE-SDK-E-043` |
| `UserCode`, `CompanyCode` ve `CostCenterCode` gönderilebilir ama **boş olamaz** (boş dize ya da yalnız boşluk; ad harf duyarsız tanınır) | `DEXA_APP_CONTEXT_PARAMETERS` | `ONE-SDK-E-140` |
| Değer metin, en çok 2.000 karakter (sayı/tarih `String(...)` ile) | `DEXA_PARAMETER_VALUE_MAX_LENGTH` | `ONE-SDK-E-044` |

Sunucu hataları `OneError.code` ile taşınır: 403 `ONE-APP-E-011...017` (kapı; `-017` = `dexa.read` yok); 403
`ONE-APP-E-024` (anahtar kayıt defterinde yok); 400 `ONE-APP-E-025` (anahtar çalıştırılamaz: katalogda yok / ERP'ye
yazan anahtar / desteklenmeyen sorgu türü); 400 `ONE-APP-E-026` (parametre izinli değil: sorgu tanımlamıyor,
`LangCode`, ya da kurulumda onay yokken gönderilen bağlam kodu); 403 `ONE-APP-E-027` (sorgu kişiye bağlı bir bağlam,
`UserCode` ya da `CompanyCode`, istiyor ve oturumdaki kişiden çözülemedi; sorgu ÇALIŞTIRILMAZ, boş değerle "tüm
kayıtlar"a dönmez; oturumdan çözülen bağlam değeri boş kaldığında da bu kod döner); 429 `ONE-APP-E-018` (kurulum
başına saatte 600 sorgu); 400 `ONE-APP-E-076` (gönderilen bağlam kodu geçersiz: boş, 64 karakterden uzun ya da kontrol
karakteri içeriyor); 403 `ONE-APP-E-077` (gönderilen `UserCode` ya da `CompanyCode` kurulumun portalında tanımlı değil);
400 `ONE-APP-E-078` (arka plan işinde sorgu bir bağlam kodu istiyor ve uygulama onu göndermedi).

### Bağlam kodları: `UserCode`, `CompanyCode`, `CostCenterCode`

Sorgu bu kodlardan birini bildiriyorsa değer şu sırayla belirlenir:

| Durum | Uygulama kodu gönderdi | Uygulama kodu göndermedi |
|---|---|---|
| Kurulumda yönetici **"kodları uygulama belirler"** onayını vermedi | 400 `ONE-APP-E-026` ile reddedilir | kişi / ajan kipinde oturumdaki kişiden çözülür; arka plan işinde reddedilir |
| Onay verildi, kişi (Portal) ya da ajan kipi | sunucu doğrular, gönderilen kullanılır | oturumdaki kişiden çözülür |
| Onay verildi, arka plan işi | sunucu doğrular, gönderilen kullanılır | çözülecek oturum olmadığı için **reddedilir** |

- **Portal doğrulaması:** `UserCode` kurulumun portalındaki **aktif** bir kişinin kodu, `CompanyCode` aynı portaldaki bir
  şirketin kodu olmalıdır; değilse sorgu ERP'ye gitmeden reddedilir (`ONE-APP-E-077`). `CostCenterCode` için portalda
  karşılaştırılacak bir kayıt yoktur: yalnız biçimi denetlenir (boş değil, en çok 64 karakter, kontrol karakteri yok). Uygulamanın gönderdiği kodla yapılan her çağrı
  denetim günlüğüne yazılır.
- **Boş değer ERP'ye gitmez:** gönderilen ya da oturumdan çözülen değer boşsa sorgu çalışmaz. SDK boş dizeyi ve yalnız
  boşluğu istek göndermeden reddeder (`ONE-SDK-E-140`). Kodu bilmiyorsanız parametreyi **hiç göndermeyin**.
- **`CostCenterCode` artık açıktır:** onu bildiren sorgular çalışır. Uygulama göndermezse değeri sunucu kişiden çözer;
  çözülemezse sorgu reddedilir.
- **`LangCode` platformundur:** uygulama hiçbir kipte gönderemez (`ONE-SDK-E-043`).

```ts
// Arka plan işinde (oturum yok) kişiye bağlı bir sorgu: kodu uygulama verir. Kurulumda onay yoksa sunucu reddeder.
const stock = await queryDexa(api, 'Ornek.MagazaStok', { CompanyCode: '01', StoreCode: storeCode })
```

## DEXA işlemleri: ERP'ye veri yazma

`runDexaCommand(api, key, parameters, { idempotencyKey })`, `POST One/services/dexa/command` ucunu `Idempotency-Key`
başlığıyla çağırır; `getDexaCommandStatus(api, idempotencyKey)` ise `GET One/services/dexa/command/{anahtar}` ucunu. Scope **`dexa.write`**.
Okuma sorgularından (`queryDexa`) ayrıdır: bu uç ERP'ye **VERİ YAZAR**. Çalışması dört kapının kesişimidir:

1. Belirteçte `dexa.write` (yoksa 403 `ONE-APP-E-017`).
2. Anahtar uygulamanın kayıt defterindeki işlem izin listesinde (`dexaCommandKeys`, Devoplus Console; yoksa 403 `ONE-APP-E-030`).
3. Anahtar koddaki ERP komut kataloğunda **One uygulamalarına açık** (yoksa 404 `ERPCMD-APP-E-006`; açmak backend kod
   değişikliğidir; ⚠️ bugün açık hiçbir işlem yoktur).
4. Kullanıcının o işleme özgü izni (yoksa 403 `ERPCMD-APP-E-0xx`).

Manifest `services.dexa.commandKeys` yalnız **beyandır** ve `scopes` içinde `dexa.write` ile birlikte yazılır.

⛔ **Idempotency (kod yazmadan önce okuyun):**

| Durum | Ne yapılır |
|---|---|
| Yeni kullanıcı eylemi ("Kaydet"e basış) | `newIdempotencyKey()` ile anahtar üretin ve isteği göndermeden ÖNCE veritabanına yazın |
| Ağ hatası / zaman aşımı | AYNI anahtarla tekrar deneyin ya da `getDexaCommandStatus` ile okuyun; ERP'ye ikinci kez yazılmaz (`replayed: true`) |
| 409 `ERPCMD-APP-E-016` (aynı işlem sürüyor) | `error.retryAfterSeconds` kadar bekleyin, durumu okuyun ya da AYNI anahtarla deneyin. Hatayı `problemResponse(error)` ile tarayıcıya döndürürseniz yanıt bu süreyi `Retry-After` başlığında ve gövdedeki `retryAfterSeconds` alanında taşır (yalnız pozitif tam sayıysa) |
| 424 `ERPCMD-APP-E-013` (ERP bağlantısı kurulamadı) | hiçbir şey gönderilmedi; AYNI anahtarla tekrar denenebilir |
| 409 `ERPCMD-APP-E-015` (anahtar farklı içerikle kullanılmış) | programlama hatası: anahtar başka bir eylemle karışmış |
| 200 `outcome: 'succeeded'` | ERP kaydı oluştu (`erp.headerId` / `erp.documentNumber`) |
| 200 `outcome: 'rejected'` | ERP reddetti (`erp.exceptionMessage`); aynı içerikle tekrar yine reddedilir |
| 200 `outcome: 'inDoubt'` | ERP'ye gidip gitmediği **BİLİNMİYOR**: YENİ anahtarla **ASLA** yeniden göndermeyin (çift kayıt); durumu okuyun, kullanıcıya "sonuç doğrulanamadı" deyin |

```ts
import { BackendClient, getDexaCommandStatus, newIdempotencyKey, requireScope, runDexaCommand } from '@devoplus/one-sdk'

requireScope(ctx, 'dexa.write')
const api = new BackendClient({ baseUrl: env.API_BASE, authorization: request.headers.get('authorization') })
// Anahtar eylem başına BİR KEZ üretilir ve göndermeden önce saklanır (tablo ve sütun adları örnektir).
const idempotencyKey = newIdempotencyKey()
await env.DB.prepare('INSERT INTO erp_ops (id, sub, state) VALUES (?, ?, ?)').bind(idempotencyKey, user.sub, 'sent').run()
// İşlem anahtarı ve parametre adları QueryStore tanımından gelir; buradakiler yalnız örnektir.
const result = await runDexaCommand(api, 'Ornek.Islem', { Body: JSON.stringify(model) }, { idempotencyKey })
if (result.outcome === 'inDoubt') {
  // Yeni anahtarla TEKRAR GÖNDERMEYİN. Durum daha sonra getDexaCommandStatus(api, idempotencyKey) ile okunur.
}
```

`DexaCommandResult` alanları backend `ErpCommandResponse` ile birebirdir: `queryStoreKey`, `idempotencyKey` (sunucunun
küçük harf biçimi), `replayed`, `outcome`, `correlationId`, `erp { shape, isSuccessful, headerId, documentNumber,
exceptionMessage, lines, raw }`. SDK sözleşmeye uymayan her 200 yanıtını **`inDoubt`** sayar: nesne olmayan gövde,
bilinmeyen `outcome`, backend kaydında nullable olmayan bir alanın eksik olması ya da herhangi bir alanın yanlış tipte
olması, **gönderilenden farklı `idempotencyKey`** ya da farklı `queryStoreKey` (yalnız ASCII büyük/küçük harf farkı
aynı anahtardır: backend katalogdaki yazımı döner). O durumda gövdeden hiçbir alan sonuca taşınmaz (anahtarlar
gönderilenlerdir, `correlationId: null`, `erp.shape: 'unrecognized'`); doğru sonuç AYNI anahtarla durum sorgusundan
okunur.
`getDexaCommandStatus` backend `ErpCommandStatusResponse`'u döner (`outcome` ayrıca `pending` ya da `notSent` olabilir;
`attemptCount`, `createdAtUtc`, `completedAtUtc`, `erp`, `steps`, `kind`, `warnings`) ya da kayıt yoksa
(`404 ERPCMD-APP-E-017`) **`null`** döner; o anahtarla hiçbir şey kaydedilmemiştir ve AYNI anahtarla gönderilebilir. Durum
yanıtı da aynı kuralla doğrulanır (komut anahtarı istekte olmadığı için `queryStoreKey` yalnız tipiyle); ayrıca
`succeeded`/`rejected` `erp` görünümü olmadan kabul edilmez.

İstemci tarafı kurallar istek **gönderilmeden** fırlatır (`buildDexaCommandRequest` / `normalizeIdempotencyKey`):

| Kural | Sabit | Hata |
|---|---|---|
| Anahtar `^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$`, harf duyarlı | `DEXA_KEY_RE` | `ONE-SDK-E-045` |
| Parametreler `{ ad: metin }` nesnesi, en çok 32 | `DEXA_MAX_PARAMETERS` | `ONE-SDK-E-046` |
| Ad `^[A-Za-z_][A-Za-z0-9_]{0,63}$` | `DEXA_PARAMETER_NAME_RE` | `ONE-SDK-E-047` |
| Değer metin (null olamaz), en çok 65.536 karakter (`Body` model JSON'u taşır) | `DEXA_COMMAND_PARAMETER_VALUE_MAX_LENGTH` | `ONE-SDK-E-048` |
| `Idempotency-Key` kanonik UUID (8-4-4-4-12), boş UUID değil; SDK küçük harfe çevirir | `IDEMPOTENCY_KEY_RE` | `ONE-SDK-E-049` |

Sorgulardaki ayrılmış bağlam parametresi yasağı (`LangCode` vb.) işlemlerde **yoktur**, çünkü sunucu böyle bir kural
uygulamıyor. 4xx yanıtların hepsi ERP'ye **hiçbir şey gönderilmediği** anlamına gelir; ayrıca 429 `ONE-APP-E-018`
(kurulum başına saatte 300 işlem).

## Zamanlanmış görev ve kuyruk

Uygulama işi manifest `jobs` ile **beyan eder** ve işleyicisini yazar; işi platform tetikler. Ayrıntı ve örnek:
belgelerdeki `/docs/jobs` sayfası; manifest kuralları `/docs/manifest` sayfasındaki `jobs` bölümünde.

| Üye | Açıklama |
|---|---|
| `defineJobs({ schedules?, queues? }): OneJobsDefinition` | İşleyicileri ada göre tanımlar; ad manifest beyanıyla aynı olmalı (`JOB_NAME_RE`; geçersiz ad hemen `ONE-SDK-E-070`). Son tanım `handleJobRequest`'in varsayılanıdır. |
| `OneJobHandler` | `(job: OneJob, env, ctx: OneContext, execution) => unknown`; `ctx` platform bağlamıdır (`readContext`). |
| `OneJob` | `{ id, name, kind: 'schedule' \| 'queue', attempt, body, scheduledTime?, enqueuedAt? }`. `id` her denemede AYNIDIR (tekrar eleme anahtarı); `attempt` 1'den başlar; tarih alanları ISO 8601 (UTC). |
| `handleJobRequest(request, env, execution, definition?): Promise<Response> \| null` | Yol `/__one/jobs/...` değilse `null` (uygulama kendi yönlendirmesine geçer). Bu yolda yalnız platform işaretli (`X-One-Auth: job`) `POST` işlenir, başka her şey `404 ONE-SDK-E-075`. İşleyici dönerse 200; 4xx `OneError` fırlatırsa o durum (kalıcı hata, yeniden denenmez); başka hata 500 (platform geri çekilmeyle yeniden dener). |
| `enqueue(name, body, { delaySeconds? }): Promise<{ id }>` | Kuyruğa ileti bırakır. Ad manifest `jobs.queues` içinde beyanlı olmalı (yoksa platform 404 `ONE-OUT-E-011`); gövde JSON ≤ 64 KiB (`JOB_BODY_MAX_BYTES`), gecikme 0-86.400 sn (`JOB_DELAY_MAX_SECONDS`). Kimlik istekten damgalanır; Vekalet / Yerine Görüntüle oturumunda, uygulama yazmaya açık değilse 403 `ONE-OUT-E-015`. |

⛔ Aynı iş birden çok kez gelebilir: kalıcı yazımı `job.id` ile tekrarsız yapın (birincil anahtar + `INSERT OR IGNORE`).
Kuyruk işi iletiyi bırakanın `sub`'unu bilgi olarak taşır (`ctx.user`); uygulama izinleri ve yönetici bilgisi işe
taşınmaz. Zamanlanmış iş kullanıcısızdır (`ctx.user` null). İkisinde de Devoplus servisleri **uygulamanın kimliğiyle**
çağrılır (bkz. aşağıdaki bölüm). `/__one/` yolu platforma ayrılmıştır ve internetten kapalıdır. Yerelde (`one dev`) zamanlama
tetiklenmez ve `enqueue` 503 döner; uygulamanın yerel adresi (`8788`) `/__one/` yolunu platform gibi reddeder. İşleyiciyi
denemek için çalışma ortamının iç adresine (`8789`) `X-One-Auth: job`, `X-One-App`, `X-One-Portal-Key` ve `X-One-Portal`
başlıklarıyla `POST /__one/jobs/<schedule|queue>/<ad>` gönderin; işin Devoplus servis çağrıları yerelde taklit edilmez.
Bu iç adres ad denetimi yapmaz ve başlıkları doğrulamaz; yalnız komut satırından deneme içindir.

## Başka uygulamalara veri açmak (dışa açım)

Uygulama, manifest `exports` ile beyan ettiği okuma uçlarını başka One uygulamalarına açabilir. Alan uygulama veriyi
yalnız kendi `imports` beyanı ve portal yöneticisinin onayıyla, yalnız aynı portalda okur; çağrıyı platform kurar.
⚠️ Platformun onay ve servis tarafı (Devoplus Console onay ekranı, backend servis ucu) yazılıyor: o taraf yayına alınana
kadar işleyici tanımlanabilir ve denenebilir, ama başka bir uygulama onu çağıramaz.

```ts
import { defineExports, handleExportRequest, handleJobRequest } from '@devoplus/one-sdk'

const exportsDef = defineExports<Env>({
  'open-orders': {
    1: async (call, env, ctx) => {
      const rows = await call.db!.prepare('SELECT id, total FROM orders WHERE status = ? LIMIT 100').bind(String(call.params.status)).all()
      return { items: rows.results }
    },
  },
})

export default {
  fetch(request, env, execution) {
    return handleExportRequest(request, env, execution) ?? handleJobRequest(request, env, execution) ?? app(request, env, execution)
  },
}
```

| Üye | Açıklama |
|---|---|
| `defineExports({ '<ad>': { <sürüm>: işleyici } })` | İşleyicileri ad ve sürüme göre tanımlar; ad ve sürüm manifest `exports` beyanıyla aynı olmalı (`EXPORT_NAME_RE`, sürüm 1-999999; biçimsiz değer hemen `ONE-SDK-E-150`). Aynı ad birden çok sürümle sunulabilir. Son tanım `handleExportRequest`'in varsayılanıdır. |
| `OneExportHandler` | `(call: OneExportCall, env, ctx: OneContext, execution) => nesne`. Dönen değer JSON'a çevrilir; kökü nesne olmalıdır (yanıt şeması). |
| `OneExportCall` | `{ name, version, params, callerApp, db }`. `params` alan uygulamanın gönderdiği JSON nesnesi; `callerApp` alan uygulamanın kısa adı (platform yazar); `db` salt-okunur veritabanı (yoksa `null`). |
| `handleExportRequest(request, env, execution, definition?): Promise<Response> \| null` | Yol `/__one/exports/...` değilse `null`. Bu yolda yalnız platform işaretli `POST` işlenir; biçimsiz yol, tanımsız ya da manifestte beyan edilmemiş ad/sürüm `404 ONE-SDK-E-151`. Platformun yazdığı onaylı sözleşme özeti (`X-One-Contract-Digest`) bu yayının özetinden farklıysa işleyici çağrılmaz (`503 ONE-SDK-E-159`, yayın geçişi; yeniden denenir). Başarılı yanıt sözleşme özetini (`X-One-Export-Digest`) taşır. |
| `readOnlyDatabase(db)`, `isReadOnlySql(sql)` | Salt-okunur veritabanı sarmalayıcısı ve deyim denetimi (aşağıda). |

Kurallar:

- **Yalnız okuma.** İşleyici veritabanını yalnız salt-okunur görür: `call.db` ve işleyiciye verilen `env.DB` aynı
  sarmalayıcıdır. Tek bir `SELECT`, `WITH` ya da `VALUES` deyimi çalışır; yazan ya da şemayı değiştiren deyim
  çalıştırılmadan `403 ONE-SDK-E-155` alır. `insert`, `update`, `delete`, `create`, `alter`, `drop`, `pragma` gibi
  sözcükleri sütun adı olarak kullanıyorsanız adı çift tırnakla yazın (`"update"`). E-posta, SMS, mobil bildirim ve DEXA'ya
  yazma bu çağrıda kullanılamaz (`403 ONE-SDK-E-158`); kuyruğa iş bırakma ve başka bir uygulamanın verisini okuma da
  platformda reddedilir (`403 ONE-OUT-E-003`). Devoplus servislerinden yalnız okuma servisleri (DEXA okuma gibi)
  kullanılır; kişinin portal belirteci bu çağrıya gelmediği için organizasyon şeması uçları okunamaz.
- **İzin.** Beyan `permission` taşıyorsa, çağıran kişide o izin yoksa işleyici hiç çağrılmaz (`403 ONE-SDK-E-153`).
  Portal yöneticisi her izne sahip sayılır (`hasPermission` ile aynı kural). Kayıt düzeyindeki ek kurallarınızı
  işleyicide `ctx` ile uygulayın.
- **Kimlik.** `ctx.user` veriyi isteyen kişidir; Vekalet ya da Yerine Görüntüle oturumunda `ctx.actor` işlemi gerçekte
  yapan kişidir, denetim kaydına onu yazın. `ctx.mode` `'export'`tır.
- **Boyut.** İstek gövdesi en çok 16 KB (`413 ONE-SDK-E-152`), yanıt en çok 256 KB (`500 ONE-SDK-E-157`).
- **Sınır.** Salt-okunurluk bir kaza önleyicidir: kodunuz ham veritabanı nesnesini başka bir yoldan saklayıp
  kullanırsa yazabilir. Platformun garanti ettiği şey, veriyi okuyan uygulamanın sizin verinize yazamamasıdır.
- **Sözleşmeyi değiştirmek** (bu yayın denetimi platformun uygulamalar arası okuma güncellemesiyle gelir; o yayına alınana
  kadar yayın eskisi gibi hatta denetlenir). Yayın ve terfide aynı ad ve sürümün açıklaması, izni ya da şeması değiştirilemez: değişiklik ne kadar
  küçük olursa olsun yeni bir sürüm verin ve eskisini alan uygulamalar geçene kadar birlikte sunun (`409 ONE-APP-E-163`).
  Aynı portalda onaylı olarak okunan bir sürümü kaldıran yayın `409 ONE-APP-E-162` alır; geçersiz `exports` / `imports`
  beyanı `409 ONE-APP-E-164`. Bu denetim geri almada yapılmaz: geri alma, sözleşmesi farklı ya da onaylı okunan bir
  sürümü kaldırmış bir sürüme de dönebilir; o zaman onay yeniden istenir ya da alan uygulama okuyamaz. Ayrıntı:
  belgelerdeki `/docs/manifest` sayfasının "Yayın kuralları" bölümü.

## Başka bir uygulamanın verisini okumak (içe aktarım)

Başka bir uygulamanın açtığı veriyi okumak için önce manifestte beyan edin; şirket yöneticisi beyanı Devoplus'ın yönetim ekranında
onaylar. Onaydan sonra:

```ts
import { BackendClient, readAppExport } from '@devoplus/one-sdk'

// manifest: "imports": [{ "app": "siparis", "export": "open-orders", "version": 2, "reason": { "tr": "..." } }]
const api = new BackendClient({ baseUrl: env.API_BASE, authorization: request.headers.get('authorization') })
const { items } = await readAppExport<{ items: Array<{ id: string }> }>(api, env, 'siparis', 'open-orders', { branch: 'IST01' })
```

| Üye | Açıklama |
|---|---|
| `readAppExport(api, env, app, exportName, params?)` | Onaylı bir dışa açımı okur ve veren uygulamanın JSON nesnesini döndürür. Sürüm sizin beyanınızdan gelir (platform yayında yazar); ayrıca vermezsiniz. |
| `readImportVersion(env, app, exportName)` | Beyan ettiğiniz sürüm; beyan yoksa `null`. |

Kurallar:

- **Beyan ve onay.** Beyan edilmemiş bir çifti okumak istek gönderilmeden `400 ONE-SDK-E-161` alır. Beyanı ekleyip
  yeniden yayımlayın; şirket yöneticisi onaylayana kadar platform çağrıyı yapmaz. Beyanın gerekçesi değişirse onay
  yeniden istenir. Veren uygulama paylaştığı verinin sözleşmesini yalnız yeni bir sürümle değiştirebilir; o sürüme
  geçmek de yeni onay ister.
- **Sürüm geçişi.** Yeni bir sürüme geçen yayınınız, o sürümün onayı gelene kadar okuyamaz: platform eski onayla yeni
  kodu çalıştırmaz.
- **Parametreler.** `params` bir JSON nesnesidir, en çok 16 KB (`400 ONE-SDK-E-162`); şemaya uyduğunu platform denetler.
- **Yanıt.** Veren uygulamanın onaylı şemaya uyan nesnesidir. Nesne olmayan yanıt `502 ONE-SDK-E-163` ile hata verir,
  boş sonuç diye gösterilmez.
- **Hatalar.** Onay yok ya da geri çekilmiş, veren uygulama bu portalda kurulu değil ya da durdurulmuş, hız sınırı,
  geçici arıza ya da veren uygulamanın hatası `OneError` ile gelir; `code` platformun kodudur.
- **Sınır.** Dışa açım işleyicisinin içinden başka bir uygulamanın verisi okunamaz (derinlik 1). Yerel geliştirmede
  (`one dev`) bu servis taklit edilmez; test ortamında deneyin.

## Portal ana sayfasında veri widget'ı

Uygulama, manifest `widgets` ile beyan ettiği veri widget'larının (`stat`, `list`, `chart`, `table`) verisini üretir;
Portal bu veriyi kendi bileşenleriyle çizer. Tarayıcı uygulamanızın adresine istek atmaz: Portal veriyi Devoplus'tan
ister, Devoplus kişiyi, pano kaydını, izni ve ayarları doğrulayıp çağrıyı platform üzerinden yapar.
⚠️ Pano tasarımcısı ve Devoplus'ın widget çağrısı henüz yayında değildir: bugün işleyici tanımlanabilir ve denenebilir,
Portal onu henüz çağırmaz. Sayfa widget'ı (`page`) bu yoldan çağrılmaz; o widget kendi sayfanızı açar.

```ts
import { defineWidgets, handleExportRequest, handleWidgetRequest } from '@devoplus/one-sdk'

const widgetsDef = defineWidgets<Env>({
  'low-stock': {
    1: async (call, env) => {
      const limit = Number(call.params.limit ?? 10)
      const rows = await env.DB.prepare('SELECT sku, qty FROM stock WHERE qty < 5 ORDER BY qty LIMIT ?').bind(limit).all()
      return { columns: [{ key: 'sku', label: call.lang === 'en' ? 'Item' : 'Ürün' }, { key: 'qty', label: { tr: 'Adet', en: 'Qty' }, type: 'number' }], rows: rows.results }
    },
  },
})

export default {
  fetch(request, env, execution) {
    return handleWidgetRequest(request, env, execution) ?? handleExportRequest(request, env, execution) ?? app(request, env, execution)
  },
}
```

| Üye | Açıklama |
|---|---|
| `defineWidgets({ '<ad>': { <sürüm>: işleyici } })` | İşleyicileri ad ve sürüme göre tanımlar; ad ve sürüm manifest `widgets` beyanıyla aynı olmalı (`WIDGET_NAME_RE`, sürüm 1-999999; biçimsiz değer hemen `ONE-SDK-E-164`). Aynı ad birden çok sürümle sunulabilir. Son tanım `handleWidgetRequest`'in varsayılanıdır. |
| `OneWidgetHandler` | `(call: OneWidgetCall, env, ctx: OneContext, execution) => nesne`. Dönen değer JSON'a çevrilir; kökü nesne olmalıdır (türün yanıt şekli). |
| `OneWidgetCall` | `{ name, version, kind, params, lang, theme }`. `params` pano tasarımcısında seçilen ayarlardır (beyan yoksa `{}`); `lang` `'tr'` ya da `'en'`, `theme` `'light'` ya da `'dark'`. |
| `handleWidgetRequest(request, env, execution, definition?): Promise<Response> \| null` | Yol `/__one/widgets/...` değilse `null`. Bu yolda yalnız platform işaretli `POST` işlenir; biçimsiz yol, tanımsız ya da manifestte beyan edilmemiş veri widget'ı `404 ONE-SDK-E-165`. Çağrıdaki widget sözleşme özeti (`X-One-Widget-Digest`) bu yayının özetinden farklıysa işleyici çağrılmaz (`409 ONE-SDK-E-169`, yayın geçişi; Portal widget'ı bir sonraki yenilemede yeniden ister). Başarılı yanıt aynı başlığı taşır. |
| `readWidgetMeta(env, name, version)` | Platformun yayında yazdığı beyanı (`{ digest, kind, permission }`) okur; beyan yoksa `null`. |

**Yanıt şekli.** İşleyicinin döndürdüğü nesne, türün Portal bileşeninin verisidir (`one-stat`, `one-chart`, `one-table`;
liste için aşağıdaki şekil). Devoplus yanıtı Portal'a göndermeden önce denetler: bilinmeyen alan, yanlış tür ya da aşılan
sınır yanıtı geçersiz kılar; widget yanıtın beklenen biçimde olmadığını yazar. Sorunun yolu ve açıklaması yalnız Devoplus
kaydına yazılır, kullanıcıya gösterilmez. Metin alanları düz metin ya da `{ tr, en? }` nesnesidir ve düz metin olarak çizilir.

| Tür | Şekil | Sınırlar |
|---|---|---|
| `stat` | `{ value, label?, unit?, format?, currency?, decimals?, delta?, deltaFormat?, deltaLabel?, deltaPolarity?, tone?, caption? }`. `value` zorunludur; sayı, düz metin ya da `null` olabilir (`null` "-" çizer). `format`, `deltaFormat`, `deltaPolarity` ve `tone` değerleri `<one-stat>` ile aynıdır. | Metin en çok 200 karakter; `decimals` 0-4; `currency` üç büyük harf (`TRY`) |
| `list` | `{ items: [{ title, subtitle?, value?, tone? }] }`; `title` boş olamaz, `value` düz metindir (`{ tr, en }` değil); `tone`: `success`, `warn`, `danger`, `info` | En çok 50 öğe; `title` ve `subtitle` 200, `value` 50 karakter |
| `chart` | `{ type, data, stacked?, horizontal?, unit? }`; `type` `<one-chart>` ile aynı; `data` `{ labels, series: [{ name, values }] }` ya da `[{ label, value }]` | En çok 6 seri ve 50 etiket; her serinin `values` sayısı etiket sayısına eşit |
| `table` | `{ columns: [{ key, label, type?, sortable?, align?, options?, currency? }], rows: [{ <key>: değer }] }`. Sütun başlığı **`label`**'dır (`title` değil). Hücre düz metin, sayı, mantıksal ya da `null`'dır; satırda sütun olmayan anahtar bulunamaz. Sütun açıkça `sortable: false` değilse Portal'da başlığına tıklanarak sıralanır. | 1-12 sütun, en çok 100 satır, sütun başına en çok 100 seçenek; hücre metni 500 karakter; sütun anahtarı harf ya da alt çizgiyle başlar, en çok 64 karakter |

Kurallar:

- **Ayarlar doğrulanmış gelir.** `params` Devoplus'ta manifestteki ayar şemasına göre doğrulanır; SDK onu yeniden
  doğrulamaz. Özet karşılaştırması, işleyicinizin yalnız bu yayının şemasına göre doğrulanmış ayarı görmesini sağlar.
- **İzin.** Beyan `permission` taşıyorsa, kişide o izin yoksa işleyici hiç çağrılmaz (`403 ONE-SDK-E-167`). Portal
  yöneticisi her izne sahip sayılır.
- **Kimlik.** `ctx.user` widget'ı gören kişidir; `ctx.mode` `'widget'`tır. Vekalet ya da Yerine Görüntüle oturumunda
  widget yalnız manifestte `actingSessions: "write"` beyan eden uygulamada çağrılır; `ctx.actor` işlemi gerçekte yapan
  kişidir.
- **Yalnız okuma servisleri.** E-posta, SMS, mobil bildirim ve DEXA'ya yazma bu çağrıda kullanılamaz
  (`403 ONE-SDK-E-158`). Widget veriyi gösterir; kaydı değiştirmez.
- **Boyut.** İstek gövdesi en çok 4 KB (`413 ONE-SDK-E-166`), yanıt en çok 64 KB (`500 ONE-SDK-E-171`).

## Devoplus servisleri arka plan işinde ve ajan aracında

İşte ve ajan aracında tarayıcıdan gelen bir belirteç yoktur; servis istemcileri (`BackendClient`) yine de çağrı yapar:
platform çıkış katmanı isteği doğru kimlikle Devoplus'a taşır (`new BackendClient({ baseUrl, authorization: null })` ya
da `request.headers.get('authorization')`, ikisi de olur). Uygulamanın yazdığı `Authorization` başlığı bu iki yolda
Devoplus'a **iletilmez**.

| Yol | Kimin adına | Kullanılabilen servisler |
|---|---|---|
| Kuyruk / zamanlanmış iş (`ctx.mode === 'job'`) | **Uygulamanın** kendi kimliği; kişi yoktur | Yalnız manifest `jobs.services` ile beyan edilip **kurulumda portal yöneticisinin onayladığı** servisler: `mail.execute`, `sms.execute`, `push.execute`, `dexa.read`, `ai.use` (her biri manifest `scopes`'ta da olmalı). Onaysız servis 403 alır. ⛔ DEXA'ya yazma (`dexa.write`) bu yolla **açılmaz**. "Kendime gönder" yoktur: alıcıyı açıkça verin. Denetim kaydında aktör uygulamadır. |
| Dışa açım işleyicisi (`ctx.mode === 'export'`) | Veriyi isteyen **kişi** (başka bir uygulamanın kullanıcısı) | Yalnız okuma servisleri. E-posta, SMS, mobil bildirim, DEXA'ya yazma, kuyruğa iş bırakma ve başka bir uygulamanın verisini okuma platformda reddedilir (`403 ONE-OUT-E-003`). `ctx.callerApp`: veriyi isteyen uygulama. |
| Ajan aracı (`ctx.mode === 'agent'`) | Aracı çağıran **kişi** | Kişinin bu uygulamada yapabildikleri; süreli onay gerektiren scope'lar (`CONSENT_SCOPES`: e-posta, SMS, mobil bildirim ve DEXA'ya yazma) bu yolda **kullanılamaz** (kişi onay ekranını göremez). `ctx.admin`: kişi kurulumun portalının şirket yöneticisi; `ctx.actor`: Vekalet / Yerine Görüntüle oturumunda işlemi yapan kişi. |

```jsonc
// one.manifest.json
"scopes": ["ai.use", "mail.execute"],
"jobs": { "schedules": [{ "name": "weekly-summary", "cron": "0 8 * * 1" }], "services": ["ai.use", "mail.execute"] }
```

## Günlük

`logEvent(ctx, level, message, data?)` sistem/çalışma zamanı olayı; `logError(ctx, message, error, data?)` beklenmeyen
hata (ad, ileti ve en çok 4.000 karakterlik yığın; `OneError` kodu ve durumu korunur); `logAudit(ctx, action, target?,
data?)` kritik kullanıcı işlemi; aktör zorunludur (anonim bağlamda fırlatır). Hepsi tek satır JSON olarak yazılır ve
**platform günlüğünde saklanır**: platform her kurulumda günlüğü açar, örnekleme manifest
`observability.headSamplingRate` (varsayılan 1). Satırları platform ekibi kurulum (uygulama × portal) süzgeciyle
görür; uygulamanın son hata kayıtları kısaltılmış olarak kullanım ve hata panosunda görünür (`one status`).

Koruma: `data` içinde adı parola, token, secret, authorization, cookie, API anahtarı, private key, credential ya da
parola/şifre geçen alanların değeri `"[gizli]"` olur (her derinlikte, girdi değişmez; `redact`); JSON'u 8 KiB'ı aşan
`data` yerine `{ truncated, bytes }` yazılır (platform günlüğü çağrı başına en çok 256 KB tutar, aşan kısım düşer;
`safeData`); ileti 1.000 karakterde kırpılır. Maskeleme bir emniyet ağıdır, izin değildir: kişisel veri ve sır yazmayın.
`problemResponse(error, ctx?)` bilinmeyen hatayı bu kayda (`ctx` verilirse bağlamıyla) düşer; yanıtta yalnız sabit
metin döner.

## Denetim günlüğü (kalıcı)

`recordAudit(api, ctx, action, target?, data?)`, `logAudit` izini yazar **ve** `POST One/services/audit` ile
Devoplus denetim günlüğüne kaydeder (scope `audit.write`). Kayıt Devoplus Console Denetim Günlüğü'nde "Devoplus
One uygulaması" türüyle, `reason = {slug}:{action}`, `targetLabel = target`, `details = data` olarak görünür; aktör
ve portal istekle gönderilmez, backend doğrulanmış bağlamdan alır. Kurallar sunucuyla birebir: `action`
`^[a-z0-9][a-z0-9._:-]*$` ≤ 64, `target` ≤ 200, `data` JSON nesnesi ≤ 4 KiB (`buildAuditRequest`; ihlal
`ONE-SDK-E-030...032` ile FIRLATIR, çünkü programlama hatasıdır). Uzak yazma **best-effort**: ağ/kapı hatası
`{ recorded: false }` + uyarı olayı döner, isteğiniz düşmez. ⛔ `target`/`data`ya kişisel veri ya da sır yazmayın
(365 gün saklanır).

## Yapay zeka servisi

`completeAi(api, ctx, istek)` (eşdeğeri `ai.complete`), `POST One/services/ai` ucunu çağırır (scope `ai.use`; kullanıcıdan ayrıca
onay istenmez). Diğer `One/services/*` uçlarıyla aynı kapıdan geçer; kullanıcı, portal ve uygulama istekle GÖNDERİLMEZ.

```ts
const { output } = await completeAi(api, ctx, { task: 'summarize', input: metin, instructions: 'üç madde', lang: 'tr' })
const { label } = await completeAi(api, ctx, { task: 'classify', input: talep, labels: ['acil', 'normal'] })
const { json } = await completeAi(api, ctx, { task: 'extract', input: fatura, schema: { type: 'object', properties: { tutar: { type: 'number' } } } })
```

| Alan | Kural (istemcide de denetlenir; ihlal istek gönderilmeden FIRLATIR) |
|---|---|
| `task` | `summarize`, `classify`, `extract`, `generate`, `chat` (`ONE-SDK-E-080`) |
| `input` | 1-24.000 karakter (`ONE-SDK-E-081`) |
| `instructions`, `lang` | ≤ 4.000 karakter; `tr` / `en` (`ONE-SDK-E-082`) |
| `labels` | yalnız `classify`; 2-50 tekil etiket, her biri ≤ 100 karakter (`ONE-SDK-E-083`) |
| `schema` | yalnız `extract`; JSON nesnesi ≤ 8 KiB (`ONE-SDK-E-084`) |
| `messages` | yalnız `chat`; ≤ 20 ileti, `role` = `user` ya da `assistant`, `content` dolu metin (`ONE-SDK-E-085`) |

Yanıt `OneAiResult`: `output` (metin), `json?` (alan çıkarma), `label?` (sınıflandırma), `model`, `usage`
(`inputTokens`, `outputTokens`). Tanınmayan yanıt `ONE-SDK-E-086` fırlatır. Model, kota ve portalda yapay zekanın açık
olup olmadığı Devoplus'tadır: kapalı/yapılandırılmamış portalda 503, kota dolunca 429 (`OneError`). ⛔ Model **sayı
hesaplamaz** ve **kayıt yazmaz**: tutar/toplam/tarih farkı kodda hesaplanıp `input`'a hazır verilir; çıktı bir öneridir,
veritabanına yazmadan önce kod doğrular. `input`'a sır ya da işin gerektirmediği kişisel veri konmaz.

## Devoplus yapay zekasına açılan araçlar

Devoplus yapay zekası, kullanıcı Devoplus'ta bir soru sorduğunda uygulamanın verisine ve işlemlerine yalnız manifest
`agent.tools[]` ile beyan edilen **araçlarla**, kullanıcının kendi kimliğiyle ulaşır. Her araç üç yerde durur: manifest
beyanı (ad, TR/EN başlık + açıklama, `readOnly`, `permission?`, `input` şeması) + işleyici + `AGENT_GUIDE.md`.

```ts
import guide from '../AGENT_GUIDE.md' with { type: 'text' } // derlemede metin olarak gömülür
import manifest from '../one.manifest.json' with { type: 'json' }

const tools = defineAgentTools<Env>({
  'list-notes': { handler: async ({ input, ctx, env }) => ({ notes: await notlariGetir(env, ctx.user!.sub, input.limit) }) },
})

export default {
  async fetch(request, env) {
    const agent = (await handleAgentRequest(request, env, tools, manifest)) ?? agentManifestResponse(request, manifest, guide)
    if (agent) return agent
    // ... uygulamanın kendi yönlendirmesi
  },
} satisfies OneApp<Env>
```

`handleAgentRequest` yalnız `POST /__one/agent/<ad>` için yanıt üretir (başka yolda `null`). Kapılar sırasıyla:
`X-One-Invoked-By: devoplus-agent` (yalnız platformun imzalı çağrısı yazar; tarayıcıdan gelen 404); araç manifest'te
VE `defineAgentTools`'ta (beyansız 404 `ONE-SDK-E-091`, işleyicisiz 501 `ONE-SDK-E-092`); oturum; `readOnly: false`
araç Vekalet / Yerine Görüntüle oturumunda 403 (`ONE-SDK-E-095`); `permission` varsa `hasPermission` (403
`ONE-SDK-E-004`); gövde JSON ve ≤ 256 KiB (`ONE-SDK-E-093`); girdi şemaya uygun (400 `ONE-SDK-E-094` + `errors:
[{ path, message }]`). İşleyici `{ input, ctx, env }` alır, JSON'a çevrilebilir bir değer döndürür; fırlattığı `OneError`
ProblemDetails olur. `ctx.admin` çağıran kişinin kurulumun portalının şirket yöneticisi olduğunu, `ctx.actor` Vekalet /
Yerine Görüntüle oturumunu bildirir. İşleyici Devoplus servislerini kişi adına çağırabilir; süreli onay gerektiren
scope'lar (e-posta, SMS, mobil bildirim ve DEXA'ya yazma) bu yolda kullanılamaz.

Girdi şeması JSON Schema'nın dar bir alt kümesidir: `type` (`object`, `string`, `number`, `integer`, `boolean`,
`array`), `properties`, `required`, `enum`, `minimum`, `maximum`, `minLength`, `maxLength`, `items`,
`description` (`AGENT_INPUT_KEYWORDS`). ⛔ Nesneler kapalıdır: şemada olmayan alan reddedilir. Doğrulayıcı
`validateAgentInput(şema, girdi)` olarak da dışa açıktır.

`agentManifestResponse(request, manifest, guide)` oturum istemeden `GET /.well-known/one-agent.json` (yalnız
`app: { slug, name }` + `agent`) ve `GET /.well-known/one-agent.md` (`AGENT_GUIDE.md`, ≤ 256 KiB) sunar; manifest'te
`agent` yoksa 404. Rehbere kullanıcı verisi, sır ya da iç adres yazılmaz.

## Veri modeli ve kayıt işlemleri

Kayıt tutan uygulamada tabloyu, tipleri ve uçları elle yazmak yerine `one.data.json`'da **varlıkları ve alanları**
tanımlayın; gerisini `one data generate` ve `createCrudHandler` yapar.

```json
{
  "version": 1,
  "entities": {
    "talepler": {
      "title": { "tr": "Talepler", "en": "Requests" },
      "fields": {
        "baslik": { "type": "text", "title": { "tr": "Başlık", "en": "Title" }, "required": true, "maxLength": 120 },
        "tutar":  { "type": "money", "title": { "tr": "Tutar", "en": "Amount" }, "min": 0 },
        "durum":  { "type": "choice", "title": { "tr": "Durum", "en": "Status" }, "required": true, "default": "acik", "index": true,
                    "choices": [ { "value": "acik", "title": { "tr": "Açık", "en": "Open" } },
                                 { "value": "kapali", "title": { "tr": "Kapalı", "en": "Closed" } } ] },
        "sorumlu": { "type": "person", "title": { "tr": "Sorumlu", "en": "Owner" } }
      }
    }
  }
}
```

| Tür | Saklanan | API'de | Kural |
|---|---|---|---|
| `text` / `longText` | metin | metin | `maxLength` (varsayılan 200 / 10.000; en çok 1.000 / 50.000); `text` tek satır |
| `number` | ondalıklı sayı | sayı | `min` / `max` |
| `money` | **kuruş, tam sayı**; kolon `<ad>_minor` | `"1234.50"` (metin) | en çok iki ondalık; **yuvarlanmaz, reddedilir**; binlik ayırıcı yok; `min` / `max` lira |
| `date` | `YYYY-AA-GG` | aynı | gerçek takvim günü |
| `datetime` | UTC ISO (`...Z`) | aynı | gönderilirken saat dilimi zorunlu (`+03:00` ya da `Z`) |
| `boolean` | 0/1 | `true`/`false` | |
| `choice` | `value` kodu (kısıtsız; seçenek denetimi kayıt işlemlerinde) | kod | kod `^[a-z0-9][a-z0-9_-]{0,39}$`; görünen ad `title`; yeni değer migration'sız eklenir |
| `person` | kullanıcı kimliği (`sub`) | aynı | |
| `file` | dosya anahtarı (`OneFiles`) | aynı | |

Her alan: `required`, `unique` (silinmemiş kayıtlar arasında), `index`, `default`. Ad kuralı `^[a-z][a-z0-9_]{0,39}$`
(çift alt çizgi, sondaki alt çizgi, `_minor` / `_fold` ekleri ve `order`, `group`, `not` gibi SQL anahtar sözcükleri
reddedilir); otomatik kolonlar `id` (uuid), `created_at`, `created_by`, `updated_at`, `updated_by`, `deleted_at` alan
adı olamaz. En çok 30 varlık, varlık başına 60 alan ve tabloda en çok 100 kolon (her metin alanının bir arama kolonu
`<alan>_fold` vardır, aşağıya bakın). `softDelete` varsayılan `true`; `false` yalnız en az 10 karakterlik
`hardDeleteReason` gerekçesiyle kabul edilir (karakterler harf harf sayılır, yalnız boşluk sayılmaz; gerekçe yoksa `one data generate` ve one-ci veri kapısı durur; model
`createCrudHandler`'a koddan verildiyse kayıt uçları 500 `ONE-SDK-E-100` döner).

**`one data generate`** şunları üretir: `migrations/NNNN_one_data.sql` (bir sonraki numara) + `src/data/types.ts` (`<Varlık>Record`,
`<Varlık>Input`, `dataModel`) + `migrations/.one-data-snapshot.json` (bir sonraki farkın tabanı; elle düzenlenmez).
`--check` hiçbir şey yazmaz, dosyalar güncel değilse 1 ile çıkar. **Yalnız ekleyen** değişiklik üretilir: yeni varlık,
yeni alan (zorunluysa `default` şart), yeni dizin. Seçim alanına **yeni değer eklemek** migration gerektirmez (yalnız
tipler ve anlık görüntü yenilenir); seçim değeri **kaldırmak** reddedilir (var olan kayıtlar o değeri taşıyabilir).
Alan/varlık silme, ad değiştirme, tür değiştirme, zorunluluk değişikliği, tekillik değişikliği ve dizin kaldırma
algılanırsa komut hiçbir dosya yazmadan nedenini söyleyip durur (görünen adlar serbesttir). Üretilmiş migration
uygulandıktan sonra değiştirilmez. ⚠️ Bu kurallardan önceki bir SDK ile kurulmuş tablolarda seçim değerleri veritabanında
sabitlenmiştir (anlık görüntüde `choiceChecks`); o alanlara değer eklemek yine reddedilir. Eski anlık görüntüyle ilk
üretim metin alanlarına arama kolonlarını ekleyen yeni bir migration yazar.

```ts
import { createCrudHandler, problemResponse, readContext } from '@devoplus/one-sdk'
import { dataModel } from './data/types.ts'

const talepler = createCrudHandler({ model: dataModel, entity: 'talepler', records: 'shared', canWrite: 'One.Talep.Write' })
// fetch içinde:  const res = await talepler.handle(request, env.DB); if (res) return res
```

| Yöntem | Yol | İş |
|---|---|---|
| GET | `/api/<varlık>` | `{ items, page, pageSize, total }` döner; parametreler `_page`, `_size` (≤ 200), `_sort=alan` / `-alan`, `alan=değer`, `alan.contains=metin` (metin alanları), `alan.from` / `alan.to` (sayı, para, tarih; dahil); en çok 20 süzgeç |
| POST | `/api/<varlık>` | 201 + kayıt |
| GET | `/api/<varlık>/<id>` | kayıt |
| PATCH | `/api/<varlık>/<id>` | gönderilen alanlar |
| DELETE | `/api/<varlık>/<id>` | 204 (yumuşak silme) |
| GET | `/api/<varlık>/export.csv`, `export.xlsx`, `report.html` | aynı süzgeçlerle dışa aktarma / yazdırma sayfası (`_lang=tr|en`) |

**Hazır liste bileşeniyle doğrudan bağlanır** (`<one-table src="/api/<varlık>" paging="page" export="xlsx csv">`): uçlar
bileşenin sorgu lehçesini de kabul eder: `limit` (= `_size`), `page` (= `_page`), `cursor` (sayfa numarası; yanıttaki
`nextCursor`, son sayfada `null`), `sort` + `order=asc|desc` (= `_sort`), `q` (metin alanlarının **herhangi birinde**
içerir), `<alan>From` / `<alan>To` (= `.from` / `.to`; `<one-filter>` tarih aralığının varsayılan adları), `lang`
(= `_lang`), `format=xlsx|csv` (listeyle aynı adreste dışa aktarma). Lehçe adı modeldeki bir alanın adıysa (ör. alan
`page`) o parametre alan süzgecidir.

Seçenekler: `records` (**zorunlu**: `shared` ile herkes görür, `own` ile herkes yalnız kendi oluşturduğunu), `canRead` /
`canWrite` (uygulama izni kodu ya da `(ctx) => boolean`), `actingSessions` (`read` varsayılan; `write` Vekalet / Yerine
Görüntüle'de yazmayı açar; manifest'te de `actingSessions: "write"` gerekir), `basePath`, `pageSize`, `exportLimit`
(varsayılan 10.000), `timeZone`. Okuma ve yazma oturum ister. Gövde yalnız `application/json`, ≤ 1 MB; sistem alanı
gönderilemez (`created_by` oturumdan yazılır). Hata gövdesi RFC 9457 + `errors: [{ field, message }]` (Türkçe,
kullanıcıya gösterilebilir). Değerler SQL'e her zaman bağlı parametre olarak gider; sıralama ve süzgeç adları yalnız
modelden seçilir.

**Arama Türkçe katlanmıştır:** `alan.contains` ve `q` büyük/küçük harfe ve Türkçe harflere duyarsızdır ("istanbul"
"İSTANBUL"u, "cay" "Çay"ı bulur; İ ı I i aynı sayılır). Her metin alanının yanında `<alan>_fold` arama kolonu durur;
`createCrudHandler` onu yazar, API yanıtında görünmez. Kolonu boş olan kayıt (eski kayıt ya da elle yazılmış SQL) da
bulunur. ⚠️ Bir metin alanını **elle yazdığınız SQL ile güncelliyorsanız** arama kolonunu da yazın
(`"<alan>_fold" = ?` + `foldSearchText(değer)`) ya da `NULL` yapın; yoksa arama eski metne göre bulur.

## Rapor ve dışa aktarma

`toCsv(rows, columns, { lang, delimiter, timeZone })`; `toXlsx(rows, columns, { lang, sheetName, timeZone })`;
`exportResponse(içerik, { fileName, format: 'csv' | 'xlsx' | 'html' })`; `renderPrintableReport({ title, subtitle,
columns, rows, totals, lang })`. Sütun: `{ key, title: { tr, en }, type?: 'text' | 'number' | 'money' | 'date' |
'datetime' | 'boolean', labels?, width? }`.

- **CSV:** UTF-8 + BOM (Excel Türkçe karakteri bozmasın), Türkçede `;` ayırıcı ve `1.234,50` sayı (Türkçe bölge ayarlı
  Excel'in liste ayırıcısı `;`'dir), İngilizcede `,`. `=`, `+`, `-`, `@` ile başlayan metin hücresine `'` eklenir
  (formül enjeksiyonu).
- **XLSX:** bağımlılıksız; sayı/para/tarih hücreleri sayı olarak yazılır ve biçim alır (Excel'de toplanabilir), metin
  hücresi formül olarak çalışmaz; ilk satır sabitlenir. En çok 50.000 satır, 100 sütun.
- **İndirme:** arayüz dosyayı belirteçli istekle alır ve `one.download({ name, mime, data })` ile Portal'a verir (en çok
  25 MB). Dosya adı `Content-Disposition`'dadır (`filename*`, Türkçe karakterli; `downloadFileName(header)`).
- **PDF:** ayrı motor yok. `renderPrintableReport` betiksiz bir yazdırma sayfası üretir; arayüz onu gizli bir
  çerçevede açıp `print()` çağırır, kullanıcı "PDF olarak kaydet"i seçer.

## Hata kodları

| Kod | Durum | Anlam |
|---|---|---|
| `ONE-SDK-E-001` | 500 | Platform bağlamı eksik (dispatch dışından çağrı) |
| `ONE-SDK-E-002` | 401 | Oturum gerekli |
| `ONE-SDK-E-003` | 403 | Scope yok (onaya bağlı scope'ta kullanıcı henüz izin vermemiş olabilir) |
| `ONE-SDK-E-004` | 403 | Uygulama izni yok (`requirePermission`) |
| `ONE-SDK-E-010...013` | 400 | Dosya anahtarı / adı geçersiz |
| `ONE-SDK-E-014` | 413 | Dosya boyut tavanı (`put` ve `readBoundedBody`) |
| `ONE-SDK-E-015` | 500 | Dosya yazılamadı |
| `ONE-SDK-E-016` | 400 | `Content-Length` başlığı geçersiz |
| `ONE-SDK-E-020...021` | 400 | Önbellek anahtarı geçersiz |
| `ONE-SDK-E-022` | 400 | Önbellek TTL sınır dışı ya da eksik |
| `ONE-SDK-E-023` | 400 | Önbellek değeri JSON'a çevrilemiyor |
| `ONE-SDK-E-024` | 413 | Önbellek değeri 1 MiB tavanı |
| `ONE-SDK-E-040` | 400 | DEXA sorgu anahtarı geçersiz |
| `ONE-SDK-E-041` | 400 | DEXA parametre kümesi nesne değil ya da 32'den fazla |
| `ONE-SDK-E-042` | 400 | DEXA parametre adı geçersiz |
| `ONE-SDK-E-043` | 400 | DEXA `LangCode` gönderilemez; platformun bağlam parametresidir (harf duyarsız) |
| `ONE-SDK-E-044` | 400 | DEXA parametre değeri metin değil ya da 2.000 karakteri aşıyor |
| `ONE-SDK-E-045` | 400 | DEXA işlem anahtarı geçersiz |
| `ONE-SDK-E-046` | 400 | DEXA işlem parametre kümesi nesne değil ya da 32'den fazla |
| `ONE-SDK-E-047` | 400 | DEXA işlem parametre adı geçersiz |
| `ONE-SDK-E-048` | 400 | DEXA işlem parametre değeri metin değil ya da 65.536 karakteri aşıyor |
| `ONE-SDK-E-049` | 400 | `Idempotency-Key` kanonik UUID değil ya da boş UUID |
| `ONE-SDK-E-050` | 400 | Org kimliği 24 karakterlik onaltılık değil (istek gönderilmez) |
| `ONE-SDK-E-051` | 400 | `orgUsersMatching` ölçütsüz çağrıldı |
| `ONE-SDK-E-060` | 400 | Bildirim senaryosu alanı boş / tanınmayan sonuç / geçersiz doğrulama kodu ya da süresi |
| `ONE-SDK-E-061` | 400 | Bildirim bağlantısı https değil ya da kimlik bilgisi taşıyor |
| `ONE-SDK-E-062` | 400 | Bildirim metni backend sınırını aşıyor |
| `ONE-SDK-E-063` | 400 | Alıcı ya da kanal verilmedi |
| `ONE-SDK-E-070` | 400 | İş / kuyruk adı geçersiz |
| `ONE-SDK-E-071` | 400 | Kuyruk iletisi gövdesi JSON'a çevrilemiyor |
| `ONE-SDK-E-072` | 413 | Kuyruk iletisi gövdesi 64 KiB tavanı |
| `ONE-SDK-E-073` | 400 | `delaySeconds` 0-86.400 tam sayı değil |
| `ONE-SDK-E-074` | 503 | İleti kuyruğa bırakılamadı (ağ hatası ya da beklenmeyen yanıt) |
| `ONE-SDK-E-075` | 404 | İş bulunamadı (`handleJobRequest`: bilinmeyen tür/ad ya da platform dışı çağrı) |
| `ONE-SDK-E-076` | 400 | İş çağrısının gövdesi JSON değil |
| `ONE-SDK-E-080...085` | 400 / 413 | Yapay zeka isteği geçersiz (iş, metin, talimat/dil, etiketler, şema, sohbet) |
| `ONE-SDK-E-086` | 502 | Yapay zeka yanıtı tanınmadı |
| `ONE-SDK-E-090` | 500 | `defineAgentTools`: araç adı geçersiz ya da işleyici yok |
| `ONE-SDK-E-091` | 404 | Araç yok / bu yoldan çağrılamaz (beyansız araç, imzasız çağrı, yanlış yöntem) |
| `ONE-SDK-E-092` | 501 | Araç manifest'te beyanlı ama işleyicisi tanımlanmamış |
| `ONE-SDK-E-093` | 400 / 413 | Araç girdisi geçerli JSON değil ya da 256 KiB'ı aşıyor |
| `ONE-SDK-E-094` | 400 | Araç girdisi şemaya uymuyor (`errors` alan listesi) |
| `ONE-SDK-E-095` | 403 | Değiştiren araç Vekalet / Yerine Görüntüle oturumunda çağrılamaz |
| `ONE-SDK-E-096` | 500 | Ajan rehberi 256 KiB'ı aşıyor |
| `ONE-SDK-E-100` | 500 | Veri modeli ya da `createCrudHandler` yapılandırması geçersiz (`one data generate` ile düzeltin) |
| `ONE-SDK-E-101` | 400 | Kayıt doğrulanamadı (`errors: [{ field, message }]`) |
| `ONE-SDK-E-102` | 404 | Kayıt bulunamadı (silinmiş ya da başkasının, `records: 'own'`) |
| `ONE-SDK-E-103` | 409 | Tekil alan değeri başka bir kayıtta kullanılıyor |
| `ONE-SDK-E-104` | 400 | Liste sorgu parametresi geçersiz (bilinmeyen alan, uygunsuz süzgeç, sayfa boyu) |
| `ONE-SDK-E-105` | 405 | Bu adreste bu yöntem kullanılamaz |
| `ONE-SDK-E-106` | 403 | Vekalet / Yerine Görüntüle oturumunda kayıt değiştirilemez |
| `ONE-SDK-E-107` | 403 | `canRead` / `canWrite` izni yok |
| `ONE-SDK-E-108` | 413 | Kayıt gövdesi 1 MB'ı aşıyor |
| `ONE-SDK-E-109` | 415 | Kayıt gövdesi `application/json` değil |
| `ONE-SDK-E-110` | 400 | Dışa aktarılacak kayıt sayısı sınırı aşıyor (süzgeci daraltın) |
| `ONE-SDK-E-120` | 400 | Dışa aktarma sütunu / saat dilimi / toplam sütunu geçersiz |
| `ONE-SDK-E-121` | 400 | Dışa aktarma satır sınırı (50.000) ya da 25 MB indirme sınırı aşıldı |
| `ONE-SDK-E-130` | 400 | Kişi araması 2-100 karakter olmalı (`peopleSearchHandler`) |
| `ONE-SDK-E-131` | 405 | Kişi araması yalnız `GET` |
| `ONE-SDK-E-140` | 400 | DEXA bağlam kodu (`UserCode`, `CompanyCode`, `CostCenterCode`) boş ya da yalnız boşluk (istek gönderilmez) |
| `ONE-SDK-E-150` | 400 | Dışa açım adı ya da sürümü geçersiz, işleyici işlev değil (`defineExports`) |
| `ONE-SDK-E-151` | 404 | Dışa açım bulunamadı (`handleExportRequest`: biçimsiz yol, tanımsız ya da beyan edilmemiş ad/sürüm, platform dışı çağrı) |
| `ONE-SDK-E-152` | 413 | Dışa açım isteğinin gövdesi 16 KB sınırını aşıyor |
| `ONE-SDK-E-153` | 403 | Dışa açımın beyan ettiği izin çağıran kişide yok (işleyici çağrılmaz) |
| `ONE-SDK-E-154` | 400 | Dışa açım isteğinin gövdesi bir JSON nesnesi değil |
| `ONE-SDK-E-155` | 403 | Dışa açım işleyicisinde yazma ya da şema değiştiren deyim (salt-okunur veritabanı) |
| `ONE-SDK-E-156` | 500 | Dışa açım işleyicisi JSON nesnesi döndürmedi |
| `ONE-SDK-E-157` | 500 | Dışa açım yanıtı 256 KB sınırını aşıyor |
| `ONE-SDK-E-158` | 403 | Yazan servis (e-posta, SMS, bildirim, DEXA'ya yazma) dışa açım ya da widget işleyicisinde kullanılamaz |
| `ONE-SDK-E-159` | 503 | Platformun onayladığı sözleşme özeti bu yayınınkiyle aynı değil; yayın geçişi sürüyor (işleyici çağrılmaz, yeniden denenir) |
| `ONE-SDK-E-160` | 400 | `readAppExport`: uygulama kısa adı ya da dışa açım adı geçersiz (istek gönderilmez) |
| `ONE-SDK-E-161` | 400 | `readAppExport`: çift manifestte `imports` ile beyan edilmemiş (istek gönderilmez) |
| `ONE-SDK-E-162` | 400 | `readAppExport`: parametreler nesne değil, JSON'a çevrilemiyor ya da 16 KB'ı aşıyor (istek gönderilmez) |
| `ONE-SDK-E-163` | 502 | `readAppExport`: veren uygulamanın yanıtı JSON nesnesi değil |
| `ONE-SDK-E-164` | 400 | Widget adı ya da sürümü geçersiz, işleyici işlev değil (`defineWidgets`) |
| `ONE-SDK-E-165` | 404 | Widget bulunamadı (`handleWidgetRequest`: biçimsiz yol, tanımsız ya da beyan edilmemiş veri widget'ı, platform dışı çağrı) |
| `ONE-SDK-E-166` | 413 | Widget isteğinin gövdesi 4 KB sınırını aşıyor |
| `ONE-SDK-E-167` | 403 | Widget'ın beyan ettiği izin kişide yok (işleyici çağrılmaz) |
| `ONE-SDK-E-168` | 400 | Widget isteğinin gövdesi `{ lang, theme, params }` biçiminde bir JSON nesnesi değil |
| `ONE-SDK-E-169` | 409 | Çağrıdaki widget sözleşme özeti bu yayınınkiyle aynı değil; yayın geçişi sürüyor (işleyici çağrılmaz) |
| `ONE-SDK-E-170` | 500 | Widget işleyicisi JSON nesnesi döndürmedi |
| `ONE-SDK-E-171` | 500 | Widget yanıtı 64 KB sınırını aşıyor |
| `ONE-SDK-E-999` | 500 | Beklenmeyen hata (`problemResponse`) |
| `ONE-DEV-E-001` | 404 | Yalnız yerelde (`one dev`): bu Devoplus ucu yerel taklitte yok |
| `ONE-DEV-E-002` | 404 | Yalnız yerelde: kullanıcı grubu örnek veride yok |
| `ONE-DEV-E-003` | 400 | Yalnız yerelde: taklit uca giden gövde geçerli bir JSON nesnesi değil ya da alan tipi yanlış |
| `ONE-DEV-E-004` | 404 | Yalnız yerelde: yol yerel çalışma ortamına ayrılmış |
| `ONE-DEV-E-005` | 404 | Yalnız yerelde: organizasyon kullanıcısı örnek veride yok |

Backend'den dönen kodlar (`ONE-APP-E-*`, `USRDIR-*` vb.) `OneError.code` ile olduğu gibi taşınır.

## Sürümleme

SemVer; `0.x` boyunca küçük sürüm artışı kırıcı olabilir; `CHANGELOG.md` **Kırıcı** başlığına bakın. Paket iki
kanaldan otomatik yayımlanır (`Publish SDK (npm)` iş akışı):

| Kanal | Ne zaman | Sürüm | npm etiketi |
|---|---|---|---|
| Ön sürüm | Platform deposunun `test` dalına birleşmede | `<package.json sürümü>-next.<n>` (ör. `0.3.0-next.512`) | `next` |
| Kararlı | `main` dalına birleşmede | `package.json` sürümü | `latest` |

Sürüm npm'de zaten varsa hiçbir şey yapılmaz; etiketi geriye çekecek sürüm (npm'dekilerden düşük) reddedilir. Sürüm
SemVer 2.0'dır; derleme üstverisi (`+...`) karşılaştırmada sayılmaz. Yayımlanan her zaman dalın ucundaki sürümdür. Ön sürüm
numarası `n` dalın ucundaki commit sayısıdır, dolayısıyla her yeni ön sürüm bir öncekinden büyüktür. Bir sürüm kararlı
olarak çıktıktan sonra `test`'teki `package.json` sürümü yükseltilir; aksi halde ön sürüm kanalı durur.

**Yayın hattında SDK sürüm denetimi.** `one-ci validate` (yayın hattının ilk adımı; `plan` ve `deploy` de geçer)
uygulamanın kurduğu SDK sürümünü asgari sürümle karşılaştırır. Bugünkü asgari sürüm `0.2.0-next.158`'dir. Sürüm sırasıyla
`node_modules/@devoplus/one-sdk`, `pnpm-lock.yaml`, `package-lock.json` ve `package.json` bildiriminden okunur. Kurulu sürüm
asgari sürümden eskiyse ya da okunamıyorsa hat çıktısına `⚠ SDK:` ile başlayan bir uyarı yazılır ve yayın **durmaz**.
Platform bu denetimi ileride zorlayıcı yapabilir (o zaman eski SDK yayını durdurur); uyarıyı kalıcı bırakmayın, SDK'yı
güncelleyip kilit dosyasıyla birlikte yükleyin.

⚠️ `^0.3.0` aralığı ön sürümleri KAPSAMAZ (SemVer kuralı). Ön sürüm de kurulsun isteniyorsa aralık `^0.3.0-next.0`
yazılır: bu aralık `0.3.0-next.*` ve `0.3.x` kararlı sürümlerini kapsar, `0.4.0`'ı kapsamaz. `^0.2.0-next.0` aralığı
`0.3.0`'a hiç yükselmez: 0.3'e geçmek için `pnpm add @devoplus/one-sdk@latest` (ön sürüm kanalında kalmak için `@next`), ardından `npx one upgrade`.
