@appaflytech/zeyrek-ai-mcp 0.0.55 → 0.0.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/config.js CHANGED
@@ -32,6 +32,7 @@ export const moduleNames = [
32
32
  "dependency",
33
33
  "deployment",
34
34
  "environment",
35
+ "customDomain",
35
36
  "merge",
36
37
  "activity",
37
38
  "revision",
@@ -70,6 +71,7 @@ export const modulePresets = {
70
71
  "access",
71
72
  "dependency",
72
73
  "environment",
74
+ "customDomain",
73
75
  "merge",
74
76
  "activity",
75
77
  "revision",
@@ -106,7 +108,7 @@ export const modulePresets = {
106
108
  "mobile",
107
109
  "assistant",
108
110
  ],
109
- admin: ["platform", "template", "organization", "deployment", "environment", "merge", "activity"],
111
+ admin: ["platform", "template", "organization", "deployment", "environment", "customDomain", "merge", "activity"],
110
112
  };
111
113
  function expandModuleNames(entries) {
112
114
  const modules = [];
@@ -822,6 +822,18 @@ export const backendRules = [
822
822
  guide: 'ngapps://guides/entity',
823
823
  check: body => entityTransferFindings(body)
824
824
  },
825
+ {
826
+ id: 'ENT-019',
827
+ module: 'entity',
828
+ commands: { create: 'CreateEntityCommand', update: 'UpdateEntityCommand' },
829
+ status: 400,
830
+ summary: "roles tablosunda recordTransferSkipExisting açılamaz",
831
+ sourceRef: 'RecordTransferValidator.ValidateAsync: requestedSkipExisting && entity.Id == Defaults.RoleEntityGuid ⇒ RecordTransferSkipExistingNotAvailableForRolesException (ADR 0072, K-4)',
832
+ guide: 'ngapps://guides/deployment',
833
+ check: body => String(body.slug ?? '').toLowerCase() === 'roles' && body.recordTransferSkipExisting === true
834
+ ? ["recordTransferSkipExisting: Keeping existing rows isn't available for 'roles' — roller kendi taşıyıcılarıyla (Module.Role) aktarılır, satır parçasıyla değil; ayar orada uygulanamadığı için platform 400 döner. Ayarı kapalı bırakın."]
835
+ : []
836
+ },
825
837
  {
826
838
  id: 'MS-017',
827
839
  module: 'microservice',
@@ -0,0 +1,76 @@
1
+ import { isIP } from 'node:net';
2
+ import { domainToASCII } from 'node:url';
3
+ import { getDomain } from 'tldts-icann';
4
+ const wildcardPrefix = '*.';
5
+ const hostPattern = /^(?=.{4,253}$)([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]([a-z0-9-]{0,61}[a-z0-9])$/;
6
+ const pendingStates = new Set(['pending_dns', 'verifying']);
7
+ export function normalizeHost(host) {
8
+ const trimmed = host.trim().replace(/\.$/, '').toLowerCase();
9
+ const isWildcard = trimmed.startsWith(wildcardPrefix);
10
+ const name = isWildcard ? trimmed.slice(wildcardPrefix.length) : trimmed;
11
+ const ascii = domainToASCII(name) || name;
12
+ return isWildcard ? `${wildcardPrefix}${ascii}` : ascii;
13
+ }
14
+ export const isPattern = (host) => host.startsWith(wildcardPrefix);
15
+ // Studio'nun kuralıyla aynı (`apps/studio/src/features/custom-domain/custom-domain.functions.ts` `isApex`): kök public
16
+ // suffix listesinden gelir (kök `docs/adr/0027`), `firma.com.tr` apex'tir. Yalnız DNS yönergesinin kayıt tipini seçer, reddetmez.
17
+ export const isApex = (host) => {
18
+ const name = isPattern(host) ? host.slice(wildcardPrefix.length) : host;
19
+ return (getDomain(name) ?? name) === host;
20
+ };
21
+ /** Backend'in reddedeceği bir host için ne · neden · ne yapılacak; geçen host için null. */
22
+ export function hostProblem(host, kind, platformBaseDomains) {
23
+ const normalized = normalizeHost(host);
24
+ const name = isPattern(normalized) ? normalized.slice(wildcardPrefix.length) : normalized;
25
+ if (isPattern(normalized) && kind !== 'pattern') {
26
+ return `'${host}' bir dal kalıbı (joker '*.'): joker yalnız custom_domain_pattern_create ile açılır, bu araçta kullanılmaz.`;
27
+ }
28
+ if (!isPattern(normalized) && kind === 'pattern') {
29
+ return `'${host}' kalıp değil: dal kalıbı '*.' ile başlar (ör. *.ornek.com). Tek bir adres için custom_domain_create kullanın.`;
30
+ }
31
+ if (!name.includes('.') || isIP(name) !== 0 || platformBaseDomains.some(base => name === base || name.endsWith(`.${base}`))) {
32
+ return `'${host}': platform alan adı altına kayıt açılamaz — platform adresi (${platformBaseDomains.join(', ')}), noktasız ad ve IP özel alan adı olamaz. Müşterinin kendi alan adını verin; platform adresi uygulamayı zaten sunar.`;
33
+ }
34
+ if (!hostPattern.test(name)) {
35
+ return `'${host}' geçerli bir alan adı değil: harf/rakam/tire etiketleri, en az bir nokta, 4-253 karakter, son etiket harfle başlar.`;
36
+ }
37
+ return null;
38
+ }
39
+ /** Müşterinin DNS sağlayıcısına gireceği kayıtlar; hedef her zaman backend'in verdiği `cnameTarget`'tır. */
40
+ export function dnsRecords(domain) {
41
+ const records = [];
42
+ if (domain.cnameTarget) {
43
+ records.push({ type: isApex(domain.host) ? 'ALIAS / CNAME flattening' : 'CNAME', name: domain.host, value: domain.cnameTarget });
44
+ }
45
+ if (domain.ownershipTxtName && domain.ownershipTxtValue) {
46
+ records.push({ type: 'TXT', name: domain.ownershipTxtName, value: domain.ownershipTxtValue });
47
+ }
48
+ return records;
49
+ }
50
+ export const dnsTable = (records) => records.map(record => `${record.type} ${record.name} ${record.value}`).join('\n');
51
+ export const isPending = (state) => pendingStates.has(String(state));
52
+ // Yanıt bütçesi (41 §5): DNS yönergesi yalnız müşterinin hâlâ kayıt girmesi gereken satırda taşınır.
53
+ const needsRecords = (state) => isPending(state) || state === 'error';
54
+ export const withDns = (row) => needsRecords(row.state) ? { ...row, dns: dnsRecords(row) } : row;
55
+ /** Tek kayıt dönen yazma yanıtı: satır + DNS yönergesi (düz metin tablo da, ajan kullanıcıya iletebilsin) + sonraki adım. */
56
+ export function domainResponse(payload, note) {
57
+ const envelope = payload && typeof payload === 'object' ? payload : {};
58
+ const row = envelope.data;
59
+ if (!row || typeof row !== 'object')
60
+ return { ...envelope, note };
61
+ const records = dnsRecords(row);
62
+ return { ...envelope, data: { ...row, dns: records }, dnsTable: dnsTable(records), note };
63
+ }
64
+ export function listResponse(payload) {
65
+ const envelope = payload && typeof payload === 'object' ? payload : {};
66
+ const rows = Array.isArray(envelope.data) ? envelope.data : [];
67
+ return { ...envelope, data: rows.map(withDns), note: noteFor(rows) };
68
+ }
69
+ /** Listede ajanın bir sonraki adımı; bekleyen ve bağsız satır yoksa not yok. */
70
+ export function noteFor(rows) {
71
+ const notes = [
72
+ rows.some(row => isPending(row.state)) ? 'doğrulama sürüyor; 60 sn sonra custom_domain_list ile yeniden çağır, state active olunca adres yayında' : '',
73
+ rows.some(row => row.state === 'unbound') ? 'ortam silinmiş; custom_domain_bind ile bağla' : ''
74
+ ].filter(Boolean);
75
+ return notes.length > 0 ? notes.join(' · ') : undefined;
76
+ }
@@ -111,6 +111,30 @@ function templateSubscriptionHint(code) {
111
111
  return '';
112
112
  return ' Bu proje şablon güncellemelerini alıyor; dal açma, dal silme ve merge planının hiçbir adımı bu projede yapılamaz. Değişikliği şablonun kaynak projesinde yapın, template_create ile yeni sürüm yayımlayın, bu projede template_check_updates → template_receive_updates ile alın. Aynı çağrıyı tekrarlamak sonucu değiştirmez.';
113
113
  }
114
+ /**
115
+ * Özel alan adı retlerinin her biri başka bir araca ya da başka bir id'ye gönderir; kod olmadan
116
+ * "400" diye okunur ve ajan aynı host'u tekrar dener. Kodlar: backend `CustomDomainExceptions.cs`,
117
+ * durumlar `ExceptionStatusMap.cs` (HostTaken 409, Provider 502, kalanı 400).
118
+ */
119
+ function customDomainHint(code) {
120
+ if (code === 'CustomDomainHostTakenException')
121
+ return " Bu host başka bir kayıtta (bu ya da başka bir kuruluşta). custom_domain_list ile bakın: aynı projedeyse var olan kaydı custom_domain_bind ile istediğiniz ortama bağlayın, yeni kayıt açmayın.";
122
+ if (code === 'PlatformHostCantBeCustomDomainException')
123
+ return ' Platformun kendi alan adı ve altı, noktasız ad ve IP özel alan adı olamaz. Müşterinin kendi alan adını verin; platform adresi uygulamayı zaten sunar.';
124
+ if (code === 'InvalidCustomDomainHostException')
125
+ return " Host biçimi geçersiz: harf/rakam/tire etiketleri, en az bir nokta. Joker (*.) yalnız custom_domain_pattern_create'te; kalıp da yalnız jokerle açılır.";
126
+ if (code === 'CustomDomainBindingOutsideProjectException')
127
+ return " applicationId / environmentId / hedef bu projenin değil. application_list ve environment_list ile bu projenin id'lerini alın.";
128
+ if (code === 'CustomDomainRedirectTargetInvalidException')
129
+ return " Yönlendirme hedefi aynı projede, yayında (state active), kind primary bir kayıt olmalı. custom_domain_list'ten o satırın id'sini verin.";
130
+ if (code === 'InvalidCustomDomainStateException')
131
+ return " Kaydın durumu bu işlemi almıyor: removing kayıt bağlanmaz ve denetlenmez, unbound kayıt önce custom_domain_bind ister, kalıbın çocuğu ve yönlendirme bağlanmaz. custom_domain_list ile state ve kind'a bakın.";
132
+ if (code === 'InvalidCustomDomainBindingException')
133
+ return ' Bağlama türle uyuşmuyor: birincil uygulama + ortam ister, kalıp yalnız uygulama, yönlendirme yalnız hedef alan adı.';
134
+ if (code === 'CustomDomainProviderException')
135
+ return ' Sertifika sağlayıcısı isteği reddetti ya da ulaşılamadı; kayıt değişmedi. Bir dakika sonra custom_domain_verify ile yeniden deneyin; tekrarlarsa platform_gap_report.';
136
+ return '';
137
+ }
114
138
  /**
115
139
  * İçe aktarma zinciri dört çağrıdır ve her hata farklı bir adıma geri gönderir: plan süresi
116
140
  * dolduysa analizden, anahtar benzersiz değilse plan gövdesinden, hata oranı yüksekse dosyanın
@@ -401,7 +425,7 @@ export function mapPlatformError(status, payload, context) {
401
425
  if (validationMessages.length > 0) {
402
426
  return new PlatformApiError(`Doğrulama hatası (${location}): ${validationMessages.join(' ; ')}. Gövde şeması için ilgili ngapps://schemas kaynağına ya da mevcut bir kaydın *_get çıktısına bakın.`, status, code);
403
427
  }
404
- return new PlatformApiError(`Geçersiz istek (${location}): ${apiMessage ?? 'API gövdeyi reddetti.'}${mergeRevertHint(code)}${templateSubscriptionHint(code)}${importHint(code, context.path)}${accessHint(code, apiMessage)}`, status, code);
428
+ return new PlatformApiError(`Geçersiz istek (${location}): ${apiMessage ?? 'API gövdeyi reddetti.'}${mergeRevertHint(code)}${templateSubscriptionHint(code)}${importHint(code, context.path)}${accessHint(code, apiMessage)}${customDomainHint(code)}`, status, code);
405
429
  }
406
430
  if (status === 401) {
407
431
  // Uygulama oturumu 401'i gateway kimliğinin değil uygulama kullanıcısının sorunudur; USERNAME/PASSWORD
@@ -467,7 +491,7 @@ export function mapPlatformError(status, payload, context) {
467
491
  : '';
468
492
  const validationMessages = collectValidationMessages(body);
469
493
  const detail = validationMessages.length > 0 ? ` → ${validationMessages.join(' ; ')}` : '';
470
- const rejected = new PlatformApiError(`İş kuralı reddetti (${location}): ${apiMessage ?? 'çakışma'}${code ? ` [${code}]` : ''}${detail}${projectConflictHint}${pageSchemaHint(code, context.path)}${processHint(code, context.path)}${bulkRowHint(code)}${importHint(code, context.path)}${versionConflictHint(code, context.path)}${mergeHint(context.path)}${accessHint(code, apiMessage)}${microfunctionArchiveHint(code)}`, status, code);
494
+ const rejected = new PlatformApiError(`İş kuralı reddetti (${location}): ${apiMessage ?? 'çakışma'}${code ? ` [${code}]` : ''}${detail}${projectConflictHint}${pageSchemaHint(code, context.path)}${processHint(code, context.path)}${bulkRowHint(code)}${importHint(code, context.path)}${versionConflictHint(code, context.path)}${mergeHint(context.path)}${accessHint(code, apiMessage)}${microfunctionArchiveHint(code)}${customDomainHint(code)}`, status, code);
471
495
  if (typeof body.rowIndex === 'number')
472
496
  rejected.rowIndex = body.rowIndex;
473
497
  return rejected;
@@ -476,7 +500,7 @@ export function mapPlatformError(status, payload, context) {
476
500
  const fallbackDetail = fallbackValidationMessages.length > 0 ? ` → ${fallbackValidationMessages.join(' ; ')}` : '';
477
501
  const errorBody = status >= 500 ? describeErrorBody(body, { code, message: apiMessage }) : [];
478
502
  const errorBodyDetail = errorBody.length > 0 ? ` — ${errorBody.join(' · ')}` : '';
479
- return new PlatformApiError(`Platform hatası ${status} (${location}): ${apiMessage ?? 'beklenmeyen hata'}${code ? ` [${code}]` : ''}${errorBodyDetail}${fallbackDetail}${diagnoseKnownFailure(status, apiMessage ?? '', code, context)}`, status, code);
503
+ return new PlatformApiError(`Platform hatası ${status} (${location}): ${apiMessage ?? 'beklenmeyen hata'}${code ? ` [${code}]` : ''}${errorBodyDetail}${fallbackDetail}${diagnoseKnownFailure(status, apiMessage ?? '', code, context)}${customDomainHint(code)}`, status, code);
480
504
  }
481
505
  const CONNECTION_RESET_CODES = new Set(['ECONNRESET', 'UND_ERR_SOCKET', 'EPIPE', 'ECONNABORTED']);
482
506
  /**
@@ -0,0 +1,56 @@
1
+ # Özel Alan Adı Rehberi
2
+
3
+ Bir uygulamanın bir ortamını müşterinin kendi alan adında (`demo.ornek.com`) yayımlamak. Kayıt platformun
4
+ çekirdek tablosundadır; ortamın verisine dokunmaz. Studio'da proje › Ayarlar › **Alan adları** ekranının
5
+ yaptığı her şey `custom_domain_*` araçlarıyla yapılır.
6
+
7
+ ## Dört kural (değişmez)
8
+
9
+ 1. **Platform alan adı yasak.** Platformun kendi tabanı (kurulumda `zeyrek.app`, lokalde `zeyrek.local`) ve
10
+ altı, noktasız ad ve IP özel alan adı olamaz. Platform adresi uygulamayı zaten sunar; özel alan adı onun
11
+ **yanına** gelir, yerine geçmez.
12
+ 2. **Apex ALIAS ister.** Kökün kendisi (`ornek.com`, `firma.com.tr`; kök public suffix listesinden bulunur) çoğu DNS
13
+ sağlayıcısında CNAME alamaz: ALIAS /
14
+ ANAME / CNAME flattening kullanılır. Sağlayıcı bunu bilmiyorsa `www.ornek.com`'u birincil açıp `ornek.com`'u
15
+ ona yönlendirme (`custom_domain_redirect_create`) olarak açın.
16
+ 3. **Dal kalıbı kotadan düşer.** `*.onizleme.ornek.com` her açık dal için `<dal-slug>.onizleme.ornek.com`
17
+ çocuğunu kendisi açar ve dal silinince düşürür; her çocuk sağlayıcıda ayrı hostname'dir ve kurulumun
18
+ ücretsiz kotasından (100) düşer. 31 karakteri aşan ya da `www` / `zeyrek-check` olan dal slug'ı çocuk almaz.
19
+ 4. **Şablon abonesi serbest, alan adı şablon içeriği değil.** Şablondan kurulup güncelleme alan proje kendi
20
+ alan adını ekler, bağlar, kaldırır; şablon güncellemesi alan adını taşımaz ve bozmaz. Şablonun kaynak
21
+ projesindeki alan adı abonelere kopyalanmaz.
22
+
23
+ ## Sıra
24
+
25
+ 1. `application_list` + `environment_list` → uygulamanın ve ortamın id'si (production kabul edilir).
26
+ 2. `custom_domain_create { projectId, host, applicationId, environmentId }` → kayıt `pending_dns`; yanıt
27
+ `dns` satırlarını ve `dnsTable` düz metnini taşır. Bu kayıtları **müşteri** kendi DNS sağlayıcısına girer;
28
+ ajan DNS'e yazamaz.
29
+ 3. `custom_domain_list` ile 60 sn arayla okuyun: `pending_dns → verifying → active`. `active` olunca adres
30
+ yayındadır. Beklemeyi kısaltmak için `custom_domain_verify`.
31
+ 4. Başka ortama taşımak: `custom_domain_bind { id, applicationId, environmentId }` — host ve DNS değişmez.
32
+ 5. Kaldırmak: `custom_domain_delete` → `removing` → listeden çıkar. Müşterinin DNS kaydı kalır.
33
+
34
+ ## Durumlar
35
+
36
+ | state | Anlamı | Sonraki adım |
37
+ |---|---|---|
38
+ | `pending_dns` | DNS kaydı henüz platformu göstermiyor | Müşteri `dnsTable`'ı girer; bekle |
39
+ | `verifying` | DNS doğru, sertifika alınıyor | Bekle |
40
+ | `active` | Yayında | — |
41
+ | `error` | 72 saatte DNS gelmedi ya da sağlayıcı reddetti; `error` alanı sebebi söyler | Sebebi gider, `custom_domain_verify` (baştan başlar) |
42
+ | `unbound` | Bağlı ortam silindi ya da uygulama kaldırıldı; host ve DNS korunur | `custom_domain_bind` |
43
+ | `removing` | Sağlayıcıdan düşürülüyor | Bekle; bu kayıtta başka işlem yok |
44
+
45
+ ## Yönlendirme ve kalıp
46
+
47
+ - `custom_domain_redirect_create { host, targetDomainId }`: 301, yol ve sorgu korunur. Hedef aynı projede
48
+ `active` bir **birincil** kayıttır; hedef silinince yönlendirme de silinir.
49
+ - `custom_domain_pattern_create { host: '*.…', applicationId }`: dallar içindir; ortam seçilmez. Çocuklar
50
+ `custom_domain_list`'te `parentDomainId` ile görünür ve elle bağlanmaz.
51
+
52
+ ## Tuzaklar
53
+
54
+ - Özel alan adı kapalı kurulumda (`CustomDomains:Enabled=false`) uçlar 404 döner; araç değil kurulum ayarıdır.
55
+ - Bağlama ya da kaldırma adresin sunduğu ortamı en geç 30 sn içinde değiştirir (çözüm önbelleği).
56
+ - Aynı host ikinci kez açılamaz (409); aynı projedeyse var olan kaydı `custom_domain_bind` ile taşıyın.
@@ -57,11 +57,17 @@ Satırlar **tablo başına** açılan kayıt taşıma ayarıyla gider. Ayar enti
57
57
  satırları ana tabloyla **otomatik** gider; onlara ayrıca ayar açılmaz. Junction satırı yalnız iki
58
58
  tarafı da taşınıyorsa gider (dosya tarafı her zaman geçer).
59
59
  - **Kimlikler 1:1 korunur:** satır production'a aynı birincil anahtarla yazılır, identity sayacı
60
- taşınan id'lerin ötesine ilerletilir. Production'da aynı id'li satır varsa üzerine yazılır.
60
+ taşınan id'lerin ötesine ilerletilir. Production'da aynı id'li satır varsa üzerine yazılır (ayar kapalıyken).
61
61
  - **`'all'` = ayna (DEP-005, backend ADR 0011):** promote'ta production o tabloda development'ın
62
62
  aynasıdır. Kaynakta olmayan satır, çevirisi ve junction satırı production'da **silinir** —
63
63
  production'a doğrudan girilmiş satırlar dahil. Kaynak tablo boşsa hedef tablo boşalır. Platformun
64
64
  kendi tabloları (`users`, `roles`, `permissions`) taşımaya açılabilir ama aynalanmaz.
65
+ - **Hedefte varsa elleme (`recordTransferSkipExisting: true`, backend ADR 0072):** hedefte aynı birincil anahtarlı
66
+ ya da başka bir unique anahtarı çakışan satır **güncellenmez**, yalnız eksik satır eklenir; tablo **aynalanmaz**
67
+ (hedefin kendi eklediği satır silinmez). Şablon güncellemesinde de geçerlidir; dal açılışı ve merge yok sayar.
68
+ `roles`'ta açılamaz (ENT-019). Identity anahtarlı tabloya iki taraf da satır eklerse kimlikler çakışır ve kaynağın
69
+ satırı gelmez — iki tarafın satır eklediği tabloda `uuid` anahtar kullanın. Studio: tablo paneli → **"Hedefte zaten olan
70
+ kayıtları güncelleme"**.
65
71
  - **`'specific'` silmez:** yalnız upsert yapar. Seçili satırın FK ile gösterdiği satırlar zincirleme
66
72
  gelir (bir satır seçmek birkaçını taşıyabilir); seçilmemiş bir satıra giden çoka-çok bağı düşer.
67
73
  - **İlişki sırası:** `'all'` açılan tablonun ilişki hedefinde de taşıma açık olmalıdır, yoksa kayıt
@@ -122,4 +128,5 @@ yeni bir deploy.
122
128
  | Çeviriler eksik, satırlar var | ana tablonun ayarı | çeviri tablosu ana tabloyla gider; ana tabloda ayar açık olmalı |
123
129
  | Görsel production'da 404 | `page_schema_get` → prop düz URL mi, `{ type:'file', ref }` mi | prop'u `ref` ile bağlayın ya da dosyayı taşınan satırın dosya sütunundan okuyun |
124
130
  | Satırın dosyası 404, satır var | satır taşınıyor mu (`recordTransferMode`) | dosya yalnız taşınan satırla gider |
125
- | Production'da silinmiş satırlar | tablo `'all'` mı | ayna kuralı (DEP-005); production'a girilen veri bu tabloda tutulmaz |
131
+ | Production'da silinmiş satırlar | tablo `'all'` mı | ayna kuralı (DEP-005); production'a girilen veri bu tabloda tutulmaz — tutulsun isteniyorsa `recordTransferSkipExisting: true` |
132
+ | Production / abone değeri deploy ya da şablon güncellemesiyle ezildi | `entity_get` → `recordTransferSkipExisting` | `true` yapın: var olan satır güncellenmez |
@@ -31,7 +31,7 @@ tetikleyen satır `row` / `previousRow` / `user`.
31
31
 
32
32
  | Çağrı | Ne yapar | Gerçek örnek (qm) |
33
33
  |---|---|---|
34
- | `system.getPageUrl(path)` | Sayfanın environment'a göre doğru serve URL'ini üretir — e-posta/redirect linki için TEK doğru yol, sabit URL yazma | `system.getPageUrl(`/web/dif-kayit?id=${row.finding_record}`)` |
34
+ | `system.getPageUrl(path)` | Sayfanın environment'a göre doğru serve URL'ini üretir — e-posta/redirect linki için TEK doğru yol, sabit URL yazma. İstek uygulamanın özel alan adından geldiyse adres o alan adında döner (`https://demo.firma.com/dif-kayit?id=…`); istek yokken (zamanlanmış iş, kuyruk) sayfayı gösteren aktif alan adı varsa yine o, yoksa platform adresi | `system.getPageUrl(`/web/dif-kayit?id=${row.finding_record}`)` |
35
35
  | `system.generateToken({claims})` | Org secret'ıyla JWT üretir — sign-in ucunun kalbi | `{ access_token: system.generateToken({ userId: user.data.id, email: user.data.email }) }` |
36
36
  | `system.validateToken(token)` | Org secret'ıyla doğrular; dönüş `{ isValid, claims }` — **claim'ler bir seviye içeride** ve anahtarları `generateToken`'a verdiğin sözlüğün anahtarları (`r.claims.userId`); `r.id` YOK | `const r = system.validateToken(cookie.APP_Token)` → condition: `token.data.isValid && token.data.claims.userId` |
37
37
  | `file.readAsBase64(fileId)` | Drive dosyasını `data:<mime>;base64,…` döner | şablon okuma |
@@ -1,6 +1,6 @@
1
1
  {
2
- "hash": "162e8c2d14c5a0737d023b55790bab139812a00ec8e2e6335ed51981bb27e548",
3
- "generatedAt": "2026-11-02T08:52:07.624Z",
2
+ "hash": "0598b6dcd46ea1951cf067de1e0f3f38b12eaaddc2669205451097bb2a089f90",
3
+ "generatedAt": "2026-11-02T13:49:36.654Z",
4
4
  "types": 377,
5
5
  "files": 196,
6
6
  "note": "Şemaları üreten backend kaynaklarının parmak izi. Elle düzenlemeyin: 'npm run generate-schemas' yazar, 'npm run schema:check' denetler.",
@@ -65,6 +65,9 @@
65
65
  "type": "string"
66
66
  },
67
67
  "nullable": true
68
+ },
69
+ "recordTransferSkipExisting": {
70
+ "type": "boolean"
68
71
  }
69
72
  },
70
73
  "additionalProperties": false
@@ -726,6 +726,9 @@
726
726
  },
727
727
  "nullable": true
728
728
  },
729
+ "recordTransferSkipExisting": {
730
+ "type": "boolean"
731
+ },
729
732
  "automationCount": {
730
733
  "type": "integer",
731
734
  "format": "int32"
@@ -59,6 +59,9 @@
59
59
  "type": "string"
60
60
  },
61
61
  "nullable": true
62
+ },
63
+ "recordTransferSkipExisting": {
64
+ "type": "boolean"
62
65
  }
63
66
  },
64
67
  "additionalProperties": false
@@ -726,6 +726,9 @@
726
726
  },
727
727
  "nullable": true
728
728
  },
729
+ "recordTransferSkipExisting": {
730
+ "type": "boolean"
731
+ },
729
732
  "automationCount": {
730
733
  "type": "integer",
731
734
  "format": "int32"
package/dist/server.js CHANGED
@@ -36,6 +36,7 @@ import { registerFileTools } from "./tools/files.js";
36
36
  import { registerRowTools } from "./tools/rows.js";
37
37
  import { registerImportTools } from "./tools/imports.js";
38
38
  import { registerEnvironmentTools } from "./tools/environments.js";
39
+ import { registerCustomDomainTools } from "./tools/customDomains.js";
39
40
  import { registerMergeTools } from "./tools/merges.js";
40
41
  import { registerRevisionTools } from "./tools/revisions.js";
41
42
  import { registerActivityTools } from "./tools/activities.js";
@@ -95,6 +96,7 @@ const moduleRegistrations = {
95
96
  dependency: registerDependencyTools,
96
97
  deployment: registerDeploymentTools,
97
98
  environment: registerEnvironmentTools,
99
+ customDomain: registerCustomDomainTools,
98
100
  merge: registerMergeTools,
99
101
  activity: registerActivityTools,
100
102
  revision: registerRevisionTools,
@@ -0,0 +1,73 @@
1
+ import { destructive, guid, readOnly, updates, writes } from './common.js';
2
+ import { domainResponse, hostProblem, listResponse, normalizeHost } from '../knowledge/customDomain.js';
3
+ import { PlatformApiError } from '../platform/errors.js';
4
+ import { handle } from '../platform/responses.js';
5
+ import { z } from 'zod';
6
+ const host = z.string().min(1).max(253)
7
+ .describe("Müşterinin alan adı (ör. demo.ornek.com). Gateway kırpar, küçültür, IDN'yi punycode'a çevirir; platformun kendi alan adı ve altı reddedilir.");
8
+ const pendingNote = 'DNS kayıtları (dnsTable) müşterinin DNS sağlayıcısına girilir; doğrulama asenkron — custom_domain_list ile 60 sn arayla okuyun, state active olunca adres yayında.';
9
+ // Platform tabanı kuruluşa göre değişir; yalnız backend bilir (`CustomDomains:BaseDomains`), uç `mounts` onu yayımlar.
10
+ async function assertHost(platform, tool, value, kind) {
11
+ const mounts = await platform.org('GET', '/custom-domains/mounts');
12
+ const problem = hostProblem(value, kind, mounts?.platformBaseDomains ?? []);
13
+ if (problem)
14
+ throw new PlatformApiError(`${tool}: ${problem}`, 400);
15
+ return normalizeHost(value);
16
+ }
17
+ export function registerCustomDomainTools(server, platform) {
18
+ const config = platform.config;
19
+ server.registerTool('custom_domain_list', {
20
+ title: 'Özel alan adları',
21
+ description: "Projenin özel alan adlarını döner (birincil · yönlendirme · dal kalıbı ve kalıbın çocukları, parentDomainId ile). Her satırda state: pending_dns | verifying | active | error | unbound | removing; yalnız active yayındadır. DNS'i bekleyen ya da hatalı satır dns yönergesini taşır. Bekleyen satır varsa note '60 sn sonra yeniden çağır' der; unbound satır ortamı silinmiş ya da uygulaması kaldırılmış kayıttır, custom_domain_bind ile bağlanır. Özel alan adı kapalı kurulumda uç 404 döner.",
22
+ inputSchema: { projectId: guid },
23
+ annotations: readOnly
24
+ }, handle(config, async ({ projectId }) => listResponse(await platform.org('GET', `/custom-domains/${projectId}`))));
25
+ server.registerTool('custom_domain_create', {
26
+ title: 'Özel alan adı ekle',
27
+ description: "Bir uygulamanın bir ortamını müşterinin alan adında yayımlar (definition = CreateCustomDomainCommand: { host, applicationId, environmentId }). Kayıt pending_dns doğar; yanıt müşterinin gireceği DNS kayıtlarını dns satırlarında ve dnsTable düz metninde verir. production ortamı kabul edilir: kayıt CORE tablosuna yazılır, ortamın verisine dokunmaz. Joker (*.) host bu araçta reddedilir — dal kalıbı custom_domain_pattern_create'tir; tek adresi başka adrese yönlendirmek custom_domain_redirect_create'tir. Şablon abonesi proje de alan adını kendisi yönetir (alan adı şablon içeriği değildir).",
28
+ inputSchema: { projectId: guid, host, applicationId: guid, environmentId: guid },
29
+ annotations: writes
30
+ }, handle(config, async ({ projectId, host: value, applicationId, environmentId }) => {
31
+ const normalized = await assertHost(platform, 'custom_domain_create', value, 'primary');
32
+ return domainResponse(await platform.org('POST', `/custom-domains/${projectId}`, { body: { host: normalized, applicationId, environmentId } }), pendingNote);
33
+ }));
34
+ server.registerTool('custom_domain_pattern_create', {
35
+ title: 'Dal kalıbı ekle',
36
+ description: "Bir uygulamanın dallarını tek kalıpla yayımlar: '*.onizleme.ornek.com' her açık dal için '<dal-slug>.onizleme.ornek.com' çocuğunu kendiliğinden açar, dal silinince düşürür (definition = CreateCustomDomainPatternCommand: { host, applicationId }). Müşteri tek joker CNAME girer. Her çocuk sağlayıcıda ayrı hostname'dir ve kotadan düşer; 31 karakteri aşan ya da 'www'/'zeyrek-check' olan dal slug'ı çocuk almaz. Kalıp joker olmayan host'u reddeder.",
37
+ inputSchema: { projectId: guid, host: host.describe("Joker host: '*.' ile başlar (ör. *.onizleme.ornek.com)."), applicationId: guid },
38
+ annotations: writes
39
+ }, handle(config, async ({ projectId, host: value, applicationId }) => {
40
+ const normalized = await assertHost(platform, 'custom_domain_pattern_create', value, 'pattern');
41
+ return domainResponse(await platform.org('POST', `/custom-domains/${projectId}/pattern`, { body: { host: normalized, applicationId } }), pendingNote);
42
+ }));
43
+ server.registerTool('custom_domain_redirect_create', {
44
+ title: 'Yönlendirme ekle',
45
+ description: "Bir alan adını aynı projedeki yayında (active) birincil alan adına 301 ile yönlendirir (ör. www.ornek.com → ornek.com; definition = CreateCustomDomainRedirectCommand: { host, targetDomainId }). Yol ve sorgu korunur. Hedef silinince yönlendirme de silinir. Hedef id'si custom_domain_list'te kind: 'primary', state: 'active' satırıdır.",
46
+ inputSchema: { projectId: guid, host, targetDomainId: guid.describe('Yönlendirmenin hedefi: aynı projede active, kind primary bir kaydın id\'si') },
47
+ annotations: writes
48
+ }, handle(config, async ({ projectId, host: value, targetDomainId }) => {
49
+ const normalized = await assertHost(platform, 'custom_domain_redirect_create', value, 'redirect');
50
+ return domainResponse(await platform.org('POST', `/custom-domains/${projectId}/redirect`, { body: { host: normalized, targetDomainId } }), pendingNote);
51
+ }));
52
+ server.registerTool('custom_domain_verify', {
53
+ title: 'Alan adını şimdi denetle',
54
+ description: "DNS'i ve sertifikayı sıradaki tur beklenmeden şimdi denetler; error durumundaki kayıt baştan başlar. Arka plan zaten dakikada bir denetler, bu yalnız beklemeyi kısaltır. unbound ve removing kayıt denetlenmez (400).",
55
+ inputSchema: { id: guid.describe('Alan adı kaydının id\'si (custom_domain_list)') },
56
+ annotations: updates
57
+ }, handle(config, async ({ id }) => domainResponse(await platform.org('POST', `/custom-domains/${id}/verify`), pendingNote)));
58
+ server.registerTool('custom_domain_bind', {
59
+ title: 'Alan adını başka ortama bağla',
60
+ description: "Birincil alan adını aynı projede başka bir uygulama ortamına bağlar (definition = BindCustomDomainCommand: { applicationId, environmentId }); unbound kayıt böyle yeniden yayına girer. Host değişmez, DNS'e dokunulmaz. Yalnız kind primary; kalıbın çocukları ve yönlendirmeler bağlanmaz.",
61
+ inputSchema: { id: guid.describe('Alan adı kaydının id\'si (custom_domain_list)'), applicationId: guid, environmentId: guid },
62
+ annotations: updates
63
+ }, handle(config, async ({ id, applicationId, environmentId }) => domainResponse(await platform.org('PUT', `/custom-domains/${id}/environment`, { body: { applicationId, environmentId } }), 'Bağlama kalıcıdır; adres en geç 30 sn içinde yeni ortamı sunar.')));
64
+ server.registerTool('custom_domain_delete', {
65
+ title: 'Alan adını kaldır',
66
+ description: "Alan adını kaldırır: kayıt removing olur, sağlayıcıdaki hostname arka planda düşürülür ve kayıt listeden çıkar. Kalıp silinirse çocukları, birincil silinirse ona yönlendiren kayıtlar da silinir. Müşterinin DNS kayıtlarına dokunulmaz.",
67
+ inputSchema: { id: guid.describe('Alan adı kaydının id\'si (custom_domain_list)') },
68
+ annotations: destructive
69
+ }, handle(config, async ({ id }) => ({
70
+ ...await platform.org('DELETE', `/custom-domains/${id}`),
71
+ note: 'kayıt removing; custom_domain_list ile listeden çıkmasını bekleyin'
72
+ })));
73
+ }
@@ -27,7 +27,7 @@ export function registerDeploymentTools(server, platform) {
27
27
  const config = platform.config;
28
28
  server.registerTool('deployment_start', {
29
29
  title: 'Deployment başlat',
30
- description: 'Projeyi kaynak environment\'tan hedef environment\'a deploy eder. Asenkron bir saga\'dır: sonuç bu yanıtta bitmez, deployment_status ile izleyin. Environment id\'lerini project_get çıktısından alın. BPM: süreç tanımları ve iş takvimleri taşınır — hedefte farklı aktif model yeni sürüm olarak yayınlanır (production\'da gelişte aktif), hedefin uyarlaması korunur; uyarlama, örnek, görev ve defter taşınmaz; kaynakta olmayan tanım silinir, açık örneği varsa emekliye ayrılır. KAYNAK YALNIZ DEVELOPMENT\'TIR: dal ya da production kaynak verilirse 400 (CantDeployFromBranchException) — dalı önce development\'a merge edin. ÖN DENETİM: deploy başlamadan production verisi denetlenir; veriyi bozacak ya da silecek her değişiklik 400 (DeploymentDataRiskException) ile listelenir ve deploy BAŞLAMAZ — biçim \'tablo.alan: tür (satır)\', türler not-null | unique | field-removed | table-removed | localization-move. YALNIZ TANIM TAŞINIR: tablo satırları ancak tabloda recordTransferMode (\'all\' | \'specific\' + transferRowIds) açıksa gider, çeviri ve junction satırları ana tabloyla birlikte. Kayıt taşıması \'all\' olan tabloda production development\'ın AYNASIDIR (kaynakta olmayan satır ve çevirisi silinir). DOSYA yalnız id referansıyla gider (file tipli prop { type:\'file\', ref:<fileId> } ya da taşınan satırın dosya sütunu); sayfaya düz yazılan /d/ serve URL\'si /p/\'ye çevrilir ama dosya kopyalanmaz, production\'da 404 döner — platform_guide(\'deployment\'). Microfunction\'ın yalnız YAYINLANMIŞ kodu taşınır ve hedefte yayınlanır; hiç yayınlanmamış olan hedefte taslak kalır. Zamanlanmış işin duraklatma durumu taşınır. YAYINLANMAMIŞ TASLAK TAŞINMAZ: kaynakta taslağı olan süreçler yanıtta unpublishedDrafts ({ slug, name, draftVersionNumber, activeVersionNumber }) ve note ile listelenir; hedefte aktif sürüm kalır. Taşımak için taslağı kaynakta yayınlayın (requireGreenScenarios açıksa önce senaryoları taslak üzerinde koşun), sonra yeniden deploy edin. DELTA (varsayılan): yalnız içerik özeti production\'dakinden farklı tanımlar taşınır, kaynakta olmayan tanım silinir; entity, rol, erişim politikası, bileşen, dosya, süreç tanımı, organizasyon yapısı ve tablo satırları her deploy\'da tam taşınır; klasör silme taşınmaz. Kaynağın ya da production\'ın özeti güvenilir değilse (son tamamlanmış özet denetimi güncel biçimde bir fill ya da farksız bir check değilse) deploy kendiliğinden tam koşar; fullMode:true her tanımı taşır. Hangi kipte koştuğu ve modül başına sayılar deployment_status yanıtında (mode, counters).',
30
+ description: 'Projeyi kaynak environment\'tan hedef environment\'a deploy eder. Asenkron bir saga\'dır: sonuç bu yanıtta bitmez, deployment_status ile izleyin. Environment id\'lerini project_get çıktısından alın. BPM: süreç tanımları ve iş takvimleri taşınır — hedefte farklı aktif model yeni sürüm olarak yayınlanır (production\'da gelişte aktif), hedefin uyarlaması korunur; uyarlama, örnek, görev ve defter taşınmaz; kaynakta olmayan tanım silinir, açık örneği varsa emekliye ayrılır. KAYNAK YALNIZ DEVELOPMENT\'TIR: dal ya da production kaynak verilirse 400 (CantDeployFromBranchException) — dalı önce development\'a merge edin. ÖN DENETİM: deploy başlamadan production verisi denetlenir; veriyi bozacak ya da silecek her değişiklik 400 (DeploymentDataRiskException) ile listelenir ve deploy BAŞLAMAZ — biçim \'tablo.alan: tür (satır)\', türler not-null | unique | field-removed | table-removed | localization-move. YALNIZ TANIM TAŞINIR: tablo satırları ancak tabloda recordTransferMode (\'all\' | \'specific\' + transferRowIds) açıksa gider, çeviri ve junction satırları ana tabloyla birlikte. Kayıt taşıması \'all\' olan tabloda production development\'ın AYNASIDIR (kaynakta olmayan satır ve çevirisi silinir) — tabloda recordTransferSkipExisting açıksa ayna yoktur, production\'da var olan satır güncellenmez, yalnız eksik satır eklenir. DOSYA yalnız id referansıyla gider (file tipli prop { type:\'file\', ref:<fileId> } ya da taşınan satırın dosya sütunu); sayfaya düz yazılan /d/ serve URL\'si /p/\'ye çevrilir ama dosya kopyalanmaz, production\'da 404 döner — platform_guide(\'deployment\'). Microfunction\'ın yalnız YAYINLANMIŞ kodu taşınır ve hedefte yayınlanır; hiç yayınlanmamış olan hedefte taslak kalır. Zamanlanmış işin duraklatma durumu taşınır. YAYINLANMAMIŞ TASLAK TAŞINMAZ: kaynakta taslağı olan süreçler yanıtta unpublishedDrafts ({ slug, name, draftVersionNumber, activeVersionNumber }) ve note ile listelenir; hedefte aktif sürüm kalır. Taşımak için taslağı kaynakta yayınlayın (requireGreenScenarios açıksa önce senaryoları taslak üzerinde koşun), sonra yeniden deploy edin. DELTA (varsayılan): yalnız içerik özeti production\'dakinden farklı tanımlar taşınır, kaynakta olmayan tanım silinir; entity, rol, erişim politikası, bileşen, dosya, süreç tanımı, organizasyon yapısı ve tablo satırları her deploy\'da tam taşınır; klasör silme taşınmaz. Kaynağın ya da production\'ın özeti güvenilir değilse (son tamamlanmış özet denetimi güncel biçimde bir fill ya da farksız bir check değilse) deploy kendiliğinden tam koşar; fullMode:true her tanımı taşır. Hangi kipte koştuğu ve modül başına sayılar deployment_status yanıtında (mode, counters).',
31
31
  inputSchema: {
32
32
  projectId: guid,
33
33
  sourceEnvironmentId: guid,
@@ -81,6 +81,7 @@ const guideRouting = {
81
81
  translation: 'sayfa/içerik çevirileri (çoklu dil)',
82
82
  access: 'kompleks proje yetkisi: organizasyon ağacı, izin kataloğu, kapsam, grant/atama, alan koruması (maske · şifre · türetilmiş alan)',
83
83
  merge: "dal aç, dalda çalış, development'a merge et; çakışma çözümü ve apply kapıları",
84
+ 'custom-domain': 'uygulamayı müşterinin alan adında yayımlama: DNS yönergesi, durumlar, yönlendirme, dal kalıbı, dört kural',
84
85
  deployment: "development → production deploy'u neyi taşır: yalnız tanım; tablo satırları recordTransferMode ile, dosyalar yalnız id referansıyla; production'da eksik satır/dosya teşhisi",
85
86
  bpm: 'süreç motoru: tanım modeli, düğümler, atama sözlüğü, bölge olayları, kutu ve görev eylemleri, bağlayıcı/belge/imza/kanallar (süreç ≠ workflow)',
86
87
  decision: 'DMN karar tabloları: model, isabet politikaları, FEEL alt kümesi, DRD, süreçte decision düğümü, DMN XML'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appaflytech/zeyrek-ai-mcp",
3
- "version": "0.0.55",
3
+ "version": "0.0.57",
4
4
  "type": "module",
5
5
  "description": "Zeyrek (NgApps) platform APIs as MCP tools — data, logic and Studio design surface in one gateway",
6
6
  "license": "UNLICENSED",
@@ -50,6 +50,7 @@
50
50
  "fast-json-patch": "^3.1.1",
51
51
  "pino": "^9.5.0",
52
52
  "playwright-core": "^1.49.0",
53
+ "tldts-icann": "^7.4.18",
53
54
  "undici": "^7.3.0",
54
55
  "zod": "^3.24.1"
55
56
  },