@appaflytech/zeyrek-ai-mcp 0.0.4 → 0.0.6

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 (101) hide show
  1. package/.env.example +8 -3
  2. package/.env.production.example +2 -0
  3. package/README.md +229 -193
  4. package/dist/config.js +20 -2
  5. package/dist/knowledge/backendConstraints.js +897 -0
  6. package/dist/knowledge/componentCatalog.js +196 -42
  7. package/dist/knowledge/coverage.js +43 -0
  8. package/dist/knowledge/flowGuards.js +30 -0
  9. package/dist/knowledge/guideSections.js +49 -0
  10. package/dist/knowledge/hydrate.js +97 -0
  11. package/dist/knowledge/languageCodes.js +36 -0
  12. package/dist/knowledge/languages.js +105 -0
  13. package/dist/knowledge/layoutSummary.js +10 -0
  14. package/dist/knowledge/localization.js +51 -0
  15. package/dist/knowledge/normalize.js +449 -16
  16. package/dist/knowledge/outlines.js +136 -12
  17. package/dist/knowledge/propTypes.js +80 -0
  18. package/dist/knowledge/rules.js +618 -43
  19. package/dist/knowledge/schemaGuard.js +199 -0
  20. package/dist/knowledge/styleResolve.js +63 -4
  21. package/dist/observability/trace.js +27 -3
  22. package/dist/platform/errors.js +46 -4
  23. package/dist/platform/http.js +33 -4
  24. package/dist/platform/patch.js +27 -0
  25. package/dist/platform/refs.js +104 -0
  26. package/dist/platform/responses.js +9 -2
  27. package/dist/platform/screenshot.js +527 -16
  28. package/dist/platform/secrets.js +40 -0
  29. package/dist/platform/tokenVault.js +89 -0
  30. package/dist/platform/writeDiff.js +281 -0
  31. package/dist/resources/examples/layout-app-shell.json +1 -1
  32. package/dist/resources/examples/microservice-table-list.json +2 -4
  33. package/dist/resources/examples/page-dynamic-form.json +2 -2
  34. package/dist/resources/examples/page-flex-sections.json +99 -0
  35. package/dist/resources/examples/page-sign-in.json +3 -2
  36. package/dist/resources/guides/component.md +69 -8
  37. package/dist/resources/guides/entity.md +404 -16
  38. package/dist/resources/guides/expressions.md +44 -2
  39. package/dist/resources/guides/external-component.md +9 -0
  40. package/dist/resources/guides/getting-started.md +77 -7
  41. package/dist/resources/guides/layout.md +53 -10
  42. package/dist/resources/guides/microservice.md +226 -13
  43. package/dist/resources/guides/middleware.md +89 -1
  44. package/dist/resources/guides/page.md +383 -46
  45. package/dist/resources/guides/translation.md +126 -8
  46. package/dist/resources/guides/view.md +16 -1
  47. package/dist/resources/guides/workflow.md +198 -27
  48. package/dist/resources/knowledge.js +5 -0
  49. package/dist/resources/rules.js +27 -0
  50. package/dist/resources/schema-fingerprint.json +8 -0
  51. package/dist/resources/schemas/CreateBulkRowCommand.json +33 -0
  52. package/dist/resources/schemas/CreateDatasourceCommand.json +15 -6
  53. package/dist/resources/schemas/CreateDiscoveredEntitiesCommand.json +19 -8
  54. package/dist/resources/schemas/CreateEntityCommand.json +52 -12
  55. package/dist/resources/schemas/CreateMicroserviceCommand.json +26 -2
  56. package/dist/resources/schemas/CreateQueryCommand.json +56 -3
  57. package/dist/resources/schemas/CreateRowCommand.json +38 -0
  58. package/dist/resources/schemas/CreateViewCommand.json +13 -10
  59. package/dist/resources/schemas/CreateWorkflowCommand.json +95 -14
  60. package/dist/resources/schemas/GetRowsQuery.json +3 -0
  61. package/dist/resources/schemas/PreviewMicroserviceCommand.json +16 -0
  62. package/dist/resources/schemas/PreviewQueryCommand.json +5 -2
  63. package/dist/resources/schemas/PreviewWorkflowCommand.json +34 -9
  64. package/dist/resources/schemas/StepDto.json +23 -3
  65. package/dist/resources/schemas/UpdateBulkRowCommand.json +33 -0
  66. package/dist/resources/schemas/UpdateDatasourceCommand.json +15 -6
  67. package/dist/resources/schemas/UpdateEntityCommand.json +49 -10
  68. package/dist/resources/schemas/UpdateMicroserviceCommand.json +28 -3
  69. package/dist/resources/schemas/UpdateQueryCommand.json +13 -3
  70. package/dist/resources/schemas/UpdateRowCommand.json +40 -0
  71. package/dist/resources/schemas/UpdateViewCommand.json +15 -10
  72. package/dist/resources/schemas/UpdateWorkflowCommand.json +95 -14
  73. package/dist/server.js +4 -0
  74. package/dist/tools/access.js +3 -3
  75. package/dist/tools/applications.js +1 -1
  76. package/dist/tools/automations.js +63 -30
  77. package/dist/tools/common.js +39 -0
  78. package/dist/tools/components.js +16 -7
  79. package/dist/tools/datasources.js +83 -21
  80. package/dist/tools/deployments.js +34 -2
  81. package/dist/tools/dtos.js +25 -12
  82. package/dist/tools/entities.js +113 -28
  83. package/dist/tools/externalComponents.js +9 -3
  84. package/dist/tools/files.js +20 -6
  85. package/dist/tools/flows.js +110 -11
  86. package/dist/tools/folders.js +7 -2
  87. package/dist/tools/globalFunctions.js +63 -0
  88. package/dist/tools/layouts.js +103 -29
  89. package/dist/tools/microservices.js +229 -38
  90. package/dist/tools/organizations.js +5 -6
  91. package/dist/tools/pages.js +365 -100
  92. package/dist/tools/platformInfo.js +152 -11
  93. package/dist/tools/projects.js +183 -33
  94. package/dist/tools/queries.js +32 -10
  95. package/dist/tools/rows.js +159 -45
  96. package/dist/tools/screenWrite.js +59 -0
  97. package/dist/tools/screens.js +28 -0
  98. package/dist/tools/translations.js +24 -8
  99. package/dist/tools/views.js +20 -10
  100. package/dist/tools/workflows.js +33 -16
  101. package/package.json +6 -3
