@appaflytech/zeyrek-ai-mcp 0.0.51 → 0.0.53

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.
Files changed (58) hide show
  1. package/dist/knowledge/componentCatalog.js +3 -1
  2. package/dist/knowledge/componentEvents.js +13 -1
  3. package/dist/knowledge/expressionPrelude.js +3 -0
  4. package/dist/knowledge/mobileComponentCatalog.js +4 -1
  5. package/dist/knowledge/mobileStore.js +258 -1
  6. package/dist/knowledge/normalize.js +4 -3
  7. package/dist/knowledge/platform/mobile.js +1 -1
  8. package/dist/knowledge/platform/mobileRules.js +79 -0
  9. package/dist/knowledge/rules.js +199 -6
  10. package/dist/knowledge/storeImport.js +69 -0
  11. package/dist/platform/argumentAliases.js +13 -4
  12. package/dist/platform/bodyFile.js +195 -38
  13. package/dist/platform/capture/actions.js +24 -5
  14. package/dist/platform/capture/capture.js +80 -11
  15. package/dist/platform/capture/persistent-browser.js +23 -1
  16. package/dist/platform/capture/readiness.js +32 -1
  17. package/dist/platform/errors.js +45 -5
  18. package/dist/platform/http.js +17 -9
  19. package/dist/platform/responses.js +7 -1
  20. package/dist/platform/secrets.js +8 -0
  21. package/dist/platform/token.js +32 -21
  22. package/dist/resources/examples/flow-credit-spend.json +233 -0
  23. package/dist/resources/examples/flow-purchase-paywall.json +662 -0
  24. package/dist/resources/examples/mobile-offline-list.json +1 -1
  25. package/dist/resources/examples/mobile-offline-queue.json +1 -1
  26. package/dist/resources/examples/mobile-offline-seamless.json +12 -3
  27. package/dist/resources/examples/store-import-mapping.json +25 -0
  28. package/dist/resources/guides/deployment.md +9 -0
  29. package/dist/resources/guides/expressions.md +1 -0
  30. package/dist/resources/guides/microservice.md +13 -1
  31. package/dist/resources/guides/mobile-publish.md +67 -2
  32. package/dist/resources/guides/mobile-steps.md +91 -3
  33. package/dist/resources/guides/mobile.md +38 -8
  34. package/dist/resources/guides/page.md +39 -8
  35. package/dist/resources/guides/translation.md +3 -1
  36. package/dist/resources/guides/workflow.md +30 -0
  37. package/dist/resources/schema-fingerprint.json +2 -2
  38. package/dist/resources/schemas/CreateWorkflowCommand.json +6 -1
  39. package/dist/resources/schemas/GlobalFunctionUpdateResultDto.json +4 -0
  40. package/dist/resources/schemas/PreviewWorkflowCommand.json +6 -1
  41. package/dist/resources/schemas/StepDto.json +6 -1
  42. package/dist/resources/schemas/UpdateWorkflowCommand.json +6 -1
  43. package/dist/server.js +4 -0
  44. package/dist/tools/applications.js +7 -2
  45. package/dist/tools/deployments.js +1 -1
  46. package/dist/tools/designSchemaPatch.js +7 -2
  47. package/dist/tools/environments.js +40 -0
  48. package/dist/tools/flows.js +102 -9
  49. package/dist/tools/mobile.js +56 -0
  50. package/dist/tools/mobilePurchases.js +236 -0
  51. package/dist/tools/mobileStore.js +2 -2
  52. package/dist/tools/mobileStoreImport.js +173 -0
  53. package/dist/tools/pages.js +45 -11
  54. package/dist/tools/platformInfo.js +4 -4
  55. package/dist/tools/screenWrite.js +107 -1
  56. package/dist/tools/variables.js +1 -1
  57. package/package.json +1 -1
  58. package/dist/resources/guides/mobile-flow.md +0 -64
@@ -1,6 +1,6 @@
1
- import { open, realpath } from 'node:fs/promises';
1
+ import { open, realpath, stat } from 'node:fs/promises';
2
2
  import { constants } from 'node:fs';
3
- import { extname, resolve, sep } from 'node:path';
3
+ import { dirname, extname, isAbsolute, resolve, sep } from 'node:path';
4
4
  import { z } from 'zod';
5
5
  import { PlatformApiError } from './errors.js';
6
6
  /**
@@ -27,77 +27,234 @@ export const BODY_FILE_ARGUMENTS = {
27
27
  /** `ops`/`operations` eşanlamlıdır (`microOps`); ikisinden biri satır içinde varsa gövde verilmiştir. */
28
28
  const synonyms = { ops: ['ops', 'operations'], operations: ['ops', 'operations'] };
29
29
  const maxBodyFileBytes = 5 * 1024 * 1024;
30
+ /**
31
+ * g-22 (D-22, K8 — `plans/defter-saha-bulgulari`): akış kodu JSON metninin içinde kaçışlı dizgi olarak duruyordu;
32
+ * Defter'de bir ters tırnak hatası düzenleri kırdı ve iş JS üreteçlerine taşındı. `{ "$file": "./adim.js" }` o
33
+ * dosyanın METNİYLE değiştirilir — gateway kodu ÇALIŞTIRMAZ (reddedilen seçenek: gateway'de .js koşturmak). Eklenen
34
+ * dosya bodyFile'ın bütün denetimlerinden geçer; sınırlar sahibin onayladığı K8 değerleridir.
35
+ */
36
+ const includedExtensions = ['.js', '.mjs', '.ts', '.txt', '.sql', '.html', '.css'];
37
+ const maxIncludedFileBytes = 1024 * 1024;
38
+ const maxInclusions = 64;
30
39
  export function bodyFileParam(argument) {
31
- return z.string().min(1).optional().describe(`'${argument}' yerine okunacak yerel .json dosyası (en çok 5 MB): gövde ajan bağlamından geçmez, gateway dosyayı okuyup '${argument}' olarak aynı doğrulamadan geçirir. '${argument}' ile birlikte verilemez. Yol gateway sürecinin çalışma dizininin ya da MCP_INPUT_DIR'in altında olmalı (sembolik bağ çözülür). Yanıtın bodyFile alanı hangi dosyanın okunduğunu söyler.`);
40
+ return z.string().min(1).optional().describe(`'${argument}' yerine okunacak yerel .json dosyası (en çok 5 MB): gövde ajan bağlamından geçmez, gateway dosyayı okuyup '${argument}' olarak aynı doğrulamadan geçirir. '${argument}' ile birlikte verilemez. Göreli yol .env'deki MCP_INPUT_DIR'e (tanımsızsa gateway'in çalışma dizinine) göre çözülür; dosya bu iki dizinden birinin altında olmalı (sembolik bağ çözülür). JSON'un herhangi bir yerine { "$file": "./adim.js" } yazılırsa o dosyanın METNİ oraya konur — kod çalıştırılmaz; yol bodyFile'ın klasörüne göre çözülür; ${includedExtensions.join(' ')}, UTF-8, dosya başına 1 MB, toplam 5 MB, en çok ${maxInclusions} ekleme; eklenen dosyanın içindeki $file işlenmez. Biçim tam { "$file": "<yol>" }'dur (tek anahtar, dizgi) — bozuk biçim ve satır içi gövdedeki $file istek gitmeden reddedilir. Akış code adımının args.value'su böylece kaçışsız düz JS dosyasında durur. Yanıtın bodyFile alanı okunan dosyanın mutlak yolunu, kökünü (root, rootSource) ve eklenen dosyaları (included: at, path, bytes) söyler.`);
32
41
  }
