@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
@@ -14,6 +14,13 @@
14
14
  * (`captureUnready: true` teşhis kipi dışında). Bozuk sayfayı görmek istiyorsan hata mesajı
15
15
  * neyin görüldüğünü zaten söyler.
16
16
  */
17
+ /**
18
+ * Önizlemenin kendi pencereleri — uygulama düğümü değil, Studio kabuğunun öğesi. Açıkken bir akış adımı kullanıcının
19
+ * yanıtını bekler: `biometric-auth` (kök ADR 0021, `@ng-apps/ui-native` `preview-biometric.tsx`) ve `confirm`/`alert`
20
+ * (Studio `components/dialog`, plans/defter-saha-bulgulari f-11; tür `data-dialog-kind`'da).
21
+ * Seçici yalnız çift tırnak taşır: betiklere tek tırnakla gömülür.
22
+ */
23
+ export const AWAITING_WINDOW_SELECTOR = '[data-element-id="preview-biometric"], [data-element-id="preview-dialog"]';
17
24
  export const readinessScript = `(function () { /* readinessSnapshot */
18
25
  var activity = window.__ngAppsFlowActivity;
19
26
  return {
@@ -22,11 +29,34 @@ export const readinessScript = `(function () { /* readinessSnapshot */
22
29
  skeleton: !!document.querySelector('.view-detail > .bx--skeleton__placeholder'),
23
30
  viewDetail: !!document.querySelector('.view-detail'),
24
31
  errorState: !!document.querySelector('.view-detail__error, .view-detail .error-page'),
25
- awaitingUser: !!document.querySelector('[data-element-id="preview-biometric"]'),
32
+ awaitingUser: !!document.querySelector('${AWAITING_WINDOW_SELECTOR}'),
33
+ awaitingWindow: (function () {
34
+ var open = document.querySelector('${AWAITING_WINDOW_SELECTOR}');
35
+ return open ? { id: String(open.getAttribute('data-element-id')), kind: open.getAttribute('data-dialog-kind') } : null;
36
+ })(),
26
37
  readyState: String(document.readyState),
27
38
  href: String(location.href)
28
39
  };
29
40
  })()`;
41
+ /** Tarayıcıdan gelen pencere değeri; kimliği olmayan değer pencere sayılmaz. */
42
+ export function parseAwaitingWindow(raw) {
43
+ if (!raw || typeof raw !== 'object')
44
+ return null;
45
+ const { id, kind } = raw;
46
+ return typeof id === 'string' && id !== '' ? { id, kind: typeof kind === 'string' ? kind : null } : null;
47
+ }
48
+ /**
49
+ * `awaiting-user` karesinin ve pencere açıkken bulunamayan `click` hedefinin notu: hangi pencere açık, dalı sürmek için hangi
50
+ * düğmeye basılır. Kimlikler çevrilmiş metinden bağımsızdır; production'da (cihaz, `/view`) bu pencereler yoktur.
51
+ */
52
+ export function awaitingUserNote(window) {
53
+ if (window?.id === 'preview-dialog') {
54
+ return window.kind === 'alert'
55
+ ? "Bir akış kullanıcı penceresini bekliyor: önizlemenin uyarı penceresi (alert adımı) açık. Kare pencereyi gösterir; kapatıp akışı sürdürmek için sonraki eylem { action: 'click', target: 'preview-dialog__confirm' }. Kimlik yalnız Studio önizlemesindedir; cihazda pencere işletim sisteminindir."
56
+ : "Bir akış kullanıcı penceresini bekliyor: önizlemenin onay penceresi (confirm adımı) açık. Kare pencereyi gösterir; onay dalı için sonraki eylem { action: 'click', target: 'preview-dialog__confirm' }, red dalı için 'preview-dialog__cancel'. Kimlikler yalnız Studio önizlemesindedir; cihazda pencere işletim sisteminindir.";
57
+ }
58
+ return "Bir akış kullanıcı penceresini bekliyor: önizlemenin biyometri penceresi açık (kök ADR 0021). Kare pencereyi gösterir; dalı sürmek için sonraki eylem { action: 'click', target: 'preview-biometric__verify' } (Doğrulandı say) ya da 'preview-biometric__cancel' (vazgeç). Production /view'da pencere yoktur.";
59
+ }
30
60
  /** Okunan değer gerçekten sayaç mı; değilse sayfa sayaç yayınlamıyordur (eski runtime). */
31
61
  export function isFlowActivity(activity) {
32
62
  if (!activity || typeof activity !== 'object')
@@ -48,6 +78,7 @@ export function parseReadiness(raw) {
48
78
  viewDetail: value.viewDetail === true,
49
79
  errorState: value.errorState === true,
50
80
  awaitingUser: value.awaitingUser === true,
81
+ awaitingWindow: parseAwaitingWindow(value.awaitingWindow),
51
82
  readyState: String(value.readyState ?? ''),
52
83
  href: value.href
53
84
  };
@@ -12,6 +12,12 @@ export class PlatformApiError extends Error {
12
12
  * izin slug'ları geçer ('entity.reveal grant'i'); `accessDecision` izni ondan okursa yanlış izin yazar.
13
13
  */
14
14
  apiMessage;
15
+ /**
16
+ * Ağ hatası geçici: ad çözümleme ya da yanıt öncesi bağlantı kopması (D-19). Ajan tekrar edilebilirliği metni
17
+ * ayrıştırmadan bilsin diye araç hata yanıtına `transient: true` olarak çıkar. Zaman aşımı geçici SAYILMAZ:
18
+ * uzun işlemi tekrar etmek aynı süreyi bir kez daha bekletir.
19
+ */
20
+ transient;
15
21
  constructor(message, status, code) {
16
22
  super(message);
17
23
  this.status = status;
@@ -474,17 +480,51 @@ export function mapPlatformError(status, payload, context) {
474
480
  }
475
481
  const CONNECTION_RESET_CODES = new Set(['ECONNRESET', 'UND_ERR_SOCKET', 'EPIPE', 'ECONNABORTED']);
476
482
  /**
477
- * Bağlantı yolda koptu mu (yanıt gelmeden): Docker Desktop'ın host port yönlendirmesi yük altında açık bağlantıları ara ara
478
- * düşürür — konteyner içinden aynı uç kopmaz (f-02, 2026-10-24 ölçümü: içeride 0/1800, host'ta `:3000` 4/1800, `:80` 2/1800).
483
+ * Ad çözümlemesi bağlantı açılmadan düşer; istek sunucuya hiç gitmemiştir. Defter uzun betiklerin ortasında `ENOTFOUND`
484
+ * gördü ve kendi istemcisine üç denemelik sarmal yazdı (D-19).
479
485
  */
480
- export function isConnectionReset(error) {
486
+ const NAME_RESOLUTION_CODES = new Set(['ENOTFOUND', 'EAI_AGAIN']);
487
+ function transientNetworkKind(error) {
481
488
  const failure = error;
482
- return CONNECTION_RESET_CODES.has(String(failure?.code)) || CONNECTION_RESET_CODES.has(String(failure?.cause?.code));
489
+ const codes = [String(failure?.code), String(failure?.cause?.code)];
490
+ if (codes.some(code => NAME_RESOLUTION_CODES.has(code)))
491
+ return 'name-resolution';
492
+ if (codes.some(code => CONNECTION_RESET_CODES.has(code)))
493
+ return 'connection-reset';
494
+ return undefined;
495
+ }
496
+ /**
497
+ * Tekrarı zararsız istekte (GET/HEAD, kimlik jetonu) geçici ağ hatasından sonraki beklemeler; uzunluğu tekrar sayısıdır
498
+ * (D-19: en çok iki tekrar). Zaman aşımı bu yola girmez — varsayılan istek süresi 100 sn, tekrar onu bir kez daha bekletirdi.
499
+ */
500
+ export const NETWORK_RETRY_DELAYS_MS = [250, 1000];
501
+ /**
502
+ * Yanıt gelmeden düşen ve kısa beklemeyle geçebilen ağ hatası: ad çözümleme (`ENOTFOUND`/`EAI_AGAIN`) ya da yolda kopan
503
+ * bağlantı. Docker Desktop'ın host port yönlendirmesi yük altında açık bağlantıları ara ara düşürür — konteyner içinden
504
+ * aynı uç kopmaz (f-02, 2026-10-24 ölçümü: içeride 0/1800, host'ta `:3000` 4/1800, `:80` 2/1800). `undici` kodu kimi
505
+ * zaman `cause.code`'da taşır.
506
+ */
507
+ export function isTransientNetworkError(error) {
508
+ return transientNetworkKind(error) !== undefined;
483
509
  }
484
- export function mapNetworkError(error, context) {
510
+ export function mapNetworkError(error, context, attempt = { attempts: 1, readOnly: context.method === 'GET' || context.method === 'HEAD' }) {
485
511
  const cause = error instanceof Error ? error : new Error(String(error));
486
512
  if (cause.name === 'TimeoutError' || cause.name === 'AbortError') {
487
513
  return new PlatformApiError(`${context.api} API zaman aşımına uğradı (${context.method} ${context.path}). Uzun süren bir işlemse durumu ilgili *_history/*_status tool'undan izleyin.`);
488
514
  }
515
+ const kind = transientNetworkKind(error);
516
+ const tried = attempt.attempts > 1 ? ` (${attempt.attempts} deneme)` : '';
517
+ if (kind === 'name-resolution') {
518
+ const failure = new PlatformApiError(`${context.api} API'nin adı çözülemedi (${context.baseUrl})${tried}: ${cause.message}. İstek sunucuya ULAŞMADI (ad çözülemedi) — aynı çağrıyı tekrarlamak güvenli. Sürüyorsa DNS/ağ bağlantısını kontrol edin.`);
519
+ failure.transient = true;
520
+ return failure;
521
+ }
522
+ if (kind === 'connection-reset') {
523
+ const failure = new PlatformApiError(attempt.readOnly
524
+ ? `${context.api} API bağlantısı yanıt gelmeden koptu (${context.method} ${context.path})${tried}: ${cause.message}. Okuma isteği — tekrarlamak güvenli.`
525
+ : `${context.api} API bağlantısı yanıt gelmeden koptu (${context.method} ${context.path}): ${cause.message}. İstek sunucuya ulaşmış olabilir — tekrarlamadan önce yazının gerçekleşip gerçekleşmediğini ilgili *_get/*_list tool'uyla doğrulayın; yeniden yazmak çift kayıt üretebilir.`);
526
+ failure.transient = true;
527
+ return failure;
528
+ }
489
529
  return new PlatformApiError(`${context.api} API'ye ulaşılamadı (${context.baseUrl}): ${cause.message}. API ayakta mı? Lokal ortam için docker compose ya da dotnet run kontrol edin.`);
490
530
  }
@@ -1,4 +1,4 @@
1
- import { PlatformApiError, isConnectionReset, mapNetworkError, mapPlatformError } from './errors.js';
1
+ import { NETWORK_RETRY_DELAYS_MS, PlatformApiError, isTransientNetworkError, mapNetworkError, mapPlatformError } from './errors.js';
2
2
  import { logStep, logsDetail, renderJson } from '../observability/trace.js';
3
3
  import { AsyncLocalStorage } from 'node:async_hooks';
4
4
  import { TokenProvider } from './token.js';
@@ -30,6 +30,9 @@ export function responseHeadersForTool(raw) {
30
30
  .map(([name, value]) => [name.toLowerCase(), Array.isArray(value) ? value.join(', ') : value])
31
31
  .filter((entry) => typeof entry[1] === 'string' && !hiddenResponseHeaders.has(entry[0])));
32
32
  }
33
+ function sleep(ms) {
34
+ return new Promise(resolve => setTimeout(resolve, ms));
35
+ }
33
36
  function buildMultipart(parts, boundary) {
34
37
  const chunks = [];
35
38
  for (const part of parts) {
@@ -74,10 +77,12 @@ function appendQuery(url, query) {
74
77
  }
75
78
  export class Platform {
76
79
  config;
80
+ wait;
77
81
  tokens;
78
- constructor(config, tokens) {
82
+ constructor(config, tokens, wait = sleep) {
79
83
  this.config = config;
80
- this.tokens = tokens ?? new TokenProvider(config);
84
+ this.wait = wait;
85
+ this.tokens = tokens ?? new TokenProvider(config, wait);
81
86
  }
82
87
  resolveProject(project) {
83
88
  const resolved = project ?? this.config.defaultProject;
@@ -134,7 +139,7 @@ export class Platform {
134
139
  }
135
140
  return `Bearer ${await this.tokens.getToken()}`;
136
141
  }
137
- async send(api, method, path, options, raw = false, isRetry = false, networkRetried = false) {
142
+ async send(api, method, path, options, raw = false, isRetry = false, networkAttempt = 1) {
138
143
  const baseUrl = api === 'org' ? this.config.orgApiBaseUrl : this.config.tenantApiBaseUrl;
139
144
  const context = { api, method, path, baseUrl, environment: options.environment, authMode: this.config.authMode };
140
145
  const url = new URL(`${baseUrl}${path}`);
@@ -207,12 +212,15 @@ export class Platform {
207
212
  }
208
213
  catch (error) {
209
214
  logStep('ağ ✕', `${method} ${url.href} · ${Date.now() - startedAt} ms sonra ulaşılamadı`, [error instanceof Error ? `${error.name}: ${error.message}` : String(error)]);
210
- // Yalnız tekrarı zararsız istek (GET/HEAD) ve yalnız bir kez: yazan istek sunucuya ulaşmış olabilir.
211
- if ((method === 'GET' || method === 'HEAD') && !networkRetried && !options.noNetworkRetry && isConnectionReset(error)) {
212
- logStep('ağ ⟳', `${method} ${url.href} · bağlantı koptu, bir kez yeniden deneniyor`);
213
- return this.send(api, method, path, options, raw, isRetry, true);
215
+ // Yalnız tekrarı zararsız istek (GET/HEAD): yazan istek sunucuya ulaşmış olabilir ve kimliksiz yazma çift kayıt üretir (D-19).
216
+ const readOnly = (method === 'GET' || method === 'HEAD') && !options.noNetworkRetry;
217
+ const delay = NETWORK_RETRY_DELAYS_MS[networkAttempt - 1];
218
+ if (readOnly && delay !== undefined && isTransientNetworkError(error)) {
219
+ logStep('ağ ⟳', `${method} ${url.href} · geçici ağ hatası, ${delay} ms sonra yeniden deneniyor (deneme ${networkAttempt + 1}/${NETWORK_RETRY_DELAYS_MS.length + 1})`);
220
+ await this.wait(delay);
221
+ return this.send(api, method, path, options, raw, isRetry, networkAttempt + 1);
214
222
  }
215
- throw mapNetworkError(error, context);
223
+ throw mapNetworkError(error, context, { attempts: networkAttempt, readOnly });
216
224
  }
217
225
  logStep('yanıt ←', `HTTP ${statusCode} · ${Date.now() - startedAt} ms · ${responseBytes} B`, statusCode >= 400 || logsDetail()
218
226
  ? [`gövde: ${options.expectBinary && statusCode < 400 ? `<ikili içerik · ${responseBytes} B>` : renderJson(payload)}`]
@@ -211,9 +211,15 @@ export function toolError(error, expectFailure) {
211
211
  // Kasıtlı negatif testte (bayat version 409, yetkisiz kişi 403) platform hatası da bir KANITTIR:
212
212
  // ölçüm aracı bu satırı plansız hata saymasın diye işaret taşınır.
213
213
  const isExpectedPlatformError = expectFailure === true && (error instanceof PlatformApiError || error instanceof PolicyBlockError);
214
+ // Ajan tekrar edilebilir ağ hatasını metni ayrıştırmadan bilsin (D-19); yazan istekte metin tekrarın güvenli olup olmadığını söyler.
215
+ const isTransient = error instanceof PlatformApiError && error.transient === true;
216
+ const structuredContent = {
217
+ ...(isExpectedPlatformError ? { expectedFailure: true } : {}),
218
+ ...(isTransient ? { transient: true } : {})
219
+ };
214
220
  return {
215
221
  content: [{ type: 'text', text: message }],
216
- ...(isExpectedPlatformError ? { structuredContent: { expectedFailure: true } } : {}),
222
+ ...(Object.keys(structuredContent).length > 0 ? { structuredContent } : {}),
217
223
  isError: true
218
224
  };
219
225
  }
@@ -27,6 +27,12 @@ const maxDepth = 12;
27
27
  * `StepCustomParameterDto {name,value,order}`, `QueryParameter {name,type,value}`: değer bağlama
28
28
  * ifadesidir (`input.token`), maskelenirse get → update turunda `***` geri yazılır (GW-001).
29
29
  * `expression` + `value` hiçbir paketli şemada geçmez, o yüzden bu ayrımın dışındadır.
30
+ *
31
+ * `expiresInHours` taşıyan ve değeri bir ifade olan satır da tanımdır (g-17): `setcookie` adımının `args`'ı
32
+ * `{ name: 'APP_Token', value: 'Steps.sign_in.data.token', expiresInHours }` — maskelenince flow_get → flow_update
33
+ * turu çereze `***` yazıp girişi bozuyordu. Değer tırnakla başlıyorsa dizgi sabitidir ve maskelenir: yürütme yanıtının
34
+ * `uiActions`'ı aynı şekli GERÇEK değerle taşır (`SetCookieStep.cs`: `value: "\"<değer>\""`), tanımdaki sabit de
35
+ * gömülü bir sırdır. `path` muafiyet değildir: `{ name, value, path }` gerçek bir çerezin serileşmiş hâlidir.
30
36
  */