@@ -1,18 +1,337 @@
1
1
  import { PlatformApiError } from './errors.js';
2
+ import { INTERNAL_ROLE_SEPARATOR, knownInternalRoles } from '../knowledge/componentCatalog.js';
2
3
  import { logStep } from '../observability/trace.js';
4
+ /**
5
+ * G-24 — gateway sayfayı kurabiliyor, okuyabiliyor ve çizdirebiliyordu ama üzerinde OLAY
6
+ * TETİKLEYEMİYORDU: "filtre değişince veri yenileniyor" iddiası yalnız şema okumasıyla
7
+ * kanıtlanabiliyordu. 09-05 koşusunda üç olay bağının ikisi şemada "bağlı" görünüp gerçekte
8
+ * çalışmıyordu (form submit hiç doğmuyordu, `code` adımının gövdesi kaydedilmemişti) ve ikisi de
9
+ * yalnız gerçek tıklamayla görüldü.
10
+ *
11
+ * Değer yazarken NATIVE setter kullanılır ve `input`+`change` olayları bubbles ile atılır: DOM
12
+ * value'sunu düz atamak framework'ün duyduğu olayı üretmez, alan dolu görünür ama akış çalışmaz.
13
+ * `resource` sayacı "istek gerçekten gitti mi" sorusunu cevaplar — koşuda kusuru kesinleştiren
14
+ * gözlem tam olarak buydu (ağ kaydında hiç istek yoktu).
15
+ */
16
+ export function actionScript(action) {
17
+ const selector = JSON.stringify(`[data-element-id="${String(action.target ?? '')}"]`);
18
+ const value = JSON.stringify(action.value ?? '');
19
+ if (action.action === 'click') {
20
+ return `(function () {
21
+ var count = performance.getEntriesByType('resource').length;
22
+ var node = document.querySelector(${selector});
23
+ if (!node) return { found: false, applied: false, requests: count };
24
+ var target = node.matches('button, a, [role="button"]') ? node : (node.querySelector('button, a, [role="button"]') || node);
25
+ target.click();
26
+ return { found: true, applied: true, requests: count };
27
+ })()`;
28
+ }
29
+ return `(function () {
30
+ var count = performance.getEntriesByType('resource').length;
31
+ var node = document.querySelector(${selector});
32
+ if (!node) return { found: false, applied: false, requests: count };
33
+ var wanted = String(${value});
34
+
35
+ // Açılır kutu (dropdown / multi-select) NATIVE alan değildir: değeri yazmak
36
+ // hiçbir şey yapmaz, seçeneğe TIKLANMASI gerekir. Liste ancak kutu açıkken
37
+ // basılır, o yüzden önce kutu açılır, sonra data-value ile seçenek bulunur
38
+ // (bulunamazsa görünen metinle eşlenir).
39
+ var combobox = node.querySelector('[role="combobox"], .ng-select__value-container, .ng-multiselect__value-container');
40
+ if (combobox) {
41
+ combobox.click();
42
+
43
+ // Liste tıklamadan SONRA basılır; aynı tick'te sorgulamak boş sonuç verir.
44
+ // evaluate döndürülen promise'i bekler, o yüzden seçim bir sonraki tur yapılır.
45
+ return new Promise(function (resolve) {
46
+ setTimeout(function () {
47
+ var options = Array.prototype.slice.call(document.querySelectorAll('[role="treeitem"][data-value]'));
48
+ var match = options.filter(function (option) { return option.getAttribute('data-value') === wanted; })[0]
49
+ || options.filter(function (option) { return (option.textContent || '').trim() === wanted; })[0];
50
+ if (!match) {
51
+ resolve({ found: true, applied: false, note: "açılır kutu açıldı ama bu değere karşılık gelen seçenek yok: " + wanted + " — options listesini page_runtime_get ile doğrulayın; seçenek sayısı: " + options.length, requests: count });
52
+ return;
53
+ }
54
+ (match.querySelector('button') || match).click();
55
+ resolve({ found: true, applied: true, requests: count });
56
+ }, 300);
57
+ });
58
+ }
59
+
60
+ var field = node.matches('input, select, textarea') ? node : node.querySelector('input, select, textarea');
61
+ if (!field) return { found: true, applied: false, note: 'düğümde input/select/textarea yok — bu komponent native alan kullanmıyor olabilir, click ile deneyin', requests: count };
62
+ var proto = field instanceof HTMLSelectElement ? HTMLSelectElement.prototype
63
+ : (field instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype);
64
+ if (field instanceof HTMLInputElement && (field.type === 'checkbox' || field.type === 'radio')) {
65
+ var shouldCheck = wanted === 'true' || wanted === '1' || wanted === 'on';
66
+ if (field.checked !== shouldCheck) field.click();
67
+ return { found: true, applied: true, requests: count };
68
+ }
69
+ var setter = Object.getOwnPropertyDescriptor(proto, 'value').set;
70
+ setter.call(field, ${value});
71
+ field.dispatchEvent(new Event('input', { bubbles: true }));
72
+ field.dispatchEvent(new Event('change', { bubbles: true }));
73
+ return { found: true, applied: true, requests: count };
74
+ })()`;
75
+ }
76
+ /**
77
+ * fullPage karesi Chromium'un viewport ötesi yakalamasıyla değil, viewport'u içerik boyuna
78
+ * büyüterek alınır: Studio runtime'ı kendi kaydırma kabını `100vh` üzerine kuruyor ve viewport
79
+ * ötesi yakalama o kabı bir kez daha çiziyordu — aynı sayfa karede iki kez görünüyordu (09-04
80
+ * Y-23). Tek karede çekilen görüntüde o birleştirme adımı hiç yok.
81
+ */
82
+ export const contentHeightScript = [
83
+ '(function () {',
84
+ ' var doc = document.documentElement;',
85
+ ' var body = document.body;',
86
+ ' var viewportHeight = window.innerHeight;',
87
+ ' var heights = [doc ? doc.scrollHeight : 0, body ? body.scrollHeight : 0];',
88
+ ' var nodes = document.querySelectorAll("*");',
89
+ ' for (var index = 0; index < nodes.length && index < 4000; index += 1) {',
90
+ ' var node = nodes[index];',
91
+ ' if (node.clientHeight >= viewportHeight * 0.6 && node.scrollHeight > node.clientHeight + 1) heights.push(node.scrollHeight);',
92
+ ' }',
93
+ ' return { contentHeight: Math.max.apply(null, heights), viewportHeight: viewportHeight };',
94
+ '})()'
95
+ ].join('\n');
96
+ /**
97
+ * Kare taşmayı gösterir ama ölçmez: aynı sayfa yüklemesinden neredeyse bedavaya çıkan ikinci
98
+ * çıktı her runtime düğümünün efektif piksel kutusudur. Düğümler `data-element-id` taşımıyorsa
99
+ * (F-04 dev'e çıkana dek) liste boş döner — tool uydurmaz, layoutReportUnavailable der.
100
+ */
101
+ export const layoutReportScript = [
102
+ 'Array.from(document.querySelectorAll("[data-element-id]")).slice(0, 500).map(function (node) {',
103
+ ' var rect = node.getBoundingClientRect();',
104
+ ' return {',
105
+ ' elementId: node.getAttribute("data-element-id"),',
106
+ ' x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height),',
107
+ ' overflowX: node.scrollWidth > node.clientWidth + 1,',
108
+ ' overflowY: node.scrollHeight > node.clientHeight + 1',
109
+ ' };',
110
+ '})'
111
+ ].join('\n');
112
+ /**
113
+ * F-09 — sayfa akışı yalnız tarayıcıda koşar, bu yüzden bir setter iteratee'sinin
114
+ * patlamasının gateway'e ulaşan HİÇBİR yolu yoktu: akış 200 dönüyor, hedef prop
115
+ * yazılmıyor ve tek belirti yığında kaybolan bir bildirim oluyordu. Runtime artık
116
+ * yapısal hata defterini bu global anahtarda yayınlıyor (`@ng-apps/flow` →
117
+ * flow-runtime-errors.ts); kare çekilirken bedavaya okunur.
118
+ */
119
+ export const flowRuntimeErrorsScript = 'Array.isArray(window.__ngAppsFlowRuntimeErrors) ? window.__ngAppsFlowRuntimeErrors.slice(0, 50) : []';
120
+ /**
121
+ * Kare ne zaman hazır? (G-30 → B-6, 2026-09-07)
122
+ *
123
+ * `delayMs` TAHMİNDİ; 09-05'te runtime koşan akış sayacını yayınlamaya başladı (`@ng-apps/flow`
124
+ * → flow-runtime-errors.ts) ve bekleme "running sıfır, completed > 0" olunca bitiyordu. Bu şart
125
+ * mount akışı OLMAYAN sayfada (giriş ekranı) hiç sağlanmaz; akış tetiklemeyen `set` eylemi de
126
+ * `completed`'ı artırmaz. 09-06·2 koşusunda üç eylemli giriş denemesi bu yüzden 47 sn sürdü;
127
+ * sentetik ölçümde sayfa 0.3 sn'de hazırken aynı çağrı 48 sn'ydi — süre sayfanın değil, bu
128
+ * döngünün. Hazırlık artık TEK anlık görüntüden okunur: akış sayacı + Studio'nun yükleme
129
+ * iskeleti (`ViewDetail.svelte` → `.view-detail > .bx--skeleton__placeholder`) + runtime
130
+ * düğümleri (`[data-element-id]`) + hata/bulunamadı hâli + adres. Karar `decideReadiness`'ta.
131
+ *
132
+ * Yüklenmemiş kare ASLA dönmez: bütçe dolduğunda sayfa hâlâ hazır değilse çağrı hatayla düşer
133
+ * (`captureUnready: true` teşhis kipi dışında). Bozuk sayfayı görmek istiyorsan hata mesajı
134
+ * neyin görüldüğünü zaten söyler.
135
+ */
136
+ export const readinessScript = `(function () { /* readinessSnapshot */
137
+ var activity = window.__ngAppsFlowActivity;
138
+ return {
139
+ activity: activity && typeof activity.running === 'number' && typeof activity.completed === 'number' ? { running: activity.running, completed: activity.completed } : null,
140
+ runtimeNodes: document.querySelectorAll('[data-element-id]').length,
141
+ skeleton: !!document.querySelector('.view-detail > .bx--skeleton__placeholder'),
142
+ viewDetail: !!document.querySelector('.view-detail'),
143
+ errorState: !!document.querySelector('.view-detail__error, .view-detail .error-page'),
144
+ readyState: String(document.readyState),
145
+ href: String(location.href)
146
+ };
147
+ })()`;
148
+ /** Okunan değer gerçekten sayaç mı; değilse sayfa sayaç yayınlamıyordur (eski runtime). */
149
+ export function isFlowActivity(activity) {
150
+ if (!activity || typeof activity !== 'object')
151
+ return false;
152
+ const { running, completed } = activity;
153
+ return typeof running === 'number' && typeof completed === 'number';
154
+ }
155
+ /** Tarayıcıdan dönen değer anlık görüntü mü; yönlendirme sırasında evaluate düşer ve undefined kalır. */
156
+ export function parseReadiness(raw) {
157
+ if (!raw || typeof raw !== 'object')
158
+ return undefined;
159
+ const value = raw;
160
+ if (typeof value.runtimeNodes !== 'number' || typeof value.href !== 'string')
161
+ return undefined;
162
+ return {
163
+ activity: isFlowActivity(value.activity) ? { running: value.activity.running, completed: value.activity.completed } : null,
164
+ runtimeNodes: value.runtimeNodes,
165
+ skeleton: value.skeleton === true,
166
+ viewDetail: value.viewDetail === true,
167
+ errorState: value.errorState === true,
168
+ readyState: String(value.readyState ?? ''),
169
+ href: value.href
170
+ };
171
+ }
172
+ /** `/dev` ve `/view` aynı sayfadır (Studio `hooks.ts` reroute); yönlendirme kararı yalnız yoldan verilir. */
173
+ export function runtimePath(url) {
174
+ try {
175
+ return new URL(url).pathname.replace(/^\/dev\//, '/view/').replace(/\/+$/, '');
176
+ }
177
+ catch {
178
+ return url;
179
+ }
180
+ }
181
+ const maxCaptureHeight = 16384;
3
182
  const navigationTimeoutMs = 20000;
4
183
  const apiResponseTimeoutMs = 15000;
5
184
  const settleMs = 600;
185
+ /** Akış bittikten sonra Svelte'in son render'ına bırakılan pay. */
186
+ const flowSettleGraceMs = 250;
187
+ const flowPollMs = 120;
188
+ /** "Hiç akış koşmadı" kararı için DOM'un kaç yoklama değişmemesi gerekir (~600 ms = eski settleMs). */
189
+ const quietPolls = 5;
190
+ /** Sayaç yayınlanmayan runtime'da tek kanıt DOM durulmasıdır; daha uzun beklenir (~1 sn). */
191
+ const legacyQuietPolls = 8;
192
+ /** Runtime dışına yönlendirme: hedef sayfanın kendi JS'i çizmeyi bitirsin (~1.5 sn). */
193
+ const redirectQuietPolls = 12;
6
194
  const idleCloseMs = 60000;
195
+ /** Etkileşim adımındaki `wait` üst sınırı; delayMs tavanıyla aynı çizgide. */
196
+ const maxActionWaitMs = 15000;
197
+ /**
198
+ * Navigasyon zaman aşımında toplam deneme sayısı. Soğuk Studio'da ilk `page.goto` Vite
199
+ * derlemesini ISITIR; ikinci deneme neredeyse her zaman geçer (09-08 S-8: aynı kare üç
200
+ * çağrıya ve ~3.5 dakikaya mal oldu). İkiden fazlası turu uzatır, bir şey kazandırmaz.
201
+ */
202
+ const navigationAttempts = 2;
203
+ /**
204
+ * Yalnız zaman aşımı yeniden denenir. `net::ERR_CONNECTION_REFUSED` / DNS gibi hızlı düşen
205
+ * hatalarda Studio ayakta değildir; tekrar yalnız bekleme süresini iki katına çıkarır ve
206
+ * çağırana yanlış ipucu verir.
207
+ */
208
+ function isNavigationTimeout(message) {
209
+ return /timeout/i.test(message);
210
+ }
211
+ /**
212
+ * Tek anlık görüntüden "kare alınabilir" kararı; iki uçta da yanlış beklememek için tek yerde.
213
+ * `quiet` görüntünün kaç yoklamadır değişmediğidir, `completedBefore` bu beklemenin başındaki
214
+ * bitmiş akış sayısı (sayfa değiştiyse çağıran sıfırlar). İskelet görünüyorsa hiçbir karar yok.
215
+ */
216
+ export function decideReadiness(snapshot, quiet, completedBefore, baselineUrl) {
217
+ if (snapshot.skeleton)
218
+ return undefined;
219
+ const { activity } = snapshot;
220
+ const idle = activity !== null && activity.running === 0;
221
+ if (idle && activity.completed > completedBefore && snapshot.runtimeNodes > 0)
222
+ return 'ready';
223
+ if (idle && snapshot.runtimeNodes > 0 && quiet >= quietPolls)
224
+ return 'ready-no-flows';
225
+ if (activity === null && snapshot.runtimeNodes > 0 && quiet >= legacyQuietPolls)
226
+ return 'ready-no-counter';
227
+ if (idle && snapshot.errorState && quiet >= quietPolls)
228
+ return 'error';
229
+ if (!snapshot.viewDetail && snapshot.readyState === 'complete' && baselineUrl !== undefined && runtimePath(snapshot.href) !== runtimePath(baselineUrl) && quiet >= redirectQuietPolls)
230
+ return 'redirected';
231
+ return undefined;
232
+ }
233
+ /** Hata mesajı için: bütçe dolduğunda sayfada NE görüldü. */
234
+ export function describeUnready(readiness, apiNote) {
235
+ const snapshot = readiness.snapshot;
236
+ const suffix = apiNote === undefined ? '' : ` · ${apiNote}`;
237
+ if (!snapshot)
238
+ return `tarayıcı sayfayı hiç değerlendiremedi (sürekli yönlendirme ya da bağlam düştü)${suffix}`;
239
+ if (snapshot.skeleton)
240
+ return `sayfa hâlâ yükleme iskeletinde${suffix}`;
241
+ if (snapshot.activity && snapshot.activity.running > 0)
242
+ return `${snapshot.activity.running} akış hâlâ koşuyor (${snapshot.activity.completed} bitti)${suffix}`;
243
+ if (snapshot.runtimeNodes === 0)
244
+ return `runtime hiç render olmadı — data-element-id taşıyan düğüm yok${snapshot.viewDetail ? '' : ', .view-detail kabı da yok'}${suffix}`;
245
+ return `sayfa durulmadı (${snapshot.runtimeNodes} düğüm, sayaç ${snapshot.activity ? `${snapshot.activity.running} koşan / ${snapshot.activity.completed} biten` : 'yok'})${suffix}`;
246
+ }
247
+ async function measureContentHeight(page) {
248
+ const measured = await page.evaluate(contentHeightScript).catch(() => undefined);
249
+ const height = measured?.contentHeight;
250
+ return typeof height === 'number' && Number.isFinite(height) && height > 0 ? Math.ceil(height) : undefined;
251
+ }
252
+ /**
253
+ * Playwright bağlamı ve tarayıcısı bu sınıfta PAYLAŞILIR. İki çekim aynı anda koştuğunda
254
+ * 2026-09-06 koşusunda ikisi de `Target page, context or browser has been closed` ile düştü
255
+ * (çağrı #204/#205, 64 sn kayıp, ajan yeniden giriş yapmak zorunda kaldı — `S-3`). Kök neden
256
+ * iki yerdeydi: (1) paylaşılan tarayıcı üstünde eşzamanlı bağlam açma, (2) her çekimin `finally`
257
+ * bloğunda ÖNCEKİ boşta-kapatma zamanlayıcısı temizlenmeden yenisinin kurulması — kaçan
258
+ * zamanlayıcı sonraki çekimin ortasında tarayıcıyı kapatıyordu.
259
+ *
260
+ * Çözüm bir seri kuyruk: çekimler tek tek koşar, bekleyen çağrı `queuedMs` ile ne kadar
261
+ * beklediğini söyler. Sıcak tarayıcı havuzu bu işin dışıdır (Ç2 · tur ekonomisi).
262
+ */
7
263
  export class StudioScreenshotter {
8
264
  config;
9
265
  launcher;
10
266
  browserPromise;
11
267
  idleTimer;
268
+ queue = Promise.resolve();
269
+ pending = 0;
270
+ recycleNext = false;
12
271
  constructor(config, launcher) {
13
272
  this.config = config;
14
273
  this.launcher = launcher;
15
274
  }
275
+ /**
276
+ * Kuyruk sınırsız birikmesin: bekleme üst sınırı aşılırsa çağrı tarayıcıya HİÇ girmeden
277
+ * anlaşılır bir hatayla döner ve sırasını öndekine zincirleyip çıkar, böylece kuyruğun geri
278
+ * kalanı serileşmiş kalır (Kısıt: tek yavaş çekimin arkasında kuyruk şişmemeli).
279
+ */
280
+ async capture(request) {
281
+ const queuedAt = Date.now();
282
+ // Sayaç SENKRON artar: `Promise.all` ile aynı turda gelen iki çağrıda ikincisi beklediğini
283
+ // ancak böyle bilebilir (async bir bayrak o turda henüz kurulmamış olurdu).
284
+ const waited = this.pending > 0;
285
+ const previous = this.queue;
286
+ this.pending += 1;
287
+ let release;
288
+ this.queue = new Promise(resolve => {
289
+ release = resolve;
290
+ });
291
+ let timer;
292
+ const capExceeded = await Promise.race([
293
+ previous.then(() => false, () => false),
294
+ new Promise(resolve => {
295
+ timer = setTimeout(() => resolve(true), this.config.screenshotQueueWaitMs);
296
+ timer.unref?.();
297
+ })
298
+ ]);
299
+ if (timer)
300
+ clearTimeout(timer);
301
+ if (capExceeded) {
302
+ this.pending -= 1;
303
+ void previous.then(release, release);
304
+ throw new PlatformApiError(`Ekran görüntüsü kuyruğunda ${Date.now() - queuedAt} ms beklendi ve üst sınır (${this.config.screenshotQueueWaitMs} ms) aşıldı — çekim başlatılmadı. Çekimleri seri çağırın ya da SCREENSHOT_QUEUE_WAIT_MS ile sınırı yükseltin.`, 503);
305
+ }
306
+ const queuedMs = Date.now() - queuedAt;
307
+ const recycled = this.recycleNext;
308
+ this.recycleNext = false;
309
+ if (waited)
310
+ logStep('tarayıcı ⏸', `çekim kuyrukta ${queuedMs} ms bekledi — eşzamanlı çekimler seri koşar`);
311
+ try {
312
+ const result = await this.captureOne(request);
313
+ return {
314
+ ...result,
315
+ ...(waited ? { queuedMs: Math.max(queuedMs, 1) } : {}),
316
+ ...(recycled ? { contextRecycled: true } : {})
317
+ };
318
+ }
319
+ catch (error) {
320
+ // Bağlam/tarayıcı bozulduysa bir sonraki çağrı TEMİZ bir tarayıcıyla başlasın; hata
321
+ // kuyruktaki komşuya bulaşmasın.
322
+ if (/browser has been closed|target (page|closed)|context.*closed/i.test(error instanceof Error ? error.message : String(error))) {
323
+ this.recycleNext = true;
324
+ const expired = this.browserPromise;
325
+ this.browserPromise = undefined;
326
+ expired?.then(instance => instance.close()).catch(() => undefined);
327
+ }
328
+ throw error;
329
+ }
330
+ finally {
331
+ this.pending -= 1;
332
+ release();
333
+ }
334
+ }
16
335
  async launch() {
17
336
  if (this.launcher)
18
337
  return this.launcher();
@@ -48,7 +367,124 @@ export class StudioScreenshotter {
48
367
  * işaretlenir ve kare yine çekilir, çünkü bozuk/boş sayfayı görmek de bir doğrulamadır. Veri
49
368
  * yanıtından sonra fontlar + kısa yerleşme beklemesi Svelte hydrate'ine alan bırakır.
50
369
  */
51
- async capture(request) {
370
+ async runActions(page, request) {
371
+ if (!request.actions || request.actions.length === 0)
372
+ return undefined;
373
+ const outcomes = [];
374
+ for (const [index, action] of request.actions.entries()) {
375
+ if (action.action === 'wait') {
376
+ await page.waitForTimeout(Math.min(Math.max(Number(action.ms ?? settleMs), 0), maxActionWaitMs));
377
+ outcomes.push({ index, action: 'wait', found: true, applied: true, requests: 0 });
378
+ continue;
379
+ }
380
+ const snapshotBefore = parseReadiness(await page.evaluate(readinessScript).catch(() => undefined));
381
+ const activityBefore = snapshotBefore?.activity ?? null;
382
+ const raw = await page.evaluate(actionScript(action)).catch(error => ({
383
+ found: false,
384
+ applied: false,
385
+ note: error instanceof Error ? error.message : String(error),
386
+ requests: 0
387
+ }));
388
+ const before = (raw ?? {});
389
+ // Hedef bulunamadıysa ajan neyi yanlış yazdığını KAREDEN çıkaramaz. `__` taşıyan hedef bir
390
+ // iç rol denemesidir: bilinen roller sayılır. Kök hedefi bulunamadıysa sorun elementId'nin
391
+ // kendisindedir, şemayı okumaya yönlendirilir (P1-3 Step 3).
392
+ if (before.found === false && !before.note && typeof action.target === 'string') {
393
+ before.note = action.target.includes(INTERNAL_ROLE_SEPARATOR)
394
+ ? `'${action.target}' sayfada yok. Bu bir İÇ ROL hedefi: biçim <elementId>__<rol> ve bilinen roller — ${knownInternalRoles().join(' · ')}. Kolon rolleri kolonun SLUG'ından türer (başlığından değil); satır sırası 0 tabanlıdır ve sayfa değişince yeniden numaralanır.`
395
+ : `'${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).`;
396
+ }
397
+ // Olay akışı uç çağırıyor: yerleşme beklemeden sonraki istek sayısını ölçmek "istek gitti mi"
398
+ // sorusunu cevaplar — koşuda kusuru kesinleştiren gözlem buydu.
399
+ //
400
+ // Bekleme ana yükleme döngüsüyle AYNI hazırlık kararıdır: akış koştuysa bitince, hiç akış
401
+ // tetiklenmediyse (metin alanına `set`) DOM durulunca, eylem sayfayı değiştirdiyse yeni
402
+ // sayfa hazır olunca çıkar. Eskiden akış tetiklemeyen her eylem `settleMs + delayMs`'in
403
+ // tamamını bekliyordu: 09-06·2 koşusunda üç eylemli giriş denemesi 47 sn (B-6).
404
+ const readiness = await this.waitForReady(page, {
405
+ budgetMs: apiResponseTimeoutMs + request.delayMs,
406
+ baselineUrl: snapshotBefore?.href ?? request.url,
407
+ completedBefore: activityBefore?.completed ?? Number.POSITIVE_INFINITY
408
+ });
409
+ if (readiness.state === 'not-ready' && request.captureUnready !== true) {
410
+ throw new PlatformApiError(`Etkileşim adımı ${index} (${action.action}${action.target === undefined ? '' : ` '${action.target}'`}) sonrasında sayfa ${readiness.waitedMs} ms içinde durulmadı, kare ALINMADI — ${describeUnready(readiness)}. Bekleme bütçesi ${apiResponseTimeoutMs + request.delayMs} ms; akış gerçekten uzun sürüyorsa delayMs ile büyütün. Yüklenmemiş hâli yine de görmek için captureUnready: true verin (kare pageState: 'not-ready' ile döner).`, 504);
411
+ }
412
+ const after = await page.evaluate("performance.getEntriesByType('resource').length").catch(() => undefined);
413
+ const requests = typeof after === 'number' && typeof before.requests === 'number' ? Math.max(after - before.requests, 0) : 0;
414
+ // Yerel bir akış (yalnız prop yazan setter) hiç uca gitmez ama işini YAPAR. Sayaç bu ayrımı
415
+ // veren tek kanıttır; sayaç yoksa flowRuns bilinmez kalır. Eylem sayfayı değiştirdiyse
416
+ // yeni sayfanın sayacı sıfırdan başlar, orada biten her akış bu eyleme yazılır.
417
+ const activityAfter = readiness.snapshot?.activity ?? null;
418
+ const navigated = snapshotBefore !== undefined && readiness.snapshot !== undefined && runtimePath(readiness.snapshot.href) !== runtimePath(snapshotBefore.href);
419
+ const flowRuns = activityAfter === null
420
+ ? undefined
421
+ : navigated
422
+ ? activityAfter.completed
423
+ : activityBefore === null ? undefined : Math.max(activityAfter.completed - activityBefore.completed, 0);
424
+ outcomes.push({
425
+ index,
426
+ action: String(action.action),
427
+ ...(action.target === undefined ? {} : { target: action.target }),
428
+ found: before.found === true,
429
+ applied: before.applied === true,
430
+ ...(before.note === undefined ? {} : { note: before.note }),
431
+ requests,
432
+ ...(flowRuns === undefined ? {} : { flowRuns }),
433
+ pageState: readiness.state
434
+ });
435
+ }
436
+ logStep('tarayıcı ⤵', `${outcomes.length} etkileşim adımı koştu`, outcomes.map(outcome => `${outcome.index}. ${outcome.action}${outcome.target ? ` '${outcome.target}'` : ''} → ${outcome.found ? (outcome.applied ? 'uygulandı' : 'bulundu ama uygulanamadı') : 'hedef BULUNAMADI'} · ${outcome.requests} ağ isteği`));
437
+ return outcomes;
438
+ }
439
+ /**
440
+ * Sayfa hazır olana kadar yoklar; hazır kararını `decideReadiness` verir. Karar verildikten
441
+ * sonra kısa bir pay bırakılır ve görüntü BİR KEZ DAHA doğrulanır: giriş akışının son adımı
442
+ * yönlendirmeyse "bitti" sinyali yeni sayfa açılırken gelir ve doğrulama olmadan kare o
443
+ * geçişte alınırdı. Adres değiştiyse sayaç yeni sayfada sıfırdan başlar; baz da sıfırlanır.
444
+ *
445
+ * Döngü hem süreyle hem TUR sayısıyla sınırlıdır: sahte saatle koşan testte `Date.now()`
446
+ * ilerlemez ve yalnız süreye bakan bir döngü hiç bitmezdi.
447
+ */
448
+ async waitForReady(page, options) {
449
+ const startedAt = Date.now();
450
+ const deadline = startedAt + options.budgetMs;
451
+ const maxPolls = Math.ceil(options.budgetMs / flowPollMs) + 1;
452
+ let completedBefore = options.completedBefore;
453
+ let previousKey;
454
+ let quiet = 0;
455
+ let last;
456
+ for (let poll = 0; poll < maxPolls; poll++) {
457
+ const snapshot = parseReadiness(await page.evaluate(readinessScript).catch(() => undefined));
458
+ if (snapshot === undefined) {
459
+ // Yönlendirme sırasında evaluate düşer: sessizlik sayacı sıfırlanır, yoklama sürer.
460
+ previousKey = undefined;
461
+ quiet = 0;
462
+ }
463
+ else {
464
+ last = snapshot;
465
+ if (runtimePath(snapshot.href) !== runtimePath(options.baselineUrl))
466
+ completedBefore = 0;
467
+ const key = JSON.stringify(snapshot);
468
+ quiet = key === previousKey ? quiet + 1 : 0;
469
+ previousKey = key;
470
+ const state = decideReadiness(snapshot, quiet, completedBefore, options.baselineUrl);
471
+ if (state !== undefined) {
472
+ await page.waitForTimeout(flowSettleGraceMs);
473
+ const confirmed = parseReadiness(await page.evaluate(readinessScript).catch(() => undefined));
474
+ if (confirmed !== undefined && !confirmed.skeleton && confirmed.href === snapshot.href && (confirmed.activity === null || confirmed.activity.running === 0)) {
475
+ return { state, waitedMs: Date.now() - startedAt, snapshot: confirmed };
476
+ }
477
+ previousKey = undefined;
478
+ quiet = 0;
479
+ }
480
+ }
481
+ if (Date.now() >= deadline || poll === maxPolls - 1)
482
+ break;
483
+ await page.waitForTimeout(flowPollMs);
484
+ }
485
+ return { state: 'not-ready', waitedMs: Date.now() - startedAt, ...(last === undefined ? {} : { snapshot: last }) };
486
+ }
487
+ async captureOne(request) {
52
488
  const startedAt = Date.now();
53
489
  if (this.idleTimer)
54
490
  clearTimeout(this.idleTimer);
@@ -69,32 +505,107 @@ export class StudioScreenshotter {
69
505
  });
70
506
  context = await browser.newContext({ viewport: request.viewport, deviceScaleFactor: 1 });
71
507
  }
72
- logStep('tarayıcı →', `GET ${request.url} · viewport ${request.viewport.width}x${request.viewport.height}${request.fullPage ? ' · fullPage' : ''}${request.token ? '' : ' · ANONİM (çerez YOK)'}`);
508
+ 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)'}`);
73
509
  try {
74
- if (request.token) {
75
- await context.addCookies([{ name: 'NG_AccessToken', value: request.token, url: request.cookieUrl, httpOnly: false }]);
510
+ const cookies = new Map();
511
+ if (request.token)
512
+ cookies.set('NG_AccessToken', request.token);
513
+ for (const cookie of request.cookies ?? [])
514
+ cookies.set(cookie.name, cookie.value);
515
+ if (cookies.size > 0) {
516
+ await context.addCookies([...cookies].map(([name, value]) => ({ name, value, url: request.cookieUrl, httpOnly: false })));
76
517
  }
77
518
  const page = await context.newPage();
78
- const observedApiStatus = page
79
- .waitForResponse(response => response.url().includes(request.apiPathMarker), { timeout: apiResponseTimeoutMs })
519
+ // Soğuk Vite derlemesinde sayfanın kendi veri isteği 15 sn'yi aşabiliyor; delayMs bu
520
+ // bütçeyi de uzatır (09-02 E-2). Yanıt hazırlık döngüsünü BLOKLAMAZ: runtime dışına
521
+ // yönlendirilen sayfa bu isteği hiç atmaz ve eskiden 15 sn boşa bekleniyordu.
522
+ const readyBudgetMs = apiResponseTimeoutMs + request.delayMs;
523
+ const watchApiResponse = () => page
524
+ .waitForResponse(response => response.url().includes(request.apiPathMarker), { timeout: readyBudgetMs })
80
525
  .then(response => response.status(), () => undefined);
81
- try {
82
- await page.goto(request.url, { waitUntil: 'domcontentloaded', timeout: navigationTimeoutMs });
526
+ // `delayMs` navigasyon bütçesini de uzatır. Eskiden sabit 20 sn'ydi ve soğuk Studio'da
527
+ // ajanın elinde tek çıkış çağrıyı tekrarlamaktı (09-08 S-8) — "sabrım bu kadar" demenin
528
+ // tek anlamı olsun diye iki bütçe de aynı düğmeye bağlı.
529
+ const navigationTimeout = navigationTimeoutMs + request.delayMs;
530
+ let observedApiStatus = watchApiResponse();
531
+ let navigationRetried = false;
532
+ for (let attempt = 1;; attempt++) {
533
+ try {
534
+ await page.goto(request.url, { waitUntil: 'domcontentloaded', timeout: navigationTimeout });
535
+ break;
536
+ }
537
+ catch (error) {
538
+ const message = error instanceof Error ? error.message : String(error);
539
+ const timedOut = isNavigationTimeout(message);
540
+ if (!timedOut || attempt >= navigationAttempts) {
541
+ const attemptNote = attempt > 1 ? ` (${attempt} deneme)` : '';
542
+ const budgetNote = timedOut
543
+ ? ` Navigasyon bütçesi ${navigationTimeout} ms'ydi; soğuk Studio'da delayMs'i büyütün — o değer artık navigasyonu da uzatır.`
544
+ : '';
545
+ throw new PlatformApiError(`Studio'ya ulaşılamadı${attemptNote}: ${request.url} — ${message}.${budgetNote} STUDIO_BASE_URL doğru mu, arayüz ayakta mı? (yerelde zeyrek-frontend/apps/studio içinde \`pnpm dev\`)`);
546
+ }
547
+ navigationRetried = true;
548
+ // Veri isteği gözlemi de sıfırlanır: ilk denemeyi bekleyen sayaç çoktan dolmuş olur ve
549
+ // yeniden denenen navigasyonun yanıtını kaçırıp boş loader karesi damgalardı.
550
+ observedApiStatus = watchApiResponse();
551
+ logStep('tarayıcı ⟳', `navigasyon ${navigationTimeout} ms'de düştü, bir kez daha deneniyor — ilk deneme soğuk derlemeyi ısıttı`);
552
+ }
83
553
  }
84
- catch (error) {
85
- const message = error instanceof Error ? error.message : String(error);
86
- throw new PlatformApiError(`Studio'ya ulaşılamadı: ${request.url} — ${message}. STUDIO_BASE_URL doğru mu, arayüz ayakta mı? (yerelde zeyrek-frontend/apps/studio içinde \`pnpm dev\`)`);
554
+ const readiness = await this.waitForReady(page, { budgetMs: readyBudgetMs, baselineUrl: request.url, completedBefore: 0 });
555
+ // Gözlendiyse dolu, hâlâ bekleniyorsa (yönlendirme, hata) undefined — ikinci vaat hemen çözülür.
556
+ const apiStatus = await Promise.race([observedApiStatus, Promise.resolve(undefined)]);
557
+ const apiNote = apiStatus === undefined ? 'sayfanın veri isteği gözlenmedi' : `veri isteği HTTP ${apiStatus}`;
558
+ if (readiness.state === 'not-ready' && request.captureUnready !== true) {
559
+ throw new PlatformApiError(`Sayfa ${readiness.waitedMs} ms içinde yüklenmedi, kare ALINMADI — ${describeUnready(readiness, apiNote)}. Bekleme bütçesi ${readyBudgetMs} ms; sayfa yalnız yavaşsa delayMs ile büyütün. Sayfa gerçekten bozuksa page_runtime_get ile şemayı, STUDIO_BASE_URL ve slug'ları doğrulayın. Yüklenmemiş hâli yine de görmek için captureUnready: true verin (kare pageState: 'not-ready' ile döner).`, 504);
87
560
  }
88
- const apiStatus = await observedApiStatus;
89
561
  await page.evaluate('document.fonts.ready.then(() => true)').catch(() => undefined);
90
- await page.waitForTimeout(settleMs + request.delayMs);
91
- const png = await page.screenshot({ type: 'png', fullPage: request.fullPage });
562
+ const flowsSettled = readiness.state === 'ready' || readiness.state === 'ready-no-flows'
563
+ ? true
564
+ : readiness.state === 'not-ready' ? false : undefined;
565
+ const finalHref = readiness.snapshot?.href;
566
+ const finalUrl = finalHref !== undefined && runtimePath(finalHref) !== runtimePath(request.url) ? finalHref : undefined;
567
+ const measured = request.fullPage ? await measureContentHeight(page) : undefined;
568
+ const captureHeight = measured === undefined ? undefined : Math.min(measured, maxCaptureHeight);
569
+ if (captureHeight !== undefined && captureHeight > request.viewport.height) {
570
+ await page.setViewportSize({ width: request.viewport.width, height: captureHeight });
571
+ await page.waitForTimeout(settleMs);
572
+ }
573
+ const actions = await this.runActions(page, request);
574
+ const layout = request.layoutReport === true
575
+ ? await page.evaluate(layoutReportScript).then(value => Array.isArray(value) ? value : []).catch(() => [])
576
+ : undefined;
577
+ const runtimeErrors = await page.evaluate(flowRuntimeErrorsScript)
578
+ .then(value => Array.isArray(value) ? value : [])
579
+ .catch(() => []);
580
+ const png = await page.screenshot({ type: 'png', fullPage: false });
92
581
  const elapsedMs = Date.now() - startedAt;
93
- logStep('tarayıcı ←', `PNG ${png.byteLength} B · ${elapsedMs} ms · sayfa API yanıtı ${apiStatus === undefined ? 'gözlenmedi' : `HTTP ${apiStatus}`}`);
94
- return { png, apiStatus, renderWaitTimedOut: apiStatus === undefined, elapsedMs };
582
+ logStep('tarayıcı ←', `PNG ${png.byteLength} B · ${elapsedMs} ms · hazırlık ${readiness.state} (${readiness.waitedMs} ms) · sayfa API yanıtı ${apiStatus === undefined ? 'gözlenmedi' : `HTTP ${apiStatus}`}${finalUrl === undefined ? '' : ` · yönlendi → ${finalUrl}`}`);
583
+ if (runtimeErrors.length > 0) {
584
+ 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 ?? ''}`));
585
+ }
586
+ return {
587
+ png,
588
+ apiStatus,
589
+ renderWaitTimedOut: apiStatus === undefined,
590
+ elapsedMs,
591
+ pageState: readiness.state,
592
+ readyAfterMs: readiness.waitedMs,
593
+ ...(finalUrl === undefined ? {} : { finalUrl }),
594
+ ...(measured === undefined ? {} : { contentHeight: measured }),
595
+ ...(measured !== undefined && measured > maxCaptureHeight ? { fullPageTruncated: true } : {}),
596
+ ...(layout === undefined ? {} : { layout }),
597
+ ...(actions === undefined ? {} : { actions }),
598
+ ...(runtimeErrors.length > 0 ? { runtimeErrors } : {}),
599
+ ...(flowsSettled === undefined ? {} : { flowsSettled }),
600
+ ...(navigationRetried ? { navigationRetried: true } : {})
601
+ };
95
602
  }
96
603
  finally {
97
604
  await context.close().catch(() => undefined);
605
+ // Zamanlayıcı ÖNCE temizlenir: eskiden her çekim yenisini kurup eskisini bırakıyordu ve
606
+ // kaçan zamanlayıcı sonraki çekimin ortasında tarayıcıyı kapatıyordu (S-3).
607
+ if (this.idleTimer)
608
+ clearTimeout(this.idleTimer);
98
609
  this.idleTimer = setTimeout(() => {
99
610
  const expired = this.browserPromise;
100
611
  this.browserPromise = undefined;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Desen `trace.ts`'in eski kopyasından DARALTILDI: `pass` parçası `passed`/`bypass` gibi
3
+ * anahtarları da yakalıyordu. Günlükte zararsızdı, tool yanıtında veri kaybı olurdu.
4
+ */
5
+ export const secretKeyPattern = /password|passwd|secret|token|authorization|api[-_]?key|credential|connection[-_]?string/i;
6
+ import { looksLikeJwt, rememberToken } from './tokenVault.js';
7
+ const maskedValue = '***';
8
+ const maxPaths = 20;
9
+ const maxDepth = 12;
10
+ function walk(value, path, masked, refs, depth) {
11
+ if (depth > maxDepth)
12
+ return value;
13
+ if (Array.isArray(value))
14
+ return value.map((entry, index) => walk(entry, `${path}[${index}]`, masked, refs, depth + 1));
15
+ if (value && typeof value === 'object') {
16
+ const result = {};
17
+ for (const [key, entry] of Object.entries(value)) {
18
+ const child = path === '' ? key : `${path}.${key}`;
19
+ if (secretKeyPattern.test(key) && entry !== null && entry !== undefined && entry !== '') {
20
+ if (masked.length < maxPaths)
21
+ masked.push(child);
22
+ // Maskelenen değer bir JWT ise kimlik testinin yolu kapanmasın: jeton kasada tutulur ve
23
+ // yanıta yalnız referansı döner (page_screenshot/page_runtime_get asUser onu çözer).
24
+ // Parola ve API anahtarı bu şekle uymadığı için referans almaz.
25
+ if (looksLikeJwt(entry) && Object.keys(refs).length < maxPaths)
26
+ refs[child] = rememberToken(entry);
27
+ result[key] = maskedValue;
28
+ continue;
29
+ }
30
+ result[key] = walk(entry, child, masked, refs, depth + 1);
31
+ }
32
+ return result;
33
+ }
34
+ return value;
35
+ }
36
+ export function maskSecrets(payload) {
37
+ const masked = [];
38
+ const tokenRefs = {};
39
+ return { payload: walk(payload, '', masked, tokenRefs, 0), masked, tokenRefs };
40
+ }