33
42
  export async function loadBodyFile(tool, args, config) {
34
43
  const argument = BODY_FILE_ARGUMENTS[tool];
35
44
  if (!argument || !args || typeof args !== 'object' || Array.isArray(args))
36
45
  return { args };
37
46
  const { bodyFile, ...rest } = args;
38
- if (bodyFile === undefined)
39
- return { args };
40
- if (typeof bodyFile !== 'string' || bodyFile.length === 0)
41
- return { args };
42
47
  const inline = (synonyms[argument] ?? [argument]).filter(name => rest[name] !== undefined);
48
+ if (typeof bodyFile !== 'string' || bodyFile.length === 0) {
49
+ assertNoInlineInclusions(tool, inline, rest);
50
+ return { args };
51
+ }
43
52
  if (inline.length > 0) {
44
53
  throw new PlatformApiError(`${tool}: bodyFile ile '${inline.join("', '")}' birlikte verilemez — gövde ya dosyadan ya satır içinden gelir. Birini kaldırın. İstek gönderilmedi.`, 400);
45
54
  }
46
- const absolute = resolve(bodyFile);
47
- if (extname(absolute).toLowerCase() !== '.json') {
48
- throw new PlatformApiError(`${tool}: bodyFile yalnız .json okur — verilen: ${absolute}. İstek gönderilmedi.`, 400);
55
+ // g-21 (D-21): göreli yol gateway sürecinin çalışma dizinine göre çözülüyordu ve ajan o dizini bilmez. Ajanın
56
+ // bildiği kök .env'deki MCP_INPUT_DIR'dir; tanımsızsa eski davranış (çalışma dizini) sürer.
57
+ const base = config.inputDirectory ? { source: 'MCP_INPUT_DIR', path: resolve(config.inputDirectory) } : { source: 'cwd', path: process.cwd() };
58
+ const roots = await allowedRoots(config);
59
+ const body = await readGuardedFile(tool, 'bodyFile', resolve(base.path, bodyFile), ['.json'], maxBodyFileBytes, roots, config);
60
+ let value;
61
+ try {
62
+ value = JSON.parse(body.buffer.toString('utf8'));
63
+ }
64
+ catch (error) {
65
+ throw new PlatformApiError(`${tool}: bodyFile JSON değil — ${body.real} (${error instanceof Error ? error.message : String(error)}). İstek gönderilmedi.`, 400);
66
+ }
67
+ const included = await includeFiles(tool, value, dirname(body.real), body.buffer.length, roots, config);
68
+ const root = isAbsolute(bodyFile) ? body.root : { source: base.source, path: await realpath(base.path).catch(() => base.path) };
69
+ return {
70
+ args: { ...rest, [argument]: included.value },
71
+ note: {
72
+ path: body.real,
73
+ root: root.path,
74
+ rootSource: root.source,
75
+ argument,
76
+ bytes: body.buffer.length,
77
+ ...(included.files.length > 0 ? { included: included.files } : {})
78
+ }
79
+ };
80
+ }
81
+ /** MCP_INPUT_DIR önce gelir: dosya iki kökün de altındaysa yanıt ajanın bildiği kökü söyler. */
82
+ async function allowedRoots(config) {
83
+ const candidates = [
84
+ ...(config.inputDirectory ? [{ source: 'MCP_INPUT_DIR', path: config.inputDirectory }] : []),
85
+ { source: 'cwd', path: process.cwd() }
86
+ ];
87
+ const resolved = await Promise.all(candidates.map(async (root) => ({ source: root.source, path: await realpath(resolve(root.path)).catch(() => undefined) })));
88
+ return resolved.filter((root) => root.path !== undefined);
89
+ }
90
+ /**
91
+ * bodyFile'ın ve her `$file` eklemesinin ORTAK okuma kapısı: uzantı (yol ve bağın hedefi), izinli kök, düzenli dosya
92
+ * ve bayt sınırı. O_NONBLOCK FIFO'nun çağrıyı bekletmesini önler; boyut dosyanın büyüklüğüne değil okunan bayta bakar.
93
+ *
94
+ * `realpath` ile `open` atomik DEĞİLDİR: O_NOFOLLOW yalnız SON bileşenin sonradan bağa çevrilmesini önler, yolun
95
+ * ortasındaki bir klasör denetimden sonra dışarıyı gösteren bağa çevrilirse `open` dışarıdaki dosyayı açar (g-22
96
+ * incelemesi, `test/body-file-race.test.ts`). Açtıktan sonra tutamacın aygıt/düğümü yolun taze `stat`'ıyla
97
+ * karşılaştırılır ve `realpath` yeniden koşturulur: yol aynı yere çözülmüyor ya da izinli kökün dışına düşüyorsa okuma
98
+ * reddedilir. Bu pencereyi DARALTIR, kapatmaz — iki denetim arasında geri çevrilen bir bağ yine görülmez; tam çözüm
99
+ * (`openat2` RESOLVE_BENEATH) Node'da yoktur. Okunan bayt yine de açılan tutamaçtan gelir.
100
+ */
101
+ async function readGuardedFile(tool, label, absolute, extensions, maxBytes, roots, config) {
102
+ const accepted = extensions.join(', ');
103
+ if (!extensions.includes(extname(absolute).toLowerCase())) {
104
+ throw new PlatformApiError(`${tool}: ${label} yalnız ${accepted} okur — verilen: ${absolute}. İstek gönderilmedi.`, 400);
49
105
  }
50
106
  let real;
51
107
  try {
52
108
  real = await realpath(absolute);
53
109
  }
54
110
  catch (error) {
55
- throw new PlatformApiError(`${tool}: bodyFile okunamadı — ${absolute} (${error instanceof Error ? error.message : String(error)}). Yol gateway sürecinin çalıştığı makineye göre çözülür.`, 400);
111
+ throw new PlatformApiError(`${tool}: ${label} okunamadı — ${absolute} (${error instanceof Error ? error.message : String(error)}). Yol gateway sürecinin çalıştığı makineye göre çözülür. İstek gönderilmedi.`, 400);
56
112
  }
57
- if (extname(real).toLowerCase() !== '.json') {
58
- throw new PlatformApiError(`${tool}: bodyFile'ın hedefi .json değil — ${real} (${absolute} bağının hedefi). İstek gönderilmedi.`, 400);
113
+ if (!extensions.includes(extname(real).toLowerCase())) {
114
+ throw new PlatformApiError(`${tool}: ${label} hedefi ${accepted} değil — ${real} (${absolute} bağının hedefi). İstek gönderilmedi.`, 400);
59
115
  }
60
- const roots = await Promise.all([process.cwd(), config.inputDirectory]
61
- .filter((root) => typeof root === 'string' && root.length > 0)
62
- .map(root => realpath(resolve(root)).catch(() => undefined)));
63
- const allowed = roots.some(root => root !== undefined && (real === root || real.startsWith(`${root}${sep}`)));
64
- if (!allowed) {
65
- throw new PlatformApiError(`${tool}: bodyFile izinli dizinlerin dışında — ${real}${real !== absolute ? ` (${absolute} bağının hedefi)` : ''}. Gateway yalnız çalışma dizininin (${process.cwd()}) ve .env'deki MCP_INPUT_DIR'in (${config.inputDirectory ?? 'tanımsız'}) altındaki dosyaları okur. İstek gönderilmedi.`, 400);
116
+ const root = roots.find(candidate => real === candidate.path || real.startsWith(`${candidate.path}${sep}`));
117
+ if (!root) {
118
+ throw new PlatformApiError(`${tool}: ${label} izinli dizinlerin dışında — ${real}${real !== absolute ? ` (${absolute} bağının hedefi)` : ''}. Gateway yalnız çalışma dizininin (${process.cwd()}) ve .env'deki MCP_INPUT_DIR'in (${config.inputDirectory ?? 'tanımsız'}) altındaki dosyaları okur. İstek gönderilmedi.`, 400);
66
119
  }
67
- // Denetim ve okuma AYNI tutamaçtan: yol denetimden sonra değişse de okunan, denetlenen dosyadır.
68
- // O_NOFOLLOW son bileşenin sonradan bağa çevrilmesini, O_NONBLOCK `.json` adlı FIFO'nun çağrıyı
69
- // bekletmesini önler; boyut dosyanın büyüklüğüne değil okunan bayta bakar.
70
120
  let handle;
71
121
  try {
72
122
  handle = await open(real, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
73
123
  }
74
124
  catch (error) {
75
- throw new PlatformApiError(`${tool}: bodyFile açılamadı — ${real} (${error instanceof Error ? error.message : String(error)}). İstek gönderilmedi.`, 400);
125
+ throw new PlatformApiError(`${tool}: ${label} açılamadı — ${real} (${error instanceof Error ? error.message : String(error)}). İstek gönderilmedi.`, 400);
76
126
  }
77
- let bytes;
78
- let text;
79
127
  try {
80
128
  const info = await handle.stat();
81
129
  if (!info.isFile()) {
82
- throw new PlatformApiError(`${tool}: bodyFile düzenli dosya değil — ${real}. İstek gönderilmedi.`, 400);
130
+ throw new PlatformApiError(`${tool}: ${label} düzenli dosya değil — ${real}. İstek gönderilmedi.`, 400);
131
+ }
132
+ const fresh = await stat(real).catch(() => undefined);
133
+ const again = await realpath(absolute).catch(() => undefined);
134
+ const stillInside = again !== undefined && (again === root.path || again.startsWith(`${root.path}${sep}`));
135
+ if (!fresh || fresh.dev !== info.dev || fresh.ino !== info.ino || again !== real || !stillInside) {
136
+ throw new PlatformApiError(`${tool}: ${label} yolu denetimden sonra değişti — ${real} artık ${again ?? 'çözülemeyen bir yere'} gösteriyor (araya bir bağ girdi). Okunmadı. İstek gönderilmedi.`, 400);
83
137
  }
84
- const buffer = Buffer.alloc(maxBodyFileBytes + 1);
85
- const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
86
- bytes = bytesRead;
87
- if (bytes > maxBodyFileBytes) {
88
- throw new PlatformApiError(`${tool}: bodyFile 5 MB sınırını aşıyor. Tanımı parçalara bölün — sayfa için page_schema_patch'e birden çok batch, akış için flow_patch. İstek gönderilmedi.`, 400);
138
+ const buffer = Buffer.alloc(maxBytes + 1);
139
+ let bytesRead = 0;
140
+ // Tek `read` dosyanın tamamını döndürmek zorunda değildir; EOF'a ya da sınırın bir baytı ötesine kadar okunur.
141
+ while (bytesRead < buffer.length) {
142
+ const chunk = await handle.read(buffer, bytesRead, buffer.length - bytesRead, bytesRead);
143
+ if (chunk.bytesRead === 0)
144
+ break;
145
+ bytesRead += chunk.bytesRead;
89
146
  }
90
- text = buffer.subarray(0, bytes).toString('utf8');
147
+ if (bytesRead > maxBytes) {
148
+ throw new PlatformApiError(maxBytes === maxBodyFileBytes
149
+ ? `${tool}: ${label} 5 MB sınırını aşıyor. Tanımı parçalara bölün — sayfa için page_schema_patch'e birden çok batch, akış için flow_patch. İstek gönderilmedi.`
150
+ : `${tool}: ${label} ${maxBytes / (1024 * 1024)} MB sınırını aşıyor — ${real}. Kodu birden çok dosyaya bölün. İstek gönderilmedi.`, 400);
151
+ }
152
+ return { real, root, buffer: buffer.subarray(0, bytesRead) };
91
153
  }
92
154
  finally {
93
155
  await handle.close();
94
156
  }
95
- let value;
96
- try {
97
- value = JSON.parse(text);
157
+ }
158
+ /** Kök JSON Pointer'ı boş dizgidir; mesajda `''` ile `/` (adı boş anahtar) karışmasın diye adıyla yazılır. */
159
+ function pointerLabel(at) {
160
+ return at === '' ? '(kök)' : at;
161
+ }
162
+ function pointerToken(key) {
163
+ return String(key).replace(/~/g, '~0').replace(/\//g, '~1');
164
+ }
165
+ /**
166
+ * Anahtarlarından biri büyük/küçük harf duyarsız `$file` olan her nesne ekleme NİYETİDİR. Yalnız tam
167
+ * `{ "$file": "<yol>" }` biçimi eklemedir; dizi/sayı değer, ek anahtar ya da `$File` yazımı sessizce nesne olarak
168
+ * platforma gitmesin diye `malformed`'a düşer (g-22 incelemesi 2).
169
+ */
170
+ function findInclusions(value, at, found, malformed) {
171
+ if (Array.isArray(value)) {
172
+ value.forEach((item, index) => findInclusions(item, `${at}/${index}`, found, malformed));
173
+ return;
98
174
  }
99
- catch (error) {
100
- throw new PlatformApiError(`${tool}: bodyFile JSON değil — ${real} (${error instanceof Error ? error.message : String(error)}). İstek gönderilmedi.`, 400);
175
+ if (!value || typeof value !== 'object')
176
+ return;
177
+ const record = value;
178
+ const keys = Object.keys(record);
179
+ const marker = keys.find(key => key.toLowerCase() === '$file');
180
+ if (marker !== undefined) {
181
+ if (keys.length === 1 && marker === '$file' && typeof record.$file === 'string') {
182
+ found.push({ at, file: record.$file });
183
+ }
184
+ else {
185
+ const problem = marker !== '$file'
186
+ ? `anahtar '${marker}' yazılmış, '$file' olmalı`
187
+ : keys.length > 1
188
+ ? `'$file' yanında başka anahtar var (${keys.filter(key => key !== '$file').join(', ')})`
189
+ : `'$file' değeri dizgi değil (${JSON.stringify(record[marker])})`;
190
+ malformed.push(`${pointerLabel(at)}: ${problem}`);
191
+ }
192
+ return;
193
+ }
194
+ for (const [key, item] of Object.entries(record))
195
+ findInclusions(item, `${at}/${pointerToken(key)}`, found, malformed);
196
+ }
197
+ /**
198
+ * Satır içi gövdede `$file` açılmaz: dosya okuma yalnız bodyFile yolunda vardır. Açılmayan nesne platforma nesne
199
+ * olarak giderdi; istek gitmeden söylenir. `$file` anahtarı yoksa yalnız bir ağaç yürüyüşüdür, okuma yapılmaz.
200
+ */
201
+ function assertNoInlineInclusions(tool, names, args) {
202
+ for (const name of names) {
203
+ const found = [];
204
+ const malformed = [];
205
+ findInclusions(args[name], '', found, malformed);
206
+ const sites = [...found.map(site => pointerLabel(site.at)), ...malformed];
207
+ if (sites.length > 0) {
208
+ throw new PlatformApiError(`${tool}: '${name}' içinde $file nesnesi var (${sites.join(' · ')}) — $file yalnız bodyFile gövdesinde açılır. Metni satır içinde doğrudan yazın ya da gövdeyi bodyFile ile (.json dosyası) verin. İstek gönderilmedi.`, 400);
209
+ }
210
+ }
211
+ }
212
+ function replaceInclusions(value, at, texts) {
213
+ const text = texts.get(at);
214
+ if (text !== undefined)
215
+ return text;
216
+ if (Array.isArray(value))
217
+ return value.map((item, index) => replaceInclusions(item, `${at}/${index}`, texts));
218
+ if (value && typeof value === 'object') {
219
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, replaceInclusions(item, `${at}/${pointerToken(key)}`, texts)]));
220
+ }
221
+ return value;
222
+ }
223
+ /**
224
+ * Eklenen metin bir kez okunur ve olduğu gibi konur: satır sonu korunur, metin yeniden JSON olarak ayrıştırılmaz —
225
+ * eklenen dosyanın içindeki `$file` bu yüzden metin kalır (iç içe ekleme yok). UTF-8 olmayan dosya sessizce � ile
226
+ * bozulacağına reddedilir; baştaki UTF-8 BOM'u çözücü düşürür (`bytes` onu yine sayar). Yer bilgisi (`at`) gövde argümanına göre JSON Pointer'dır.
227
+ */
228
+ async function includeFiles(tool, value, folder, bodyBytes, roots, config) {
229
+ const sites = [];
230
+ const malformed = [];
231
+ findInclusions(value, '', sites, malformed);
232
+ if (malformed.length > 0) {
233
+ throw new PlatformApiError(`${tool}: bodyFile'da biçimi bozuk $file nesnesi — ${malformed.join(' · ')}. Ekleme yalnız tam { "$file": "<yol>" } biçimidir (tek anahtar, dizgi değer). İstek gönderilmedi.`, 400);
234
+ }
235
+ if (sites.length === 0)
236
+ return { value, files: [] };
237
+ if (sites.length > maxInclusions) {
238
+ throw new PlatformApiError(`${tool}: bodyFile ${sites.length} $file eklemesi taşıyor; sınır ${maxInclusions}. İlk fazlası: ${pointerLabel(sites[maxInclusions]?.at ?? '')} — gövdeyi birden çok çağrıya bölün. İstek gönderilmedi.`, 400);
239
+ }
240
+ const decoder = new TextDecoder('utf-8', { fatal: true });
241
+ const texts = new Map();
242
+ const files = [];
243
+ let total = bodyBytes;
244
+ for (const site of sites) {
245
+ const label = `$file (${pointerLabel(site.at)} → '${site.file}')`;
246
+ const read = await readGuardedFile(tool, label, resolve(folder, site.file), includedExtensions, maxIncludedFileBytes, roots, config);
247
+ total += read.buffer.length;
248
+ if (total > maxBodyFileBytes) {
249
+ throw new PlatformApiError(`${tool}: bodyFile ve $file eklemeleri birlikte 5 MB toplam gövde sınırını aşıyor (${pointerLabel(site.at)} eklemesinde). Tanımı parçalara bölün. İstek gönderilmedi.`, 400);
250
+ }
251
+ try {
252
+ texts.set(site.at, decoder.decode(read.buffer));
253
+ }
254
+ catch {
255
+ throw new PlatformApiError(`${tool}: ${label} UTF-8 değil — ${read.real}. Dosyayı UTF-8 olarak kaydedin. İstek gönderilmedi.`, 400);
256
+ }
257
+ files.push({ at: site.at, path: read.real, bytes: read.buffer.length });
101
258
  }
102
- return { args: { ...rest, [argument]: value }, note: `${bodyFile} → ${argument} (${bytes} B)` };
259
+ return { value: replaceInclusions(value, '', texts), files };
103
260
  }
@@ -1,4 +1,5 @@
1
1
  import { INTERNAL_ROLE_SEPARATOR } from '../../knowledge/componentCatalog.js';
2
+ import { AWAITING_WINDOW_SELECTOR } from './readiness.js';
2
3
  import { studioWebTarget } from './targets/studio-web.js';
3
4
  /**
4
5
  * `swipe` jestinin adım sayısı ve aralığı: react-native-web yanıtlayıcısı tek seferde atılan hareketi jest saymaz
@@ -38,12 +39,23 @@ export function actionScript(action, captureTarget = studioWebTarget) {
38
39
  return `(function () {
39
40
  var count = performance.getEntriesByType('resource').length;
40
41
  var node = document.querySelector(${selector});
41
- if (!node) return { found: false, applied: false, requests: count };
42
+ if (!node) {
43
+ // g-11: açık önizleme penceresi (onay/uyarı/biyometri) sayfanın düğmesini örtüyor olabilir; not hangisi açık söyler.
44
+ var open = document.querySelector('${AWAITING_WINDOW_SELECTOR}');
45
+ return { found: false, applied: false, requests: count, openWindow: open ? { id: String(open.getAttribute('data-element-id')), kind: open.getAttribute('data-dialog-kind') } : null };
46
+ }
42
47
  // g-04 sırası: düğümün kendisi basılabilirse o → <id>__press → içteki ilk basılabilir öğe.
43
48
  // Mobil çoklu seçim tetikleyicisi kökte role="combobox" taşır ve içindeki ilk role="button"
44
49
  // çipin "kaldır" düğmesidir — eskiden ona basılıp seçim siliniyordu (37 S-18). Tek basışlı
45
50
  // denetimin yüzeyi <id>__press'tir (ui-native press-role-guard), anahtarınki role="switch".
46
51
  var press = document.querySelector(${JSON.stringify(`[data-element-id="${String(action.target ?? '')}${INTERNAL_ROLE_SEPARATOR}press"]`)});
52
+ // g-11 incelemesi: önizleme penceresi (onay/uyarı/biyometri) açıkken altında kalan sayfa öğesine gerçek kullanıcı
53
+ // basamaz; DOM tıklaması basardı ve sayfa eylemi pencere yanıtlanmadan koşardı.
54
+ var shield = document.querySelector('${AWAITING_WINDOW_SELECTOR}');
55
+ if (shield && !shield.contains(node)) {
56
+ var shieldId = String(shield.getAttribute('data-element-id'));
57
+ return { found: true, applied: false, requests: count, openWindow: { id: shieldId, kind: shield.getAttribute('data-dialog-kind') }, note: "önizleme penceresi " + shieldId + " açık ve sayfayı örtüyor: gerçek kullanıcı alttaki öğeye basamaz, eylem uygulanmadı — önce pencereyi yanıtlayın (" + shieldId + "__confirm / __cancel, biyometride __verify / __cancel)" };
58
+ }
47
59
  var target = ${pressSurface} || node.matches('button, a, [role="button"], [role="switch"], [role="combobox"]') ? node
48
60
  : (press || node.querySelector('button, a, [role="button"], [role="switch"]') || node);
49
61
  // S-2: sekme grubunun KÖKÜNE click, DOM sırasındaki ilk sekmeye gider ve "uygulandı" der —
@@ -433,6 +445,13 @@ export function actionScript(action, captureTarget = studioWebTarget) {
433
445
  // K1 (g-04): gerçek kullanıcı alana odaklanır, yazar ve çıkar. Blur'suz yazımda odak kaybına
434
446
  // bağlı akış (ui-native girişlerin 'changed'i) hiç koşmuyordu (37 S-19). focus() odağı alamazsa
435
447
  // (görünmez alan, odaksız belge) olaylar elle atılır: Svelte 'blur'u, React 'focusout'u dinler.
448
+ // blur: false (g-12) telefonda klavye açıkken yazılan hâli sınar: odak alanda kalır, not söyler.
449
+ var keepFocus = ${JSON.stringify(action.blur === false)};
450
+ var done = function () {
451
+ var result = { found: true, applied: true, requests: count };
452
+ if (keepFocus) result.note = "blur: false — odak alanda kaldı; odak kaybına bağlı akış ('changed') bu adımda koşmaz";
453
+ return finish(result);
454
+ };
436
455
  var focusField = function () {
437
456
  field.focus();
438
457
  if (document.activeElement === field) return;
@@ -448,15 +467,15 @@ export function actionScript(action, captureTarget = studioWebTarget) {
448
467
  if (field instanceof HTMLInputElement && (field.type === 'checkbox' || field.type === 'radio')) {
449
468
  var shouldCheck = wanted === 'true' || wanted === '1' || wanted === 'on';
450
469
  if (field.checked !== shouldCheck) field.click();
451
- blurField();
452
- return finish({ found: true, applied: true, requests: count });
470
+ if (!keepFocus) blurField();
471
+ return done();
453
472
  }
454
473
  var setter = Object.getOwnPropertyDescriptor(proto, 'value').set;
455
474
  setter.call(field, ${value});
456
475
  field.dispatchEvent(new Event('input', { bubbles: true }));
457
476
  field.dispatchEvent(new Event('change', { bubbles: true }));
458
- blurField();
459
- return finish({ found: true, applied: true, requests: count });
477
+ if (!keepFocus) blurField();
478
+ return done();
460
479
  })()`;
461
480
  }
462
481
  /** `rf_range__start` → `rf_range`: iç rol hedefinin prop'ları kök düğümde yaşar. */
@@ -6,7 +6,7 @@ import { PersistentScreenshotBrowser, persistentContextOpener, screenshotProfile
6
6
  import { actionScript, activeTabScript, rootElementId, runtimePropsScript } from './actions.js';
7
7
  import { domSummaryScript } from './summary.js';
8
8
  import { createPageConsoleLog, describePageFailure, isExpressionEcho, parseRenderErrors, renderErrorsScript, splitExpressionErrors } from './console-log.js';
9
- import { decideReadiness, describeUnready, parseReadiness, readinessScript, runtimePath } from './readiness.js';
9
+ import { awaitingUserNote, decideReadiness, describeUnready, parseAwaitingWindow, parseReadiness, readinessScript, runtimePath } from './readiness.js';
10
10
  /** Runtime'ın Connect Data bağlantısı için bastığı sözde akış kimliği: hata bir akış adımından değil bağlı prop'un okumasından gelir. */
11
11
  const CONNECT_DATA_FLOW_ID = 'connect-data';
12
12
  /**
@@ -159,6 +159,9 @@ const resizeSettleBudgetMs = 3000;
159
159
  * çağrıya ve ~3.5 dakikaya mal oldu). İkiden fazlası turu uzatır, bir şey kazandırmaz.
160
160
  */
161
161
  const navigationAttempts = 2;
162
+ /** g-12 oturumları: depo durumu çerez (jeton dahil) taşıdığı için diske değil süreç belleğine yazılır. */
163
+ const sessionTtlMs = 30 * 60_000;
164
+ const maxSessions = 8;
162
165
  /**
163
166
  * Yalnız zaman aşımı yeniden denenir. `net::ERR_CONNECTION_REFUSED` / DNS gibi hızlı düşen
164
167
  * hatalarda Studio ayakta değildir; tekrar yalnız bekleme süresini iki katına çıkarır ve
@@ -232,6 +235,7 @@ export class StudioScreenshotter {
232
235
  queue = Promise.resolve();
233
236
  pending = 0;
234
237
  recycleNext = false;
238
+ sessions = new Map();
235
239
  constructor(config, launcher) {
236
240
  this.config = config;
237
241
  this.launcher = launcher;
@@ -307,7 +311,7 @@ export class StudioScreenshotter {
307
311
  * işaretlenir ve kare yine çekilir, çünkü bozuk/boş sayfayı görmek de bir doğrulamadır. Veri
308
312
  * yanıtından sonra fontlar + kısa yerleşme beklemesi Svelte hydrate'ine alan bırakır.
309
313
  */
310
- async runActions(page, request, consoleLog) {
314
+ async runActions(page, context, request, consoleLog) {
311
315
  if (!request.actions || request.actions.length === 0)
312
316
  return undefined;
313
317
  const outcomes = [];
@@ -319,12 +323,23 @@ export class StudioScreenshotter {
319
323
  }
320
324
  const snapshotBefore = parseReadiness(await page.evaluate(readinessScript).catch(() => undefined));
321
325
  const activityBefore = snapshotBefore?.activity ?? null;
322
- const raw = await page.evaluate(actionScript(action, request.target)).catch(error => ({
323
- found: false,
324
- applied: false,
325
- note: error instanceof Error ? error.message : String(error),
326
- requests: 0
327
- }));
326
+ // `network` (g-12) sayfada betik koşturmaz: bağlamın ağını değiştirir ve sayfanın gördüğü `navigator.onLine`'ı
327
+ // okur. Ağ açılınca koşan eşitleme akışları aşağıdaki aynı hazırlık beklemesiyle `flowRuns`'a yazılır.
328
+ const raw = action.action === 'network'
329
+ ? context.setOffline === undefined
330
+ ? { found: true, applied: false, note: 'ağ denetimi yok: tarayıcı bağlamı setOffline desteklemiyor — ağ DEĞİŞMEDİ', requests: 0 }
331
+ : await (async () => {
332
+ const count = await page.evaluate("performance.getEntriesByType('resource').length").catch(() => 0);
333
+ await context.setOffline?.(action.value === 'offline');
334
+ const online = await page.evaluate('navigator.onLine').catch(() => undefined);
335
+ return { found: true, applied: true, requests: typeof count === 'number' ? count : 0, ...(typeof online === 'boolean' ? { online } : {}) };
336
+ })()
337
+ : await page.evaluate(actionScript(action, request.target)).catch(error => ({
338
+ found: false,
339
+ applied: false,
340
+ note: error instanceof Error ? error.message : String(error),
341
+ requests: 0
342
+ }));
328
343
  const before = (raw ?? {});
329
344
  // Hedef bulunamadıysa ajan neyi yanlış yazdığını KAREDEN çıkaramaz. `__` taşıyan hedef bir
330
345
  // iç rol denemesidir: bilinen roller sayılır. Kök hedefi bulunamadıysa sorun elementId'nin
@@ -335,6 +350,9 @@ export class StudioScreenshotter {
335
350
  : action.target.includes(INTERNAL_ROLE_SEPARATOR)
336
351
  ? `'${action.target}' sayfada yok. Bu bir İÇ ROL hedefi: biçim <elementId>__<rol> ve bilinen roller — ${knownInternalRoles().join(' · ')}. Roller görünen metinden değil kayıttaki anahtardan türer (sekme key'i, kart id'si); hedef bileşenin rollerini component_get çıktısındaki internalRoles verir.`
337
352
  : `'${action.target}' elementId'si sayfada yok. page_schema_get ile elementId'leri listeleyin; bir komponentin İÇ kontrolünü hedefliyorsanız <elementId>__<rol> biçimini kullanın (component_get çıktısındaki internalRoles).`;
353
+ const openWindow = parseAwaitingWindow(before.openWindow);
354
+ if (openWindow !== null)
355
+ before.note = `${before.note} Açık önizleme penceresi: ${openWindow.id}${openWindow.kind === null ? '' : ` (${openWindow.kind})`}; sayfanın düğmeleri onun altında kalır.`;
338
356
  }
339
357
  // Olay akışı uç çağırıyor: yerleşme beklemeden sonraki istek sayısını ölçmek "istek gitti mi"
340
358
  // sorusunu cevaplar — koşuda kusuru kesinleştiren gözlem buydu.
@@ -376,9 +394,11 @@ export class StudioScreenshotter {
376
394
  : undefined;
377
395
  const propWritten = typeof propsAfter === 'string' ? propsAfter !== before.propsBefore : undefined;
378
396
  const applied = before.applied === true && propWritten !== false;
397
+ // g-11: eylem bir önizleme penceresi açtıysa (onay, uyarı, biyometri) sıradaki eylemin neye basacağı burada söylenir.
398
+ const awaitingNote = readiness.state === 'awaiting-user' ? awaitingUserNote(readiness.snapshot?.awaitingWindow) : undefined;
379
399
  const note = propWritten === false
380
400
  ? `değer DOM'a yazıldı ama '${rootElementId(String(action.target ?? ''))}' düğümünün runtime prop'ları DEĞİŞMEDİ — bileşen olayı kendi change yoluna çevirmedi ya da değer zaten aynıydı. Çok alanlı bileşende <elementId>__<rol> kutusunu hedefleyin (component_get → internalRoles).`
381
- : before.note;
401
+ : [before.note, awaitingNote].filter((part) => part !== undefined && part !== '').join(' ') || undefined;
382
402
  outcomes.push({
383
403
  index,
384
404
  action: String(action.action),
@@ -395,6 +415,7 @@ export class StudioScreenshotter {
395
415
  ...(before.optionsSeen === undefined ? {} : { optionsSeen: before.optionsSeen }),
396
416
  ...(before.waitedMs === undefined ? {} : { waitedMs: before.waitedMs }),
397
417
  ...(before.submitVia === undefined ? {} : { submitVia: before.submitVia }),
418
+ ...(before.online === undefined ? {} : { online: before.online }),
398
419
  pageState: readiness.state
399
420
  });
400
421
  }
@@ -491,7 +512,47 @@ export class StudioScreenshotter {
491
512
  context = await browser.newContext(contextOptions);
492
513
  }
493
514
  logStep('tarayıcı →', `GET ${request.url} · viewport ${request.viewport.width}x${request.viewport.height}${request.fullPage ? ' · fullPage' : ''}${request.token ? '' : ' · gateway jetonu YOK'}${(request.cookies ?? []).length > 0 ? ` · caller çerezleri: ${(request.cookies ?? []).map(cookie => cookie.name).join(', ')}` : ''}${request.token || (request.cookies ?? []).length > 0 ? '' : ' · ANONİM (çerez YOK)'}${request.colorScheme ? ` · tema ${request.colorScheme}` : ''}${request.motion === 'play' ? ' · hareket oynar' : ''}${request.headers ? ` · başlık: ${Object.keys(request.headers).join(', ')}` : ''}`);
515
+ let sessionRestored;
516
+ // Oturum depoyu bir kez kaydeder: başarılı yolda yanıttan önce, hata yolunda `finally`'de. Yükleme yarıda kaldıysa
517
+ // (`setStorageState` önce siler, sonra yazar) yarı boş depo iyi kaydın üstüne yazılmaz (g-12 inceleme #6).
518
+ let sessionPending = request.session !== undefined;
519
+ // Okunamayan depo eski kaydı BIRAKMAZ: bırakılsaydı sonraki çekim aradaki yazmaları sessizce kaybeden eski depoyu
520
+ // `sessionRestored: true` ile yüklerdi (g-12 inceleme #5).
521
+ const saveSession = async () => {
522
+ sessionPending = false;
523
+ if (request.session === undefined || !context.storageState)
524
+ return true;
525
+ const state = await context.storageState().catch(() => undefined);
526
+ this.sessions.delete(request.session);
527
+ if (state === undefined)
528
+ return false;
529
+ this.sessions.set(request.session, { state, at: Date.now() });
530
+ for (const [key, entry] of this.sessions) {
531
+ if (Date.now() - entry.at > sessionTtlMs || this.sessions.size > maxSessions)
532
+ this.sessions.delete(key);
533
+ }
534
+ return true;
535
+ };
494
536
  try {
537
+ // Oturum çerezlerden ÖNCE yüklenir: `setStorageState` mevcut depoyu siler, sonra yazılan jeton çerezi kalsın.
538
+ // Yeteneği olmayan bağlamda çağrı sessizce taze depoyla çekilmez (colorScheme'in kuralı).
539
+ if (request.session !== undefined) {
540
+ if (!context.storageState || !context.setStorageState) {
541
+ throw new PlatformApiError(`session='${request.session.split('|')[0]}' uygulanamadı: tarayıcı bağlamı depo durumunu okuyup yükleyemiyor (storageState/setStorageState). Kare taze depoyla çekilirdi ve bu sessizce yanlış olurdu.`);
542
+ }
543
+ const saved = this.sessions.get(request.session);
544
+ if (saved !== undefined && Date.now() - saved.at > sessionTtlMs)
545
+ this.sessions.delete(request.session);
546
+ sessionRestored = saved !== undefined && Date.now() - saved.at <= sessionTtlMs;
547
+ // İlk çağrı da boş depoyla başlar: kalıcı profilde önceki çekimlerden kalan köken anlık görüntüye girmesin (#8).
548
+ try {
549
+ await context.setStorageState(sessionRestored && saved !== undefined ? saved.state : { cookies: [], origins: [] });
550
+ }
551
+ catch (error) {
552
+ sessionPending = false;
553
+ throw error;
554
+ }
555
+ }
495
556
  const cookies = new Map();
496
557
  if (request.token)
497
558
  cookies.set('NG_AccessToken', request.token);
@@ -573,7 +634,7 @@ export class StudioScreenshotter {
573
634
  : readiness.state === 'not-ready' ? false : undefined;
574
635
  const finalHref = readiness.snapshot?.href;
575
636
  const finalUrl = finalHref !== undefined && runtimePath(finalHref) !== runtimePath(request.url) ? finalHref : undefined;
576
- const actions = await this.runActions(page, request, consoleLog);
637
+ const actions = await this.runActions(page, context, request, consoleLog);
577
638
  // Ölçüm EYLEMLERDEN SONRA: eylem sekme değiştirir, filtre uygular, panel açar — içerik
578
639
  // boyu o zaman kesinleşir. Eskiden ölçüm eylemlerden önceydi ve ikinci sekmede 40 satırlık
579
640
  // tablo açan bir çekim viewport boyunda kalıyordu; `contentHeight` ilk sekmenin boyuydu
@@ -643,6 +704,7 @@ export class StudioScreenshotter {
643
704
  if (runtimeErrors.length > 0) {
644
705
  logStep('akış hatası ✕', `${runtimeErrors.length} akış adımı hata bıraktı — sayfa yine 200 döndü`, runtimeErrors.map(error => `${error.step?.name ?? '?'}${error.element ? ` → ${error.element}.${error.property ?? '?'}` : ''}: ${error.message ?? ''}`));
645
706
  }
707
+ const sessionSaved = await saveSession();
646
708
  return {
647
709
  ...(png === undefined ? {} : { png, mimeType: format === 'jpeg' ? 'image/jpeg' : 'image/png' }),
648
710
  ...(png === undefined ? {} : (size => size === undefined ? {} : { imageSize: size })(imageSize(png))),
@@ -653,6 +715,7 @@ export class StudioScreenshotter {
653
715
  renderWaitTimedOut: apiStatus === undefined,
654
716
  elapsedMs,
655
717
  pageState: readiness.state,
718
+ ...(readiness.state === 'awaiting-user' && readiness.snapshot?.awaitingWindow ? { awaitingWindow: readiness.snapshot.awaitingWindow } : {}),
656
719
  readyAfterMs: readiness.waitedMs,
657
720
  ...(finalUrl === undefined ? {} : { finalUrl }),
658
721
  ...(measured === undefined ? {} : { contentHeight: measured }),
@@ -665,10 +728,16 @@ export class StudioScreenshotter {
665
728
  ...(pageErrors.length > 0 ? { pageErrors } : {}),
666
729
  ...(renderErrors.length > 0 ? { renderErrors } : {}),
667
730
  ...(flowsSettled === undefined ? {} : { flowsSettled }),
668
- ...(navigationRetried ? { navigationRetried: true } : {})
731
+ ...(navigationRetried ? { navigationRetried: true } : {}),
732
+ ...(sessionRestored === undefined ? {} : { sessionRestored }),
733
+ ...(sessionSaved ? {} : { sessionSaved: false })
669
734
  };
670
735
  }
671
736
  finally {
737
+ // Hata yolunda da depo kapanıştan ÖNCE alınır (kalıcı profil kapanışta depoyu siler): ağsız yazılmış kayıt
738
+ // çekim düşse de oturumda kalır. En yenisi sona yazılır; süresi dolan ve sekizi aşan en eski oturum düşer.
739
+ if (sessionPending)
740
+ await saveSession();
672
741
  await context.close().catch(() => undefined);
673
742
  // Zamanlayıcı ÖNCE temizlenir: eskiden her çekim yenisini kurup eskisini bırakıyordu ve
674
743
  // kaçan zamanlayıcı sonraki çekimin ortasında tarayıcıyı kapatıyordu (S-3).
@@ -72,6 +72,8 @@ export class PersistentScreenshotBrowser {
72
72
  * Tek çekimlik oturum. Eski taze bağlamın yalıtımı korunur: başlık ve init script'i bağlama
73
73
  * değil sayfaya yazılır (sonraki çekime sızmaz — `impersonated-user-id` dahil), kapanışta
74
74
  * çerezler ve gezilen kökenlerin depoları (localStorage, IndexedDB, …) diskteki profilden silinir.
75
+ * `session` (g-12) bu silmeyi atlamaz: depo kapanıştan önce okunur, sonraki çekimde yüklenir; yüklenen kökenler de
76
+ * kapanışta silinir. Ağ kesildiyse sayfalar kapandıktan SONRA geri açılır — bağlam sonraki çekimle paylaşılır.
75
77
  */
76
78
  class PersistentContextSession {
77
79
  context;
@@ -79,6 +81,8 @@ class PersistentContextSession {
79
81
  initScripts = [];
80
82
  headers;
81
83
  pages = [];
84
+ restoredOrigins = new Set();
85
+ offline = false;
82
86
  constructor(context, options) {
83
87
  this.context = context;
84
88
  this.options = options;
@@ -92,6 +96,20 @@ class PersistentContextSession {
92
96
  async setExtraHTTPHeaders(headers) {
93
97
  this.headers = headers;
94
98
  }
99
+ async setOffline(offline) {
100
+ await this.context.setOffline(offline);
101
+ this.offline = offline;
102
+ }
103
+ storageState() {
104
+ return this.context.storageState({ indexedDB: true });
105
+ }
106
+ async setStorageState(state) {
107
+ for (const entry of state.origins ?? []) {
108
+ if (typeof entry.origin === 'string')
109
+ this.restoredOrigins.add(entry.origin);
110
+ }
111
+ await this.context.setStorageState(state);
112
+ }
95
113
  async newPage() {
96
114
  const page = await this.context.newPage();
97
115
  this.pages.push(page);
@@ -104,7 +122,7 @@ class PersistentContextSession {
104
122
  return page;
105
123
  }
106
124
  async close() {
107
- const origins = new Set();
125
+ const origins = new Set(this.restoredOrigins);
108
126
  for (const page of this.pages) {
109
127
  try {
110
128
  const origin = new URL(page.url()).origin;
@@ -126,5 +144,9 @@ class PersistentContextSession {
126
144
  await this.context.clearCookies().catch(() => undefined);
127
145
  for (const page of this.pages)
128
146
  await page.close().catch(() => undefined);
147
+ // Ağ EN SON açılır: sayfa açıkken açılsaydı çevrimdışı uygulama `online` olayıyla kuyruğunu sunucuya boşaltırdı —
148
+ // salt okunur bir çekimden görünmez yazma, oturumla taşınan kuyrukta da çift kayıt (g-12 inceleme #4).
149
+ if (this.offline)
150
+ await this.context.setOffline(false).catch(() => undefined);
129
151
  }
130
152
  }