31
37
  export function secretPairName(record) {
32
38
  if (!('value' in record))
@@ -35,6 +41,8 @@ export function secretPairName(record) {
35
41
  return record.expression;
36
42
  if ('type' in record || 'order' in record)
37
43
  return undefined;
44
+ if ('expiresInHours' in record && typeof record.value === 'string' && !/^\s*["'`]/.test(record.value))
45
+ return undefined;
38
46
  if (typeof record.name === 'string')
39
47
  return record.name;
40
48
  return typeof record.key === 'string' ? record.key : undefined;
@@ -1,4 +1,4 @@
1
- import { PlatformApiError, mapNetworkError, mapPlatformError } from './errors.js';
1
+ import { NETWORK_RETRY_DELAYS_MS, PlatformApiError, isTransientNetworkError, mapNetworkError, mapPlatformError } from './errors.js';
2
2
  import { logStep } from '../observability/trace.js';
3
3
  import { request } from 'undici';
4
4
  function readExpiry(token) {
@@ -17,9 +17,11 @@ function readExpiry(token) {
17
17
  }
18
18
  export class TokenProvider {
19
19
  config;
20
+ wait;
20
21
  cached;
21
- constructor(config) {
22
+ constructor(config, wait = ms => new Promise(resolve => setTimeout(resolve, ms))) {
22
23
  this.config = config;
24
+ this.wait = wait;
23
25
  }
24
26
  async getToken() {
25
27
  if (this.config.token)
@@ -40,29 +42,38 @@ export class TokenProvider {
40
42
  let statusCode;
41
43
  let payload;
42
44
  logStep('kimlik →', `önbellekte geçerli token yok — POST ${this.config.orgApiBaseUrl}/connect/token · kullanıcı ${this.config.username ?? '(yok)'}`);
43
- try {
44
- const response = await request(`${this.config.orgApiBaseUrl}/connect/token`, {
45
- method: 'POST',
46
- headers: {
47
- host: `${this.config.organization}.${this.config.hostSuffix}`,
48
- 'content-type': 'application/x-www-form-urlencoded',
49
- accept: 'application/json'
50
- },
51
- body: form.toString(),
52
- signal: AbortSignal.timeout(this.config.requestTimeoutMs)
53
- });
54
- statusCode = response.statusCode;
55
- const text = await response.body.text();
45
+ // Jeton isteği kayıt yazmaz: geçici ağ hatasında GET gibi yeniden denenir ve hata tekrarın güvenli olduğunu söyler
46
+ // (D-19; inceleme: soğuk jeton önbelleğinde tek DNS kesintisi okuyan aracı da düşürüyordu).
47
+ for (let attempt = 1;; attempt += 1) {
56
48
  try {
57
- payload = text ? JSON.parse(text) : undefined;
49
+ const response = await request(`${this.config.orgApiBaseUrl}/connect/token`, {
50
+ method: 'POST',
51
+ headers: {
52
+ host: `${this.config.organization}.${this.config.hostSuffix}`,
53
+ 'content-type': 'application/x-www-form-urlencoded',
54
+ accept: 'application/json'
55
+ },
56
+ body: form.toString(),
57
+ signal: AbortSignal.timeout(this.config.requestTimeoutMs)
58
+ });
59
+ statusCode = response.statusCode;
60
+ const text = await response.body.text();
61
+ try {
62
+ payload = text ? JSON.parse(text) : undefined;
63
+ }
64
+ catch {
65
+ payload = text;
66
+ }
67
+ break;
58
68
  }
59
- catch {
60
- payload = text;
69
+ catch (error) {
70
+ const delay = NETWORK_RETRY_DELAYS_MS[attempt - 1];
71
+ if (delay === undefined || !isTransientNetworkError(error))
72
+ throw mapNetworkError(error, context, { attempts: attempt, readOnly: true });
73
+ logStep('kimlik ⟳', `POST ${this.config.orgApiBaseUrl}/connect/token · geçici ağ hatası, ${delay} ms sonra yeniden deneniyor (deneme ${attempt + 1}/${NETWORK_RETRY_DELAYS_MS.length + 1})`);
74
+ await this.wait(delay);
61
75
  }
62
76
  }
63
- catch (error) {
64
- throw mapNetworkError(error, context);
65
- }
66
77
  if (statusCode >= 400)
67
78
  throw mapPlatformError(statusCode, payload, context);
68
79
  const accessToken = payload && typeof payload === 'object' ? payload.access_token : undefined;
@@ -0,0 +1,233 @@
1
+ {
2
+ "_meta": {
3
+ "synthetic": true,
4
+ "note": "Mobil kredi harcama ekranı (kök docs/adr/0027-uygulama-ici-satin-alma.md bakiye bölümü, plans/mobil-uygulama-ici-satin-alma g-02): bakiye Balances.kredi'den okunur, düğme balance-spend ile 10 kredi düşer; bakiye yetmezse (insufficient) kullanıcıya söylenir. Akış FLOW-046'dan temiz geçer (test/coverage.test.ts). Kredi satın almak ayrı bir teklif ekranıdır: consumable ürün (kredi_100) purchase adımıyla alınır (flow-purchase-paywall).",
5
+ "pairsWith": "flow-purchase-paywall (kredi satışı); önce mobile_product_upsert { key: 'kredi_100', kind: 'consumable', balance: 'kredi', grantsAmount: 100, priceTry, texts }",
6
+ "sourceRef": "platform_guide('mobile-steps') §Kredi harcama tarifi; platform_guide('mobile-publish') §Satın almalar (tüketilen ürün); backend 04-bakiye-defteri.md §Harcama (409 insufficient, çevrimdışı unavailable)"
7
+ },
8
+ "_rules": {
9
+ "order": "Sıra: (1) mobile_product_upsert ile consumable ürün (balance: 'kredi'); (2) page_create + schemaPatchOps (kök önce safe-area); (3) flowCreateArgs; (4) bindOps ile akış kimliğini düğmeye bağla. Bakiye adı katalogdaki consumable ürünün balance alanıdır: katalogda olmayan statik ad FLOW-046 ile yazımda durur.",
10
+ "args": "balance-spend.value bakiye adı, balance-spend.title MİKTARDIR (args.amount/args.balance sessizce düşer). İkisi de metin şablonudur: düz '10' ya da seçimden '{{ adet.value }}'; {{ }} dışındaki ifade harfi harfine metin sayılır.",
11
+ "branches": "Adım KAPIDIR: error dalı → hata akışı → ikisi de yoksa akış adımda biter. Steps.<ad>.error.status: insufficient (bakiye yetmiyor, sunucu 409) · unavailable (çevrimdışı ya da sunucuya ulaşılamadı — çevrimdışı kuyruğa girmez) · unsupported (web uygulaması, production /view, eski kabuk) · invalid (ad ya da miktar geçersiz) · failed.",
12
+ "balance": "Harcamadan sonra Balances kendiliğinden güncellenir; sonucu ayrıca okumayın. Bakiye yalnız sunucudan gelir, istemcide hesaplanmaz.",
13
+ "signIn": "Girişsiz uygulamada bakiye kuruluma yazılır ve tüketilen ürün mağazada geri yüklenemez: telefon değişince bakiye gider (MOB-036). Girişli uygulamada hesaba taşınır."
14
+ },
15
+ "pageCreateArgs": {
16
+ "name": "Kredi",
17
+ "slug": "kredi"
18
+ },
19
+ "schemaPatchOps": [
20
+ {
21
+ "op": "add_component",
22
+ "parent": "page_container",
23
+ "component": {
24
+ "elementId": "guvenli_alan",
25
+ "name": "guvenli_alan",
26
+ "type": "safe-area",
27
+ "kind": "component",
28
+ "props": {},
29
+ "appearance": {
30
+ "marginType": "none",
31
+ "widthType": "fill"
32
+ },
33
+ "blocks": [],
34
+ "events": [],
35
+ "validations": [],
36
+ "stateBindings": [],
37
+ "children": [
38
+ {
39
+ "elementId": "kredi_ekrani",
40
+ "name": "kredi_ekrani",
41
+ "type": "container",
42
+ "kind": "component",
43
+ "props": {
44
+ "direction": {
45
+ "type": "static",
46
+ "ref": null,
47
+ "data": "column"
48
+ },
49
+ "gap": {
50
+ "type": "static",
51
+ "ref": null,
52
+ "data": "medium"
53
+ },
54
+ "horizontalAlign": {
55
+ "type": "static",
56
+ "ref": null,
57
+ "data": "stretch"
58
+ }
59
+ },
60
+ "appearance": {
61
+ "marginType": "none",
62
+ "widthType": "fill"
63
+ },
64
+ "blocks": [],
65
+ "events": [],
66
+ "validations": [],
67
+ "stateBindings": [],
68
+ "children": [
69
+ {
70
+ "elementId": "txt_kredi_baslik",
71
+ "name": "txt_kredi_baslik",
72
+ "type": "text",
73
+ "kind": "component",
74
+ "props": {
75
+ "content": {
76
+ "type": "static",
77
+ "ref": null,
78
+ "data": "Rapor üret"
79
+ },
80
+ "type": {
81
+ "type": "static",
82
+ "ref": null,
83
+ "data": "h2"
84
+ }
85
+ },
86
+ "appearance": {
87
+ "marginType": "none",
88
+ "widthType": "fill"
89
+ },
90
+ "blocks": [],
91
+ "events": [],
92
+ "validations": [],
93
+ "stateBindings": [],
94
+ "children": []
95
+ },
96
+ {
97
+ "elementId": "txt_bakiye",
98
+ "name": "txt_bakiye",
99
+ "type": "text",
100
+ "kind": "component",
101
+ "props": {
102
+ "content": {
103
+ "type": "code",
104
+ "ref": "{{ 'Kredin: ' + (Balances.kredi ?? 0) }}",
105
+ "data": null
106
+ }
107
+ },
108
+ "appearance": {
109
+ "marginType": "none",
110
+ "widthType": "fill"
111
+ },
112
+ "blocks": [],
113
+ "events": [],
114
+ "validations": [],
115
+ "stateBindings": [],
116
+ "children": []
117
+ },
118
+ {
119
+ "elementId": "btn_uret",
120
+ "name": "btn_uret",
121
+ "type": "button",
122
+ "kind": "component",
123
+ "props": {
124
+ "text": {
125
+ "type": "static",
126
+ "ref": null,
127
+ "data": "Rapor üret (10 kredi)"
128
+ },
129
+ "kind": {
130
+ "type": "static",
131
+ "ref": null,
132
+ "data": "primary"
133
+ }
134
+ },
135
+ "appearance": {
136
+ "marginType": "none",
137
+ "widthType": "fill"
138
+ },
139
+ "blocks": [],
140
+ "events": [],
141
+ "validations": [],
142
+ "stateBindings": [],
143
+ "children": []
144
+ }
145
+ ]
146
+ }
147
+ ]
148
+ }
149
+ }
150
+ ],
151
+ "flowCreateArgs": {
152
+ "host": "page",
153
+ "hostId": "<kredi sayfasının id'si>",
154
+ "name": "rapor-uret",
155
+ "slug": "rapor-uret",
156
+ "steps": [
157
+ {
158
+ "stepId": "start",
159
+ "name": "start",
160
+ "type": "start",
161
+ "kind": "normal",
162
+ "args": {},
163
+ "branches": [
164
+ {
165
+ "name": "always",
166
+ "nextStepId": "harca"
167
+ }
168
+ ]
169
+ },
170
+ {
171
+ "stepId": "harca",
172
+ "name": "harca",
173
+ "type": "balance-spend",
174
+ "kind": "normal",
175
+ "args": {
176
+ "value": "kredi",
177
+ "title": "10"
178
+ },
179
+ "branches": [
180
+ {
181
+ "name": "always",
182
+ "nextStepId": "hazir"
183
+ },
184
+ {
185
+ "name": "error",
186
+ "nextStepId": "olmadi"
187
+ }
188
+ ]
189
+ },
190
+ {
191
+ "stepId": "hazir",
192
+ "name": "hazir",
193
+ "type": "alert",
194
+ "kind": "normal",
195
+ "args": {
196
+ "message": "Raporun hazır. {{ Balances.kredi }} kredin kaldı.",
197
+ "alertType": "success"
198
+ },
199
+ "branches": [
200
+ {
201
+ "name": "always",
202
+ "nextStepId": null
203
+ }
204
+ ]
205
+ },
206
+ {
207
+ "stepId": "olmadi",
208
+ "name": "olmadi",
209
+ "type": "alert",
210
+ "kind": "normal",
211
+ "args": {
212
+ "message": "{{ Steps.harca.error.status === 'insufficient' ? 'Krediniz yetmiyor: önce kredi satın alın.' : 'Harcama yapılamadı: ' + Steps.harca.error.message }}",
213
+ "alertType": "error"
214
+ },
215
+ "branches": [
216
+ {
217
+ "name": "always",
218
+ "nextStepId": null
219
+ }
220
+ ]
221
+ }
222
+ ],
223
+ "errorSteps": []
224
+ },
225
+ "bindOps": [
226
+ {
227
+ "op": "set_event",
228
+ "target": "btn_uret",
229
+ "event": "click",
230
+ "eventValue": "<rapor-uret akış id>"
231
+ }
232
+ ]
233
+ }