@appaflytech/zeyrek-ai-mcp 0.0.36 → 0.0.40

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 (210) hide show
  1. package/.env.example +15 -0
  2. package/README.md +5 -5
  3. package/dist/config.js +37 -0
  4. package/dist/index.js +2 -0
  5. package/dist/knowledge/access.js +802 -0
  6. package/dist/knowledge/backendConstraints.js +18 -1
  7. package/dist/knowledge/case.js +41 -0
  8. package/dist/knowledge/componentCatalog.js +524 -53
  9. package/dist/knowledge/componentEvents.js +132 -7
  10. package/dist/knowledge/connectDataBind.js +851 -0
  11. package/dist/knowledge/connectDataDefinition.js +344 -0
  12. package/dist/knowledge/connectDataExecute.js +93 -0
  13. package/dist/knowledge/connectDataRules.js +128 -0
  14. package/dist/knowledge/decision.js +121 -0
  15. package/dist/knowledge/expressionPrelude.js +60 -0
  16. package/dist/knowledge/layoutSummary.js +140 -2
  17. package/dist/knowledge/microserviceParameters.js +58 -0
  18. package/dist/knowledge/mining.js +94 -0
  19. package/dist/knowledge/mobileComponentCatalog.js +876 -0
  20. package/dist/knowledge/mobileComponentEvents.js +259 -0
  21. package/dist/knowledge/mobileStore.js +396 -0
  22. package/dist/knowledge/normalize.js +240 -72
  23. package/dist/knowledge/outlines.js +36 -0
  24. package/dist/knowledge/platform/lint.js +385 -0
  25. package/dist/knowledge/platform/mobile.js +142 -0
  26. package/dist/knowledge/platform/mobileLayoutLint.js +324 -0
  27. package/dist/knowledge/platform/mobileRules.js +231 -0
  28. package/dist/knowledge/platform/mobileSpacing.js +82 -0
  29. package/dist/knowledge/platform/mobileTabs.js +108 -0
  30. package/dist/knowledge/platform/nativeBoxModel.js +44 -0
  31. package/dist/knowledge/platform/profile.js +25 -0
  32. package/dist/knowledge/platform/resolve.js +122 -0
  33. package/dist/knowledge/platform/web.js +39 -0
  34. package/dist/knowledge/policy.js +18 -4
  35. package/dist/knowledge/process.js +510 -0
  36. package/dist/knowledge/reservedTables.js +79 -0
  37. package/dist/knowledge/rowFilter.js +43 -0
  38. package/dist/knowledge/rules.js +485 -22
  39. package/dist/knowledge/schemaGuard.js +11 -0
  40. package/dist/knowledge/styleResolve.js +64 -10
  41. package/dist/knowledge/styleResolveNative.js +529 -0
  42. package/dist/knowledge/themeResolve.js +3 -3
  43. package/dist/observability/trace.js +13 -5
  44. package/dist/platform/argumentAliases.js +128 -0
  45. package/dist/platform/bodyFile.js +103 -0
  46. package/dist/platform/capture/actions.js +398 -0
  47. package/dist/platform/capture/browser.js +1 -0
  48. package/dist/platform/capture/capture.js +683 -0
  49. package/dist/platform/capture/console-log.js +122 -0
  50. package/dist/platform/capture/index.js +11 -0
  51. package/dist/platform/capture/readiness.js +103 -0
  52. package/dist/platform/capture/summary.js +77 -0
  53. package/dist/platform/capture/target.js +5 -0
  54. package/dist/platform/capture/targets/studio-mobile.js +15 -0
  55. package/dist/platform/capture/targets/studio-web.js +5 -0
  56. package/dist/platform/diagnostics.js +10 -4
  57. package/dist/platform/domSummary.js +1 -1
  58. package/dist/platform/errors.js +163 -10
  59. package/dist/platform/http.js +29 -8
  60. package/dist/platform/impersonation.js +45 -0
  61. package/dist/platform/jsonArray.js +24 -0
  62. package/dist/platform/responses.js +39 -5
  63. package/dist/platform/schemaReadMemory.js +60 -0
  64. package/dist/platform/secrets.js +68 -2
  65. package/dist/platform/strictArguments.js +68 -12
  66. package/dist/platform/tokenVault.js +187 -4
  67. package/dist/resources/examples/bpm-gorev-kontrol.json +294 -0
  68. package/dist/resources/examples/bpm-iso-dokuman.json +228 -0
  69. package/dist/resources/examples/bpm-satinalma.json +376 -0
  70. package/dist/resources/examples/connect-data-convert-verify.json +107 -0
  71. package/dist/resources/examples/connect-data-dropdown-roles.json +66 -0
  72. package/dist/resources/examples/connect-data-edit-m2m.json +73 -0
  73. package/dist/resources/examples/connect-data-query-params.json +50 -0
  74. package/dist/resources/examples/connect-data-table-entity.json +147 -0
  75. package/dist/resources/examples/connect-data-workflow-microservice.json +65 -0
  76. package/dist/resources/examples/middleware-authenticate.json +1 -1
  77. package/dist/resources/examples/mobile-app-shell-drawer.json +244 -0
  78. package/dist/resources/examples/mobile-app-shell-stack.json +157 -0
  79. package/dist/resources/examples/mobile-app-shell-tab.json +208 -0
  80. package/dist/resources/examples/mobile-form-screen.json +365 -0
  81. package/dist/resources/examples/mobile-list-detail-form.json +703 -0
  82. package/dist/resources/examples/page-detail-profile.json +2 -1
  83. package/dist/resources/examples/page-dynamic-form.json +3 -2
  84. package/dist/resources/examples/page-flex-sections.json +1 -1
  85. package/dist/resources/examples/page-html-embed.json +1 -1
  86. package/dist/resources/examples/workflow-basic.json +8 -2
  87. package/dist/resources/guides/access.md +810 -0
  88. package/dist/resources/guides/bpm.md +691 -0
  89. package/dist/resources/guides/case.md +153 -0
  90. package/dist/resources/guides/component.md +92 -5
  91. package/dist/resources/guides/decision.md +185 -0
  92. package/dist/resources/guides/entity.md +50 -8
  93. package/dist/resources/guides/expressions.md +14 -5
  94. package/dist/resources/guides/functions.md +1 -1
  95. package/dist/resources/guides/getting-started.md +38 -1
  96. package/dist/resources/guides/layout.md +11 -7
  97. package/dist/resources/guides/merge.md +42 -7
  98. package/dist/resources/guides/microservice.md +88 -3
  99. package/dist/resources/guides/middleware.md +69 -5
  100. package/dist/resources/guides/mobile-layout.md +193 -0
  101. package/dist/resources/guides/mobile-publish.md +148 -0
  102. package/dist/resources/guides/mobile.md +565 -0
  103. package/dist/resources/guides/page.md +469 -82
  104. package/dist/resources/guides/theme.md +8 -2
  105. package/dist/resources/guides/translation.md +1 -1
  106. package/dist/resources/guides/workflow.md +58 -1
  107. package/dist/resources/read.js +41 -0
  108. package/dist/resources/rules.js +11 -5
  109. package/dist/resources/schema-fingerprint.json +7 -5
  110. package/dist/resources/schemas/AccessExplanationDto.json +144 -0
  111. package/dist/resources/schemas/AccessMatrixDto.json +170 -0
  112. package/dist/resources/schemas/AccessPermissionDto.json +57 -0
  113. package/dist/resources/schemas/AccessPolicyDto.json +223 -0
  114. package/dist/resources/schemas/AccessResourceDto.json +86 -0
  115. package/dist/resources/schemas/AppSessionDto.json +53 -0
  116. package/dist/resources/schemas/ConnectDataDescribeDto.json +344 -0
  117. package/dist/resources/schemas/ConnectDataRequestModel.json +202 -0
  118. package/dist/resources/schemas/ConnectDataResultDto.json +421 -0
  119. package/dist/resources/schemas/ConnectorCredentialDto.json +70 -0
  120. package/dist/resources/schemas/ConvertConnectDataToMicroserviceCommand.json +37 -0
  121. package/dist/resources/schemas/CreateAccessPolicyCommand.json +209 -0
  122. package/dist/resources/schemas/CreateDecisionCommand.json +221 -0
  123. package/dist/resources/schemas/CreateEntityCommand.json +118 -0
  124. package/dist/resources/schemas/CreateMicroserviceCommand.json +13 -1
  125. package/dist/resources/schemas/CreateProcessDefinitionCommand.json +1337 -0
  126. package/dist/resources/schemas/CreateQueryCommand.json +4 -0
  127. package/dist/resources/schemas/CreateWorkflowCommand.json +146 -1
  128. package/dist/resources/schemas/DataJobDto.json +6 -1
  129. package/dist/resources/schemas/DecisionDto.json +302 -0
  130. package/dist/resources/schemas/DetachConnectDataMicroserviceCommand.json +28 -0
  131. package/dist/resources/schemas/ExplainAccessQuery.json +33 -0
  132. package/dist/resources/schemas/GenerateProcessDocumentCommand.json +116 -0
  133. package/dist/resources/schemas/GlobalFunctionUpdateResultDto.json +24 -0
  134. package/dist/resources/schemas/MicroserviceDto.json +15 -3
  135. package/dist/resources/schemas/OrganizationProjectDto.json +343 -0
  136. package/dist/resources/schemas/OrganizationSchemaDto.json +196 -0
  137. package/dist/resources/schemas/OrganizationSchemaVersionDto.json +207 -0
  138. package/dist/resources/schemas/PatchProcessDefinitionCommand.json +76 -0
  139. package/dist/resources/schemas/PreviewMicroserviceCommand.json +10 -1
  140. package/dist/resources/schemas/PreviewWorkflowCommand.json +25 -1
  141. package/dist/resources/schemas/ProcessDefinitionDto.json +1480 -0
  142. package/dist/resources/schemas/ProcessSimulationDto.json +96 -0
  143. package/dist/resources/schemas/SaveConnectorCredentialCommand.json +90 -0
  144. package/dist/resources/schemas/SaveProcessEventSourceCommand.json +61 -0
  145. package/dist/resources/schemas/SaveProcessMailSourceCommand.json +89 -0
  146. package/dist/resources/schemas/SignProcessTaskCommand.json +73 -0
  147. package/dist/resources/schemas/SimulateProcessDefinitionQuery.json +92 -0
  148. package/dist/resources/schemas/StartMobileBuildCommand.json +54 -0
  149. package/dist/resources/schemas/StepDto.json +25 -1
  150. package/dist/resources/schemas/UpdateAccessPolicyCommand.json +209 -0
  151. package/dist/resources/schemas/UpdateDecisionCommand.json +216 -0
  152. package/dist/resources/schemas/UpdateEntityCommand.json +118 -0
  153. package/dist/resources/schemas/UpdateMicroserviceCommand.json +13 -1
  154. package/dist/resources/schemas/UpdateProcessDefinitionCommand.json +1357 -0
  155. package/dist/resources/schemas/UpdateQueryCommand.json +4 -0
  156. package/dist/resources/schemas/UpdateViewCommand.json +0 -2
  157. package/dist/resources/schemas/UpdateWorkflowCommand.json +146 -1
  158. package/dist/resources/schemas/UpsertMobileStoreSettingsCommand.json +333 -0
  159. package/dist/resources/schemas/ValidateProcessDefinitionQuery.json +1339 -0
  160. package/dist/server.js +31 -1
  161. package/dist/tools/access.js +369 -2
  162. package/dist/tools/accessOperations.js +135 -0
  163. package/dist/tools/accessPolicies.js +170 -0
  164. package/dist/tools/accessRuntime.js +203 -0
  165. package/dist/tools/applications.js +63 -3
  166. package/dist/tools/bpm.js +343 -0
  167. package/dist/tools/bpmGovernance.js +266 -0
  168. package/dist/tools/bpmOperations.js +230 -0
  169. package/dist/tools/bpmReporting.js +154 -0
  170. package/dist/tools/bpmRouting.js +150 -0
  171. package/dist/tools/bpmRuntime.js +396 -0
  172. package/dist/tools/case.js +178 -0
  173. package/dist/tools/caseRuntime.js +89 -0
  174. package/dist/tools/common.js +19 -6
  175. package/dist/tools/components.js +118 -14
  176. package/dist/tools/connectData.js +450 -0
  177. package/dist/tools/connectors.js +193 -0
  178. package/dist/tools/datasources.js +3 -3
  179. package/dist/tools/decisions.js +206 -0
  180. package/dist/tools/dependencies.js +1 -1
  181. package/dist/tools/deployments.js +38 -3
  182. package/dist/tools/designSchemaPatch.js +34 -9
  183. package/dist/tools/dtos.js +36 -5
  184. package/dist/tools/entities.js +79 -12
  185. package/dist/tools/environments.js +1 -1
  186. package/dist/tools/externalComponents.js +26 -5
  187. package/dist/tools/flows.js +119 -13
  188. package/dist/tools/folders.js +1 -1
  189. package/dist/tools/imports.js +5 -2
  190. package/dist/tools/layouts.js +50 -17
  191. package/dist/tools/merges.js +2 -2
  192. package/dist/tools/microservices.js +77 -25
  193. package/dist/tools/mobile.js +280 -0
  194. package/dist/tools/mobileStore.js +572 -0
  195. package/dist/tools/modules.js +48 -0
  196. package/dist/tools/organizationStructure.js +137 -0
  197. package/dist/tools/pages.js +467 -100
  198. package/dist/tools/platformInfo.js +28 -8
  199. package/dist/tools/profileWrite.js +92 -0
  200. package/dist/tools/projects.js +71 -9
  201. package/dist/tools/rows.js +69 -13
  202. package/dist/tools/schemaOps.js +61 -1
  203. package/dist/tools/screenWrite.js +36 -7
  204. package/dist/tools/screens.js +95 -5
  205. package/dist/tools/templates.js +26 -1
  206. package/dist/tools/translations.js +1 -1
  207. package/dist/tools/variables.js +11 -5
  208. package/dist/tools/workflows.js +10 -5
  209. package/package.json +2 -2
  210. package/dist/platform/screenshot.js +0 -1162
@@ -0,0 +1,802 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { applyPolicyMode } from './policy.js';
5
+ /**
6
+ * Platformun kendisinin yönettiği hazır entity'ler (`Defaults.SystemManagedEntityIds`,
7
+ * EntityConstants.cs). Genel satır yazma yolu (`entities/{id}/row*`, table kaynaklı uç, table
8
+ * step) bunlara `400 SystemEntityWriteNotAllowed` döner: organizasyon ağacı senkron saga'sıyla ve
9
+ * Studio'nun `organization/*` uçlarıyla, policy kataloğu `access/*` uçlarıyla yazılır. Id'ler
10
+ * her kurulumda aynı sabitlerdir; slug'lar entity kaydının adıdır.
11
+ */
12
+ export const organizationStructureEntities = {
13
+ organization_units: '01a0a05e-1c9a-7022-a0c7-6b8320756a4c',
14
+ organization_unit_closure: '01a0a05e-1ca3-7b6a-9f1b-7f1633f7f05a',
15
+ organization_positions: '01a0a05e-1ca3-700f-95c6-18fe1e30079c',
16
+ organization_position_closure: '01a0a05e-1ca4-7b73-b90e-4aa7d8eeeeef',
17
+ user_positions: '01a0a05e-1ca4-7841-b6b3-45c5892d5501'
18
+ };
19
+ export const accessCatalogEntities = {
20
+ access_permissions: '01a0a05e-1ca5-73f1-ace1-2550ecc3713a',
21
+ access_resources: '01a0a05e-1ca5-7b96-a910-47c2132c31cf',
22
+ access_policies: '01a0a05e-1ca5-7d16-9448-01ecff8cd827',
23
+ access_grants: '01a0a05e-1ca5-77c9-ba35-273920b57527',
24
+ access_assignments: '01a0a05e-1ca6-75a7-9479-3c522be60ef9'
25
+ };
26
+ /**
27
+ * Yerleşik datasource'un ağaçtaki öneki: `DatasourceTreeDto.Slug` datasource'un ADIDIR ve seed onu
28
+ * "Built-in Datasource" diye açar (id her kurulumda `DatasourceDefaults.BuiltInDatasourceId`). Önek
29
+ * harf ve rakam dışındaki karakterleri atılarak karşılaştırılır: 'BuiltIn', 'built-in', 'Built-in Datasource'.
30
+ */
31
+ const builtInDatasourcePrefixes = new Set(['builtin', 'builtindatasource']);
32
+ /**
33
+ * `entityId` üç biçimde gelir: GUID, slug ya da `datasource/slug`. Backend yalnız entity id'sine bakar
34
+ * (`SystemManagedEntityIds.Contains(entityDto.Id)`): dış datasource'taki aynı adlı tablo
35
+ * (`ik/user_positions`) serbesttir. Bu yüzden GUID her zaman, slug yalnız öneksizse ya da önek yerleşik
36
+ * datasource'sa eşleşir; başka adla yazılmış önek çözümden sonra id'yle denetlenir (`rows.ts`). Eşleşme
37
+ * istek gitmeden yapılır ve büyük/küçük harf duyarsızdır.
38
+ */
39
+ export function systemManagedEntity(ref) {
40
+ if (typeof ref !== 'string' || ref.trim() === '')
41
+ return undefined;
42
+ const value = ref.trim().toLowerCase();
43
+ const separator = value.indexOf('/');
44
+ const slug = separator === -1 ? value : value.slice(separator + 1);
45
+ const slugApplies = separator === -1 || builtInDatasourcePrefixes.has(value.slice(0, separator).replace(/[^a-z0-9]/g, ''));
46
+ for (const [group, entities] of [['organization', organizationStructureEntities], ['access', accessCatalogEntities]]) {
47
+ for (const [entitySlug, id] of Object.entries(entities)) {
48
+ if (id === value || (slugApplies && entitySlug === slug))
49
+ return { slug: entitySlug, group };
50
+ }
51
+ }
52
+ return undefined;
53
+ }
54
+ /**
55
+ * İzin kataloğu (`AccessPermissionConstants`, b-22 — 43 slug; BPM b-01 beşini ekledi, 2026-09-24'te
56
+ * `automation.trigger` çıktı: otomasyon yetki kataloğunda değil — K-2). `supportsScope` altı entity
57
+ * eyleminde ve `process.view-all`'da, `supportsFieldAccess` yalnız `field.read`'te. Katalog yalnız kompleks projede
58
+ * tohumlanır; kaynağı `access_permission_list`'tir, bu kopya istek gitmeden denetim içindir.
59
+ */
60
+ export const accessPermissionSlugs = [
61
+ 'entity.read', 'entity.create', 'entity.update', 'entity.delete', 'entity.export', 'entity.import', 'entity.reveal', 'entity.reassign', 'entity.audit',
62
+ 'field.read', 'field.write', 'field.filter',
63
+ 'row.read', 'row.update', 'row.delete', 'row.export', 'row.reveal',
64
+ 'microservice.execute', 'page.view', 'workflow.execute', 'ui-flow.run', 'function.execute', 'microfunction.execute',
65
+ 'query.execute', 'query.unrestricted', 'view.read',
66
+ 'file.read', 'file.upload', 'file.delete', 'folder.read', 'folder.upload', 'folder.delete',
67
+ 'organization-unit.read', 'organization-unit.manage', 'organization-unit.assign',
68
+ 'system.access.manage', 'system.access.explain', 'system.user.read', 'system.user.manage',
69
+ 'process.start', 'process.manage', 'process.view-all', 'task.complete', 'task.reassign'
70
+ ];
71
+ export const scopedPermissions = new Set(['entity.read', 'entity.update', 'entity.delete', 'entity.export', 'entity.reveal', 'entity.reassign', 'process.view-all']);
72
+ export const accessResourceTypes = ['entity', 'field', 'row', 'microservice', 'page', 'workflow', 'automation', 'ui-flow', 'function', 'microfunction', 'query', 'view', 'file', 'folder', 'organization-unit', 'system', 'process', 'task'];
73
+ export const accessScopes = ['own', 'unit', 'unit-tree', 'subordinates', 'function', 'company', 'all'];
74
+ const knownPermissions = new Set(accessPermissionSlugs);
75
+ const conditionSubjects = new Set(['userId', 'unitIds', 'positionIds', 'companyIds', 'roleSlugs', 'functionCodes']);
76
+ const equalityOperators = new Set(['eq', 'neq']);
77
+ const membershipOperators = new Set(['in', 'notin']);
78
+ const comparisonOperators = new Set(['lt', 'lte', 'gt', 'gte']);
79
+ const nullnessOperators = new Set(['isnull', 'isnotnull']);
80
+ const maxConditionDepth = 5;
81
+ const maxConditionNodes = 50;
82
+ function asObject(value) {
83
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
84
+ }
85
+ function asList(value) {
86
+ return Array.isArray(value) ? value : [];
87
+ }
88
+ function hasValue(value) {
89
+ return value !== undefined && value !== null && value !== '';
90
+ }
91
+ /** `ConditionNodeValidator` aynası (b-26): derinlik ≤ 5, düğüm ≤ 50, işleç ↔ değer uyumu. */
92
+ function conditionProblems(node, path, depth, counter, into) {
93
+ const record = asObject(node);
94
+ if (!record) {
95
+ into.push(`${path}: boş ya da nesne değil`);
96
+ return;
97
+ }
98
+ if (depth > maxConditionDepth) {
99
+ into.push(`${path}: derinlik ${maxConditionDepth}'i aşıyor`);
100
+ return;
101
+ }
102
+ if (++counter.nodes > maxConditionNodes) {
103
+ into.push(`${path}: düğüm sayısı ${maxConditionNodes}'yi aşıyor`);
104
+ return;
105
+ }
106
+ const isGroup = record.all !== undefined || record.any !== undefined;
107
+ if (record.all !== undefined && record.any !== undefined)
108
+ into.push(`${path}: 'all' ve 'any' birlikte`);
109
+ if (isGroup) {
110
+ if (hasValue(record.field) || hasValue(record.op) || record.value !== undefined)
111
+ into.push(`${path}: grup düğümü field/op/value taşıyamaz`);
112
+ const groupName = record.all !== undefined ? 'all' : 'any';
113
+ const children = asList(record[groupName]);
114
+ if (children.length === 0)
115
+ into.push(`${path}.${groupName}: boş grup`);
116
+ children.forEach((child, index) => conditionProblems(child, `${path}.${groupName}[${index}]`, depth + 1, counter, into));
117
+ return;
118
+ }
119
+ if (typeof record.field !== 'string' || record.field.trim() === '')
120
+ into.push(`${path}: 'field' eksik`);
121
+ const op = typeof record.op === 'string' ? record.op : '';
122
+ if (!equalityOperators.has(op) && !membershipOperators.has(op) && !comparisonOperators.has(op) && !nullnessOperators.has(op)) {
123
+ into.push(`${path}: bilinmeyen işleç '${String(record.op)}' (eq · neq · in · notin · lt · lte · gt · gte · isnull · isnotnull)`);
124
+ return;
125
+ }
126
+ const value = record.value;
127
+ const valueObject = asObject(value);
128
+ let fits;
129
+ if (value === undefined || value === null) {
130
+ fits = nullnessOperators.has(op);
131
+ }
132
+ else if (Array.isArray(value)) {
133
+ fits = membershipOperators.has(op) && value.length > 0 && value.every(item => item === null || typeof item !== 'object');
134
+ }
135
+ else if (valueObject && Object.keys(valueObject).length === 1 && typeof valueObject.subject === 'string') {
136
+ const subject = valueObject.subject;
137
+ fits = conditionSubjects.has(subject) && (membershipOperators.has(op) ? subject !== 'userId' : equalityOperators.has(op) && subject === 'userId');
138
+ }
139
+ else if (valueObject && Object.keys(valueObject).length === 1 && valueObject.now === true) {
140
+ fits = equalityOperators.has(op) || comparisonOperators.has(op);
141
+ }
142
+ else if (valueObject) {
143
+ fits = false;
144
+ }
145
+ else {
146
+ fits = equalityOperators.has(op) || comparisonOperators.has(op);
147
+ }
148
+ if (!fits)
149
+ into.push(`${path}: değer '${op}' işlecine uymuyor (liste → in/notin; { subject: 'userId' } → eq/neq, diğer özne değişkenleri → in/notin; { now: true } → eq/neq/lt…; değersiz → isnull/isnotnull)`);
150
+ }
151
+ function conditionFields(node, into) {
152
+ const record = asObject(node);
153
+ if (!record)
154
+ return;
155
+ asList(record.all).forEach(child => conditionFields(child, into));
156
+ asList(record.any).forEach(child => conditionFields(child, into));
157
+ if (typeof record.field === 'string' && record.field !== '')
158
+ into.add(record.field);
159
+ }
160
+ function grantLabel(grant, index) {
161
+ return `grants[${index}] (${String(grant.permission ?? '?')}${hasValue(grant.resourceKey) ? ` @ ${String(grant.resourceKey)}` : ''})`;
162
+ }
163
+ /**
164
+ * K-31 anahtarları: `entity:<entityId>` · `field:<entityId>:<fieldId>`; diğer tipler `<tip>:<id>`.
165
+ * `AccessResource.ReadEntityId` aynası — entity ve alan anahtarından entity id'si okunur.
166
+ */
167
+ export function entityIdOfResourceKey(key) {
168
+ if (typeof key !== 'string')
169
+ return undefined;
170
+ const segments = key.split(':');
171
+ if (segments.length < 2 || (segments[0] !== 'entity' && segments[0] !== 'field'))
172
+ return undefined;
173
+ return segments[1];
174
+ }
175
+ function grantShape(grants) {
176
+ const blocks = [];
177
+ const warnings = [];
178
+ const seen = new Set();
179
+ grants.forEach((entry, index) => {
180
+ const grant = asObject(entry);
181
+ if (!grant) {
182
+ blocks.push(`grants[${index}]: nesne değil`);
183
+ return;
184
+ }
185
+ const label = grantLabel(grant, index);
186
+ const permission = typeof grant.permission === 'string' ? grant.permission : '';
187
+ const known = knownPermissions.has(permission);
188
+ const scope = hasValue(grant.scope) ? String(grant.scope) : undefined;
189
+ const hasCondition = grant.condition !== undefined && grant.condition !== null;
190
+ if (!known)
191
+ warnings.push(`${label}: '${permission}' gateway'in izin kataloğunda yok — access_permission_list ile doğrulayın (platform bilinmeyen izne 400 AccessPermissionUnknown döner)`);
192
+ if (hasValue(grant.resourceKey) && permission !== '') {
193
+ const type = permission.slice(0, permission.indexOf('.') === -1 ? permission.length : permission.indexOf('.'));
194
+ const key = String(grant.resourceKey);
195
+ if (key !== type && !key.startsWith(`${type}:`))
196
+ blocks.push(`${label}: kaynak anahtarı izin tipiyle uyuşmuyor — '${permission}' için anahtar '${type}:…' olmalı (AccessResourceTypeMismatch)`);
197
+ }
198
+ if (grant.effect === 'deny' && hasValue(grant.fieldAccess)) {
199
+ blocks.push(`${label}: reddet (deny) grant'i alan erişimi taşımaz (AccessDenyFieldAccessNotSupported); kapsam ve koşul taşıyabilir, o zaman eylemi kesmez, yalnız kapsamındaki satırları süzer (K-10); koşulsuz scope: 'all' ise kapsamsız sayılır ve eylemi keser (ACC-042)`);
200
+ }
201
+ if (hasCondition && scope === undefined)
202
+ blocks.push(`${label}: koşul yalnız kapsamla birlikte yazılır, kapsama AND ile eklenir; saf koşul için scope: 'all' (K-11, AccessConditionRequiresScope)`);
203
+ if (hasValue(grant.subordinatesLine) && scope !== 'subordinates')
204
+ blocks.push(`${label}: subordinatesLine yalnız scope: 'subordinates' ile (K-33)`);
205
+ if (scope !== undefined && known && !scopedPermissions.has(permission)) {
206
+ blocks.push(`${label}: '${permission}' kapsam desteklemiyor — kapsam yalnız ${[...scopedPermissions].join(' · ')} izinlerinde (AccessScopeNotSupported)`);
207
+ }
208
+ if (hasValue(grant.fieldAccess) && known && permission !== 'field.read')
209
+ blocks.push(`${label}: fieldAccess yalnız 'field.read' izninde (AccessFieldAccessNotSupported)`);
210
+ if (hasCondition && !hasValue(grant.resourceKey))
211
+ blocks.push(`${label}: koşul alanları grant'in entity/alan anahtarından çözülür — anahtarsız (tipin tamamı) grant'te her koşul alanı 'unknown field' olur (AccessConditionInvalidException)`);
212
+ if (hasCondition) {
213
+ const problems = [];
214
+ conditionProblems(grant.condition, 'condition', 1, { nodes: 0 }, problems);
215
+ blocks.push(...problems.map(problem => `${label}: ${problem} (AccessConditionInvalidException)`));
216
+ }
217
+ const identity = `${permission}|${String(grant.resourceKey ?? '')}|${String(grant.effect ?? '')}`;
218
+ if (seen.has(identity))
219
+ blocks.push(`${label}: aynı izin + anahtar + etki ikinci kez yazılmış — platform tekrarlı grant'e 409 (DuplicateException) döner; tek grant'te birleştirin`);
220
+ seen.add(identity);
221
+ });
222
+ return { blocks, warnings };
223
+ }
224
+ function assignmentShape(assignments) {
225
+ const problems = [];
226
+ const seen = new Set();
227
+ assignments.forEach((entry, index) => {
228
+ const assignment = asObject(entry);
229
+ if (!assignment)
230
+ return;
231
+ const label = `assignments[${index}] (${String(assignment.subjectType ?? '?')} ${String(assignment.subjectId ?? '?')})`;
232
+ if (assignment.includeDescendants === true && assignment.subjectType !== 'unit')
233
+ problems.push(`${label}: includeDescendants yalnız subjectType: 'unit' ile (AccessAssignmentDescendantsRequiresUnit)`);
234
+ if (typeof assignment.startsAt === 'string' && typeof assignment.endsAt === 'string' && Date.parse(assignment.endsAt) <= Date.parse(assignment.startsAt)) {
235
+ problems.push(`${label}: endsAt startsAt'ten sonra olmalı (AccessAssignmentPeriodInvalid)`);
236
+ }
237
+ const identity = `${String(assignment.subjectType)}|${String(assignment.subjectId)}`;
238
+ if (seen.has(identity))
239
+ problems.push(`${label}: aynı özne ikinci kez atanmış — platform 409 döner`);
240
+ seen.add(identity);
241
+ });
242
+ return problems;
243
+ }
244
+ const protectedModes = new Set(['masked', 'encrypted', 'masked-encrypted']);
245
+ const FALLBACK_MASK_TYPES = ['last-n', 'first-n', 'first-last', 'email', 'phone', 'full', 'pattern', 'first-n-last-n'];
246
+ /** Backend `FieldProtectionConstants.PhoneSeparatorCharacters`: telefon maskesinde bu karakterler (ve boşluk) maske karakteri olamaz. */
247
+ const PHONE_SEPARATOR_CHARACTERS = '+-()./';
248
+ /**
249
+ * Maske türleri paketlenmiş şemanın `FieldMaskType` enum'undan (`refresh-schemas`): tür listesi backend'le tek
250
+ * kaynaktan gelir, yeni tür için gateway'de liste güncellenmez (kök docs/adr/0001, `rowFilterEnums` emsali).
251
+ * Türe özgü sayı kuralları PROT-003'te kalır. Dosya okunamazsa (bozuk paket) bugünkü liste.
252
+ */
253
+ export function protectionMaskTypes(schemaPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'resources', 'schemas', 'CreateEntityCommand.json')) {
254
+ try {
255
+ const schemas = JSON.parse(readFileSync(schemaPath, 'utf8')).components?.schemas ?? {};
256
+ const values = Object.entries(schemas).find(([name]) => name.endsWith('.FieldMaskType'))?.[1]?.enum;
257
+ return values && values.length > 0 ? values : FALLBACK_MASK_TYPES;
258
+ }
259
+ catch {
260
+ return FALLBACK_MASK_TYPES;
261
+ }
262
+ }
263
+ const maskTypes = new Set(protectionMaskTypes());
264
+ const derivationTypes = new Set(['slice', 'regex', 'domain']);
265
+ function protectionMode(field) {
266
+ const mode = asObject(field.protection)?.mode;
267
+ return typeof mode === 'string' && protectedModes.has(mode) ? mode : undefined;
268
+ }
269
+ function fieldLabel(field, index) {
270
+ return `fields[${index}] '${String(field.slug ?? field.id ?? '?')}'`;
271
+ }
272
+ function protectionProblems(fields) {
273
+ const problems = [];
274
+ fields.forEach((field, index) => {
275
+ const mode = protectionMode(field);
276
+ if (!mode)
277
+ return;
278
+ const label = fieldLabel(field, index);
279
+ const reasons = [
280
+ ...(field.commonType !== undefined && field.commonType !== 'text' ? [`commonType '${String(field.commonType)}' (yalnız 'text')`] : []),
281
+ ...(field.display === true ? ['display alanı'] : []),
282
+ ...(field.primaryKey === true ? ['primaryKey'] : []),
283
+ ...(field.identity === true ? ['identity'] : []),
284
+ ...(field.isLocalized === true ? ['isLocalized'] : []),
285
+ ...((hasValue(field.relationType) && field.relationType !== 'none') || hasValue(field.relationId) ? ['ilişki alanı'] : [])
286
+ ];
287
+ if (reasons.length > 0)
288
+ problems.push(`${label}: korunamaz — ${reasons.join(', ')} (ProtectionFieldTypeNotSupported / ProtectionDisplayFieldNotAllowed)`);
289
+ const mask = asObject(asObject(field.protection)?.mask);
290
+ if ((mode === 'masked' || mode === 'masked-encrypted') && !mask) {
291
+ problems.push(`${label}: '${mode}' modunda protection.mask zorunlu ({ type: 'last-n', visible: 4 } gibi)`);
292
+ }
293
+ else if (mask) {
294
+ const type = String(mask.type ?? '');
295
+ const visible = typeof mask.visible === 'number' ? mask.visible : 0;
296
+ const visibleEnd = typeof mask.visibleEnd === 'number' ? mask.visibleEnd : 0;
297
+ const maskCharacter = typeof mask.char === 'string' && mask.char !== '' ? mask.char.charAt(0) : '*';
298
+ if (!maskTypes.has(type))
299
+ problems.push(`${label}: mask.type '${type}' geçersiz (${[...maskTypes].join(' · ')})`);
300
+ if ((type === 'last-n' || type === 'first-n' || type === 'first-n-last-n') && visible <= 0)
301
+ problems.push(`${label}: mask.type '${type}' visible > 0 ister`);
302
+ if (type === 'first-n-last-n' && visibleEnd <= 0)
303
+ problems.push(`${label}: mask.type 'first-n-last-n' visibleEnd > 0 ister (visible baştan, visibleEnd sondan görünen karakter sayısı)`);
304
+ if (type === 'pattern' && !hasValue(mask.pattern))
305
+ problems.push(`${label}: mask.type 'pattern' mask.pattern ister ('#' açık karakter)`);
306
+ if (type === 'phone' && (/\s/.test(maskCharacter) || PHONE_SEPARATOR_CHARACTERS.includes(maskCharacter)))
307
+ problems.push(`${label}: mask.type 'phone' mask.char '${maskCharacter}' olamaz — ayırıcı ya da boşluk (${PHONE_SEPARATOR_CHARACTERS}) maske karakteri olursa geri yazılan maske gerçek değerin yerine geçer`);
308
+ }
309
+ });
310
+ return problems;
311
+ }
312
+ function derivationProblems(fields) {
313
+ const blocks = [];
314
+ const warnings = [];
315
+ fields.forEach((field, index) => {
316
+ const hasSource = hasValue(field.derivedFromFieldId);
317
+ const rule = asObject(field.derivationRule);
318
+ if (!hasSource && !rule)
319
+ return;
320
+ const label = fieldLabel(field, index);
321
+ if (hasSource !== Boolean(rule)) {
322
+ blocks.push(`${label}: türetilmiş alan derivedFromFieldId VE derivationRule ister — ikisi birlikte verilir (ProtectionDerivedSourceInvalid)`);
323
+ return;
324
+ }
325
+ if (hasValue(field.id) && field.derivedFromFieldId === field.id)
326
+ blocks.push(`${label}: alan kendisinden türetilemez (ProtectionDerivedSourceInvalid)`);
327
+ if (field.commonType !== undefined && field.commonType !== 'text')
328
+ blocks.push(`${label}: türetilmiş alan yalnız 'text' olabilir`);
329
+ const type = String(rule.type ?? '');
330
+ const start = typeof rule.start === 'number' ? rule.start : 0;
331
+ const length = typeof rule.length === 'number' ? rule.length : 0;
332
+ if (!derivationTypes.has(type))
333
+ blocks.push(`${label}: derivationRule.type '${type}' geçersiz (slice · regex · domain) (ProtectionDerivedRuleInvalid)`);
334
+ if (start < 0)
335
+ blocks.push(`${label}: derivationRule.start negatif olamaz (ProtectionDerivedRuleInvalid)`);
336
+ if (length < 0 || (type === 'slice' && length === 0))
337
+ blocks.push(`${label}: 'slice' kuralı length > 0 ister (ProtectionDerivedRuleInvalid)`);
338
+ if (type === 'regex') {
339
+ if (!hasValue(rule.pattern)) {
340
+ blocks.push(`${label}: 'regex' kuralı pattern ister (ProtectionDerivedRuleInvalid)`);
341
+ }
342
+ else {
343
+ try {
344
+ new RegExp(String(rule.pattern));
345
+ }
346
+ catch {
347
+ warnings.push(`${label}: pattern JavaScript'te derlenmedi; platform .NET Regex'le derler — .NET'e özgü sözdizimi değilse düzeltin (ProtectionDerivedRuleInvalid)`);
348
+ }
349
+ }
350
+ }
351
+ });
352
+ return { blocks, warnings };
353
+ }
354
+ function systemEntityWriteMessage(entity, operation) {
355
+ const verb = operation === 'delete' ? 'row_delete' : operation === 'update' ? 'row_update' : 'row_create';
356
+ const owner = entity.group === 'organization'
357
+ ? "Kompleks projede ağaç organizasyonun yayınlanmış şemasından senkronla gelir (yönetim konsolu → yayın); projeye özel birim/pozisyon ve kullanıcı ataması Studio › Organizasyon ekranından yapılır — MCP'de bu yazma yolu yoktur ve bilinçli olarak açılmadı (yapı yazımı aboneliği kapatır, dal ortamında 409)."
358
+ : "Policy, grant ve atama access_policy_create / access_policy_update ile yazılır; izin kataloğu ve kaynak kataloğu platformun kendisi tarafından tohumlanır.";
359
+ return `'${entity.slug}' platformun yönettiği hazır bir entity'dir; ${verb} ile yazılamaz — platform 400 SystemEntityWriteNotAllowed döner (genel satır uçları, table kaynaklı uç ve table step'te aynı kapı). ${owner} Okumak serbesttir: row_list('${entity.slug}'). Rehber: platform_guide('access') §'Organizasyon ağacı'.`;
360
+ }
361
+ /** ORG-001 bulgusu: kural ham `entityId`'yi, `rows.ts` çözülen id'yi aynı fonksiyonla denetler. */
362
+ export function systemEntityWriteFindings(ref, operation) {
363
+ const entity = systemManagedEntity(ref);
364
+ return entity
365
+ ? [{ ruleId: 'ORG-001', severity: 'block', message: systemEntityWriteMessage(entity, operation), guide: 'ngapps://guides/access' }]
366
+ : [];
367
+ }
368
+ function workflowSteps(definition) {
369
+ return [...asList(definition.steps), ...asList(asObject(definition.automation)?.steps)]
370
+ .map(step => asObject(step))
371
+ .filter((step) => Boolean(step));
372
+ }
373
+ /**
374
+ * Projenin türü ve ortamın zorlama modu (`GET /p/{slug}` → `accessModel`, `environments[].enforcementMode`).
375
+ * Okuma düşerse `undefined`: teşhis alanları susar, çağrının kendisi etkilenmez.
376
+ */
377
+ export async function projectAccessContext(platform, project, environment) {
378
+ try {
379
+ const payload = await platform.org('GET', `/p/${platform.resolveProject(project)}`);
380
+ const data = asObject(payload?.data) ?? {};
381
+ const name = platform.resolveEnvironment(environment);
382
+ const current = asList(data.environments).map(entry => asObject(entry)).find(entry => entry?.name === name);
383
+ return {
384
+ accessModel: typeof data.accessModel === 'string' ? data.accessModel : 'simple',
385
+ ...(typeof current?.enforcementMode === 'string' ? { enforcementMode: current.enforcementMode } : {}),
386
+ environment: name
387
+ };
388
+ }
389
+ catch {
390
+ return undefined;
391
+ }
392
+ }
393
+ const runtimeDenials = {
394
+ AppSessionRequiredException: {
395
+ reason: "oturum yok — kompleks proje + enforce ortamda oturumsuz her microservice ve sayfa isteği 401 döner (anonim uç işareti yok)",
396
+ next: "parolasız kullanıcı gözü için aynı çağrıyı asUserId (users satır id'si; development/dal) ile tekrarlayın; oturum açılmış üye için access_session_create ile giriş yapıp tokenRefs'teki tok_… değerini asUser olarak verin"
397
+ },
398
+ AppSessionInvalidException: {
399
+ reason: 'uygulama jetonu geçersiz, süresi dolmuş (15 dk) ya da iptal edilmiş',
400
+ next: 'access_session_create ile yeniden giriş yapın'
401
+ },
402
+ CsrfTokenMismatchException: {
403
+ reason: "oturum çerezle geldi ve durum değiştiren istekte X-ZY-CSRF başlığı yok ya da yanlış",
404
+ next: "asUser kullanın: jeton Bearer olarak gider ve Bearer'lı istek CSRF istemez"
405
+ },
406
+ ForbiddenException: {
407
+ reason: 'karar motoru izni reddetti',
408
+ next: "access_preview { userId, permission, resourceKey } ile hangi atamanın/grant'in eksik olduğunu görün"
409
+ },
410
+ AccessQueryNotScopedException: {
411
+ reason: "sorgu kaynaklı uç ya da query adımı kapsamlı/korumalı bir tabloya dokunuyor ve sorgu isUnrestricted değil",
412
+ next: "kullanıcıya özel veri için table kaynağına geçin; tüm veriyi özetleyen rapor ise sorguyu isUnrestricted işaretleyin (ACC-005)"
413
+ },
414
+ AccessFieldWriteDeniedException: {
415
+ reason: 'kullanıcının yazma yetkisi olmayan bir alana değer gönderildi',
416
+ next: 'gövdeden alanı çıkarın ya da field.read / entity.update grant\'ini gözden geçirin (access_preview fieldAccess.canWrite)'
417
+ },
418
+ AccessFilterDeniedException: {
419
+ reason: 'kullanıcının filtre yetkisi olmayan (canFilter: false) bir alana filtre yazıldı',
420
+ next: "access_preview fieldAccess.canFilter; korumalı alanda tam eşleşme için field.filter izni"
421
+ },
422
+ AccessRevealDeniedException: {
423
+ reason: "reveal izni yok, proje basit ya da zorlama modu off",
424
+ next: 'entity.reveal grant\'i (kapsamlı) ve enforce/shadow mod'
425
+ }
426
+ };
427
+ /**
428
+ * Microservice ve sayfa yanıtına eklenen `accessDecision`: platform kararı yanıtta ayrı bir alan olarak
429
+ * gelmez, hata kodundan ve ortamın modundan türetilir. Basit projede ve erişimle ilgisiz hatada `undefined`.
430
+ */
431
+ export function accessDecisionOf(context, status, code, message) {
432
+ if (!context || context.accessModel !== 'complex')
433
+ return undefined;
434
+ const mode = context.enforcementMode ?? 'bilinmiyor';
435
+ const denial = typeof code === 'string' ? runtimeDenials[code] : undefined;
436
+ if (denial) {
437
+ // İzin yalnız ForbiddenException'da ve ham platform mesajında TAM sözcük olarak aranır: alt dize eşleşmesi
438
+ // `microfunction.execute` içinde `function.execute` bulur, gateway ipucu metni ise ('entity.reveal grant'i')
439
+ // başka kodlarda yanlış izin yazdırır. Bugünkü backend mesajı slug taşımaz (`ForbiddenException` kaynağı
440
+ // parametresiz yerelleşir); bulunamazsa alan yazılmaz.
441
+ const tokens = code === 'ForbiddenException' && typeof message === 'string' ? message.match(/(?<![\w.-])[a-z][a-z-]*(?:\.[a-z][a-z-]*)+/g) ?? [] : [];
442
+ const permission = tokens.find(token => knownPermissions.has(token));
443
+ return { decision: 'deny', status, code, mode, ...(permission ? { permission } : {}), reason: denial.reason, next: denial.next };
444
+ }
445
+ if (typeof status === 'number' && status >= 400) {
446
+ return status === 404 && mode === 'enforce'
447
+ ? { decision: 'unknown', status, mode, reason: 'kayıt yok ya da çağıranın satır kapsamı dışında — platform kapsam dışı kaydı 404 ile gizler (varlık sızdırmaz)', next: 'access_preview { userId, permission, resourceKey, rowId }' }
448
+ : undefined;
449
+ }
450
+ return {
451
+ decision: mode === 'enforce' ? 'allow' : 'not-enforced',
452
+ status,
453
+ mode,
454
+ reason: mode === 'enforce'
455
+ ? 'karar motoru izin verdi; satırlar kapsamla SQL\'de süzülmüş, alanlar erişim düzeyine göre dönmüş olabilir'
456
+ : mode === 'shadow'
457
+ ? "gölge mod: istek engellenmedi; zorunlu olsaydı reddedilecek ya da süzülecek istekler karar günlüğüne yazılır"
458
+ : "zorlama kapalı: karar motoru devrede değil, kimlik ve yetki uygulamanın kendi middleware'inde"
459
+ };
460
+ }
461
+ /** `AccessResourceRef.TryParse` aynası: tip tek başına ya da tipe göre GUID segmentleri. */
462
+ export function resourceKeyProblem(key) {
463
+ const parts = key.split(':');
464
+ const type = parts[0] ?? '';
465
+ const references = parts.slice(1);
466
+ const guid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
467
+ if (parts.some(part => part.trim() === ''))
468
+ return `'${key}': anahtarda boş segment var`;
469
+ if (!accessResourceTypes.includes(type))
470
+ return `'${key}': bilinmeyen kaynak tipi '${type}' (${accessResourceTypes.join(' · ')})`;
471
+ if (references.length === 0)
472
+ return undefined;
473
+ const valid = type === 'entity'
474
+ ? references.length === 1 && guid.test(references[0])
475
+ : type === 'field'
476
+ ? references.length === 2 && references.every(reference => guid.test(reference))
477
+ : type === 'row'
478
+ ? references.length === 2 && guid.test(references[0])
479
+ : references.every(reference => guid.test(reference));
480
+ return valid ? undefined : `'${key}': biçim geçersiz — 'entity:<entityId>' · 'field:<entityId>:<fieldId>' · 'row:<entityId>:<rowId>' · '<tip>:<id>' ya da yalnız '<tip>' (AccessResourceKeyInvalid)`;
481
+ }
482
+ /**
483
+ * ACC-006: kompleks projede kimliği platform kurar (uygulama oturumu → güvenilir özne → uç yolunda `user`
484
+ * değişkeni). Bağlı middleware'ler zorlama modundan bağımsız her modda koşar (`ExecuteMicroserviceBehavior`
485
+ * ve `ExecutePageBehavior` middleware döngüsünde mod dallanması yok): set-user adımı (`SetUserStep`) ya da
486
+ * middleware yanıtındaki `user` platformun `user`'ını ezer ve yanlış bir ikinci doğruluk kaynağı olur. Rol
487
+ * listesi hiçbir modda karar girdisi değildir — yalnız `triggerSource.roles` → `Authorized(user.roles)`.
488
+ * Projenin türü gövdeden okunamaz; yalnız set-user'lı middleware yazımında okunur.
489
+ */
490
+ export async function identityMiddlewareFindings(platform, project, environment, definition, kind, policyOverride = false) {
491
+ if (platform.config.policyMode === 'off')
492
+ return [];
493
+ if ((kind ?? definition.type) !== 'middleware')
494
+ return [];
495
+ if (!workflowSteps(definition).some(step => step.type === 'set-user'))
496
+ return [];
497
+ const context = await projectAccessContext(platform, project, environment);
498
+ if (context?.accessModel !== 'complex')
499
+ return [];
500
+ return applyPolicyMode(platform.config, [{
501
+ ruleId: 'ACC-006',
502
+ severity: 'warn',
503
+ message: "Kompleks projede kimlik middleware'i gereksizdir ve bağlı kalırsa etkisiz DEĞİLDİR. Platform uygulama oturumunu kendisi yönetir (POST /{proje}/runtime/{app}/auth/sign-in, çerez ZY_App_{proje}) ve uç yolunda 'user' değişkenini (id, email, roles, roles_csv, positions, units) güvenilir özneyle ÖNCEDEN doldurur. Uca ya da sayfaya bağlı middleware'ler zorlama modundan bağımsız HER MODDA koşar ve reddedebilir; set-user adımı ya da middleware yanıtındaki 'user' platformun 'user'ını EZER — karar motoru özneyi oturumdan okur, ama ifadeler artık ikinci bir kimliği görür. Kimlik middleware'ini uçların ve sayfaların middlewareIds'inden çıkarın. Yetki erişim politikasıyla verilir: platform microservice.roles / page.roleIds rol listesini hiçbir modda karar için kullanmaz (liste yalnız Authorized(user.roles) çağıran middleware'de okunur). Ayar setter'ı gibi kimlik dışı middleware'ler serbesttir.",
504
+ guide: 'ngapps://guides/access'
505
+ }], policyOverride);
506
+ }
507
+ /**
508
+ * ACC-008: kompleks projede eski rol listeleri (`page.roleIds`, `microservice.roles`) karar girdisi
509
+ * DEĞİLDİR — `enforce`'ta tek kapı PDP'dir (K-29). Liste yalnız `triggerSource.roles`'a geçer ve
510
+ * `Authorized(user.roles)` çağıran bir middleware okur; dolu bırakmak ikinci bir doğruluk kaynağı
511
+ * görüntüsü verir. Block değil warn: `off` ve `shadow` modunda eski davranış sürer, yani liste hâlâ
512
+ * anlamlı olabilir. Studio karşılığı `showsLegacyRoleSelector` — seçici kompleks projede çizilmez.
513
+ *
514
+ * Gövdede rol listesi yoksa proje türü hiç sorulmaz; tur maliyeti yalnız dolu listede ödenir.
515
+ */
516
+ export async function legacyRoleListFindings(platform, project, environment, definition, surface, policyOverride = false) {
517
+ if (platform.config.policyMode === 'off')
518
+ return [];
519
+ const roleIds = definition.roleIds ?? definition.roles;
520
+ if (!Array.isArray(roleIds) || roleIds.length === 0)
521
+ return [];
522
+ const context = await projectAccessContext(platform, project, environment);
523
+ if (context?.accessModel !== 'complex')
524
+ return [];
525
+ const field = surface === 'page' ? 'page.roleIds' : 'microservice.roles';
526
+ return applyPolicyMode(platform.config, [{
527
+ ruleId: 'ACC-008',
528
+ severity: 'warn',
529
+ message: `Kompleks projede '${field}' rol listesi KARAR GİRDİSİ DEĞİLDİR: zorlama açıkken (enforce) tek kapı karar motorudur ve platform bu listeyi hiçbir modda yetki için okumaz (K-29). Liste yalnız triggerSource.roles'a geçer, yani ona bir anlam veren tek şey Authorized(user.roles) çağıran bir middleware'dir — o da yoksa ${roleIds.length} rol sessizce etkisizdir ve bakan kişiye ikinci bir doğruluk kaynağı gibi görünür. Yetkiyi erişim politikasıyla verin: access_policy_create / access_matrix_save ile '${surface === 'page' ? 'page.view' : 'microservice.execute'}' iznini tanımlayın, atamayı access_matrix_save ile role/pozisyona bağlayın. Listeyi boşaltın (roleIds: []) ya da bilinçli tutuyorsanız bu uyarıyı yok sayın — 'off' ve 'shadow' modunda eski davranış sürüyor. Studio bu projede rol seçicisini zaten çizmez. Reçete: platform_guide('access') §Eski rol listeleri.`,
530
+ guide: 'ngapps://guides/access'
531
+ }], policyOverride);
532
+ }
533
+ /** PROT-003'ün gövdesi; `entity_protection_update` tek alan için aynı denetimi kullanır. */
534
+ export function fieldProtectionFindings(fields) {
535
+ const problems = protectionProblems(fields.map(field => asObject(field) ?? {}));
536
+ return problems.length > 0
537
+ ? [{ ruleId: 'PROT-003', severity: 'block', message: `Alan koruması bu alana uygulanamaz:\n · ${problems.join('\n · ')}\nKoruma yalnız düz METİN alana (display, birincil anahtar, identity, çok dilli ve ilişki alanı olmayan) konur.`, guide: 'ngapps://guides/access' }]
538
+ : [];
539
+ }
540
+ /**
541
+ * `FieldProtection.HasSameDefinition` aynası: mod, exactSearch, normalize ve maske. Korumasız alan
542
+ * (`null` ya da `mode: 'none'`) tek imzaya iner; maske karakteri verilmezse platform varsayılanı `*`.
543
+ */
544
+ function protectionSignature(value) {
545
+ const protection = asObject(value);
546
+ const mode = typeof protection?.mode === 'string' ? protection.mode : 'none';
547
+ if (mode === 'none')
548
+ return 'none';
549
+ const mask = asObject(protection?.mask);
550
+ return JSON.stringify([mode, protection?.exactSearch === true, protection?.normalize ?? 'none', mask ? [mask.type ?? null, mask.visible ?? 0, mask.visibleEnd ?? 0, mask.char ?? '*', mask.pattern ?? null] : null]);
551
+ }
552
+ /**
553
+ * Güncelleme yolu VAR OLAN alanın korumasını değiştiremez: `EntityService` gelen alanın
554
+ * `protection`'ı doluysa ve saklananla aynı tanım değilse `ProtectionChangeNotAllowedException`
555
+ * atar — dolu satırlar arka plan işiyle dönüştürülmelidir (`POST …/fields/{fieldId}/protection`).
556
+ * Alan id'siyle eşlenir; gövdede `protection` taşımayan alan platformda da değişmiş sayılmaz.
557
+ */
558
+ export function changedExistingProtections(before, after) {
559
+ const previous = new Map(asList(asObject(before)?.fields)
560
+ .map(field => asObject(field))
561
+ .filter((field) => Boolean(field) && hasValue(field.id))
562
+ .map(field => [String(field.id), field]));
563
+ return asList(asObject(after)?.fields)
564
+ .map(field => asObject(field))
565
+ .filter((field) => Boolean(field) && previous.has(String(field.id)) && field.protection !== undefined && field.protection !== null)
566
+ .filter(field => protectionSignature(previous.get(String(field.id)).protection) !== protectionSignature(field.protection))
567
+ .map(field => String(field.slug ?? field.id));
568
+ }
569
+ export function accessPolicyRules() {
570
+ return [
571
+ {
572
+ id: 'ORG-001',
573
+ module: 'row',
574
+ appliesTo: ['create', 'update', 'delete'],
575
+ sourceRef: "EntityService.cs (CreateRow/UpdateRow/DeleteRow/bulk yolları): Defaults.SystemManagedEntityIds.Contains(entityDto.Id) ⇒ InvalidParameterException(SystemEntityWriteNotAllowed); küme EntityConstants.cs OrganizationStructureEntityIds + AccessEntityIds (b-13, 05 §3.1)",
576
+ check: (definition, context) => systemEntityWriteFindings(definition.entityId, context.operation)
577
+ },
578
+ {
579
+ id: 'ACC-001',
580
+ module: 'accessPolicy',
581
+ appliesTo: ['create', 'update'],
582
+ sourceRef: "AccessGrant ctor (K-10 deny kapsam taşıyabilir, yalnız fieldAccess yasak; K-11 koşul kapsamla, K-33 subordinatesLine) + AccessGrant.EnsureMatchesPermission (AccessScopeNotSupported, AccessFieldAccessNotSupported, AccessResource.EnsureKeyBelongsToType) + ConditionNodeValidator (derinlik 5, düğüm 50, işleç/değer) + AccessAssignment kuralları; izin kataloğu AccessPermissionConstants (b-22)",
583
+ check: definition => {
584
+ const grant = grantShape(asList(definition.grants));
585
+ const assignment = assignmentShape(asList(definition.assignments));
586
+ const blocks = [...grant.blocks, ...assignment];
587
+ return [
588
+ ...(blocks.length > 0 ? [{ ruleId: 'ACC-001', severity: 'block', message: `Policy gövdesi platformun grant/atama kurallarına uymuyor (platform reddeder — biçim hataları 400, tekrarlı grant/atama 409 DuplicateException):\n · ${blocks.join('\n · ')}`, guide: 'ngapps://guides/access' }] : []),
589
+ ...(grant.warnings.length > 0 ? [{ ruleId: 'ACC-001', severity: 'warn', message: grant.warnings.join(' · '), guide: 'ngapps://guides/access' }] : [])
590
+ ];
591
+ }
592
+ },
593
+ {
594
+ id: 'ORG-003',
595
+ module: 'userPosition',
596
+ appliesTo: ['update'],
597
+ sourceRef: "UserPosition ctor/SetPeriod (UserPositionPeriodInvalid) + UserPosition.EnsureSinglePrimary (UserPositionPrimaryDuplicate) + UserPosition.EnsureSeatAvailable (OrganizationPositionSeatOccupied, 409 — tek koltuklu pozisyonda çakışan ikinci KALICI atama; TB-96)",
598
+ check: definition => {
599
+ const positions = asList(definition.positions)
600
+ .map(entry => asObject(entry))
601
+ .filter((position) => position !== undefined && position !== null);
602
+ const problems = [];
603
+ const seen = new Set();
604
+ positions.forEach((position, index) => {
605
+ const endsAt = position.endsAt;
606
+ if (typeof endsAt !== 'string')
607
+ return;
608
+ const startsAt = typeof position.startsAt === 'string' ? Date.parse(position.startsAt) : Date.now();
609
+ if (Date.parse(endsAt) <= startsAt) {
610
+ problems.push(`positions[${index}]: endsAt startsAt'ten sonra olmalı (UserPositionPeriodInvalid) — startsAt verilmezse platform ŞİMDİ uygular`);
611
+ }
612
+ });
613
+ positions.forEach((position, index) => {
614
+ const identity = `${String(position.positionId)}|${String(position.assignmentType ?? 'permanent')}`;
615
+ if (seen.has(identity))
616
+ problems.push(`positions[${index}]: aynı pozisyona aynı türde ikinci atama — platform 409 döner (UserPosition.EnsureSinglePrimary)`);
617
+ seen.add(identity);
618
+ });
619
+ if (positions.filter(position => position.isPrimary === true).length > 1) {
620
+ problems.push('Bir kullanıcının en çok bir birincil ataması olur (UserPositionPrimaryDuplicate)');
621
+ }
622
+ return problems.length === 0
623
+ ? []
624
+ : [{ ruleId: 'ORG-003', severity: 'block', message: `Atama listesi platformun kurallarına uymuyor (istek gönderilmedi):\n · ${problems.join('\n · ')}`, guide: 'ngapps://guides/access' }];
625
+ }
626
+ },
627
+ {
628
+ id: 'ACC-009',
629
+ module: 'accessPolicy',
630
+ appliesTo: ['create', 'update'],
631
+ sourceRef: "AccessAssignment ctor/Override/SetPeriod: OverridesDeny yalnız SubjectType == User ve dolu EndsAt ile (AccessOverrideRequiresUserWithEndDate, 400); yazmak AccessPolicyService.EnsureOverrideAllowed üzerinden access.override.manage izni ister (AccessOverridePermissionRequiredException, 403) — istisnanın öznesini değiştirmek ve aktif istisna taşıyan politikanın grant kümesine dokunmak (ekleme · silme · değiştirme, access_matrix_save dâhil) aynı izni ister; karar motorunda AccessDecisionResolver.HasOverride bütün deny'leri düşürür ve reason 'allowed-by-override' olur (TB-95)",
632
+ check: definition => {
633
+ const overrides = asList(definition.assignments)
634
+ .map((entry, index) => ({ assignment: asObject(entry), index }))
635
+ .filter(entry => entry.assignment?.overridesDeny === true);
636
+ const malformed = overrides.filter(entry => entry.assignment?.subjectType !== 'user' || !hasValue(entry.assignment?.endsAt));
637
+ if (malformed.length > 0) {
638
+ return [{
639
+ ruleId: 'ACC-009',
640
+ severity: 'block',
641
+ message: `overridesDeny yalnız subjectType: 'user' ve dolu endsAt ile yazılır (AccessOverrideRequiresUserWithEndDate, 400 — istek gönderilmedi):\n · ${malformed.map(entry => `assignments[${entry.index}] (${String(entry.assignment?.subjectType ?? '?')} ${String(entry.assignment?.subjectId ?? '?')})`).join('\n · ')}`,
642
+ guide: 'ngapps://guides/access'
643
+ }];
644
+ }
645
+ return overrides.length === 0
646
+ ? []
647
+ : [{
648
+ ruleId: 'ACC-009',
649
+ severity: 'warn',
650
+ message: `Bu istek ${overrides.length} kullanıcı istisnası yazıyor (overridesDeny). İstisna hem kapsamlı hem kapsamsız bütün deny'leri düşürür ve açıklamada reason 'allowed-by-override' olur; yazmak access.override.manage izni ister — 403 alırsanız izni olan hesapla tekrarlayın. İstisna taşıyan politikanın grant'lerine dokunmak da aynı izni ister (access_matrix_save dâhil): istisna o grant kümesi kadar yasak aşar, küme değişince istisnanın sınırı da değişir. Süre dolunca istisna kendiliğinden düşer, bu yüzden endsAt zorunludur.`,
651
+ guide: 'ngapps://guides/access'
652
+ }];
653
+ }
654
+ },
655
+ {
656
+ id: 'ACC-004',
657
+ module: 'workflow',
658
+ appliesTo: ['create', 'update', 'preview'],
659
+ sourceRef: "TableStep.cs / QueryStep.cs: rule.RunAs == StepRunAs.System ⇒ accessEnforcementService.Suppress() — satır kapsamı, alan erişimi ve ham SQL guard'ı atlanır; StepRunAs.TriggeringUser ⇒ RunAsTriggeringUser() — otomasyonda tetikleyen kullanıcının kapsamı geri gelir (AutomationConsumer aktörü çözüp özneye koyar); system adımı denetim kaydı yazmaz (TB-45); runAs StepRuleDto ile taşınır, lint gövdeden okur",
660
+ check: definition => {
661
+ const steps = workflowSteps(definition).filter(step => step.type === 'table' || step.type === 'query');
662
+ const unexplained = steps.filter(step => asObject(step.rule)?.runAs === 'system' && !(typeof step.caution === 'string' && step.caution.trim() !== ''));
663
+ const triggering = steps.filter(step => asObject(step.rule)?.runAs === 'triggering-user');
664
+ const label = (step) => String(step.name ?? step.id ?? step.type);
665
+ return [
666
+ ...(unexplained.length > 0 ? [{ ruleId: 'ACC-004', severity: 'warn', message: `runAs: 'system' adımı gerekçesiz: ${unexplained.map(label).join(', ')}. Bu adım kompleks projede satır kapsamını, alan erişimini ve ham SQL korumasını ATLAR — çağıranın göremediği satırları okur/yazar ve platform bunun için denetim kaydı yazmaz. Gerçekten gerekiyorsa (sayaç, sistem özeti) nedenini adımın 'caution' alanına yazın; kural o zaman susar. Çoğu adım için doğrusu 'inherit'tir (varsayılan).`, guide: 'ngapps://guides/access' }] : []),
667
+ ...(triggering.length > 0 ? [{ ruleId: 'ACC-004', severity: 'warn', message: `runAs: 'triggering-user' adımı tetikleyen kullanıcının kapsamıyla koşar: ${triggering.map(label).join(', ')}. Otomasyon zincirinde aktör çözülemezse (zamanlanmış iş, sistem tetikleyicisi) adım 'inherit' gibi davranır ve system öznesiyle koşar. Ayırım önemliyse platform_guide('access') §'Kısıtsız sorgu ve runAs'.`, guide: 'ngapps://guides/access' }] : [])
668
+ ];
669
+ }
670
+ },
671
+ {
672
+ id: 'ACC-005',
673
+ module: 'query',
674
+ appliesTo: ['create', 'update'],
675
+ sourceRef: "AccessEnforcementService.EnsureQueryScopedAsync(rawQuery, datasourceId, isUnrestricted): işaretsiz sorgu kapsamlı/korumalı entity slug'ını metinde geçirirse 403 AccessQueryNotScoped (sezgisel eşleşme, TB-49); işareti koymak query.unrestricted iznini denetlemez (TB-43), yürütme denetim kaydı yazmaz (TB-45)",
676
+ check: definition => definition.isUnrestricted === true
677
+ ? [{ ruleId: 'ACC-005', severity: 'warn', message: "isUnrestricted: true — bu sorgu kompleks projede satır kapsamını ve alan korumasını HİÇ uygulamadan çalışır: onu çağıran her kullanıcı sahiplik alanlı tablonun bütün satırlarını görür. İşareti koymak şu an bir izin kapısından geçmez ve yürütme denetim kaydı bırakmaz. Yalnız gerçekten tüm veriyi özetlemesi gereken rapor için kullanın; kullanıcıya özel liste table kaynaklı uçla ya da workflow table adımıyla kurulur (kapsam SQL'de uygulanır).", guide: 'ngapps://guides/access' }]
678
+ : []
679
+ },
680
+ {
681
+ id: 'PROT-003',
682
+ module: 'entity',
683
+ appliesTo: ['create', 'update'],
684
+ sourceRef: 'FieldDtoValidator When(Protection.Mode != None): CommonType == Text, Display/PrimaryKey/Identity/IsLocalized false, Mask NotNull (masked*); Field.EnsureProtectable (ilişki alanı); FieldMaskRule ctor (last-n/first-n visible > 0, first-n-last-n visible ve visibleEnd > 0, pattern zorunlu, phone mask.char ayırıcı/boşluk olamaz)',
685
+ check: definition => fieldProtectionFindings(asList(definition.fields))
686
+ },
687
+ {
688
+ id: 'PROT-004',
689
+ module: 'entity',
690
+ appliesTo: ['create', 'update'],
691
+ sourceRef: 'FieldDtoValidator When(DerivedFromFieldId || DerivationRule): ikisi birlikte + CommonType Text; Field.SetDerivation (kendisinden türetme yok); FieldDerivationRule ctor (start ≥ 0, slice length > 0, regex pattern derlenir)',
692
+ check: definition => {
693
+ const { blocks, warnings } = derivationProblems(asList(definition.fields));
694
+ return [
695
+ ...(blocks.length > 0 ? [{ ruleId: 'PROT-004', severity: 'block', message: `Türetilmiş alan kuralı geçersiz:\n · ${blocks.join('\n · ')}`, guide: 'ngapps://guides/access' }] : []),
696
+ ...(warnings.length > 0 ? [{ ruleId: 'PROT-004', severity: 'warn', message: warnings.join(' · '), guide: 'ngapps://guides/access' }] : [])
697
+ ];
698
+ }
699
+ }
700
+ ];
701
+ }
702
+ async function entityShape(resolver, entityRef) {
703
+ try {
704
+ const ref = await resolver.entity(entityRef);
705
+ const record = await resolver.entityRecord(ref);
706
+ const fields = asList(record.fields).map(field => asObject(field) ?? {});
707
+ return {
708
+ slug: ref.slug,
709
+ protectedSlugs: new Set(fields.filter(field => protectionMode(field)).map(field => String(field.slug))),
710
+ conditionSlugs: new Set(fields.filter(field => !protectionMode(field) && field.isSystemField !== true && field.hidden !== true).map(field => String(field.slug))),
711
+ hasOwnerFields: fields.some(field => field.slug === 'sys_owner_user_id'),
712
+ fieldSlugById: new Map(fields.filter(field => typeof field.id === 'string').map(field => [String(field.id), { slug: String(field.slug), isProtected: protectionMode(field) !== undefined }]))
713
+ };
714
+ }
715
+ catch {
716
+ return undefined;
717
+ }
718
+ }
719
+ /**
720
+ * ACC-002 ve ACC-003 gövdeden anlaşılamaz: entity'nin alanlarına (koruma, sahiplik alanı) bakmak
721
+ * gerekir. Gateway yalnız KAPSAMLI ya da KOŞULLU grant'lerin entity'lerini okur; okuma düşerse
722
+ * kural susar — ön denetim bir kolaylıktır, yazmayı kendi arızası yüzünden engellemez.
723
+ */
724
+ export async function accessPolicyEntityFindings(platform, resolver, body) {
725
+ if (platform.config.policyMode === 'off')
726
+ return [];
727
+ const grants = asList(body.grants).map(entry => asObject(entry)).filter((grant) => Boolean(grant));
728
+ const isMaskGrant = (grant) => grant.effect !== 'deny' && grant.permission === 'field.read' && grant.fieldAccess === 'masked' && typeof grant.resourceKey === 'string' && grant.resourceKey.split(':').length === 3;
729
+ const relevant = grants.filter(grant => grant.effect !== 'deny' && ((grant.condition !== undefined && grant.condition !== null) || (hasValue(grant.scope) && grant.scope !== 'all') || isMaskGrant(grant)));
730
+ const shapes = new Map();
731
+ const protectedHits = [];
732
+ const unknownFields = [];
733
+ const withoutOwner = [];
734
+ const unprotectedMasks = [];
735
+ for (const grant of relevant) {
736
+ const entityId = entityIdOfResourceKey(grant.resourceKey);
737
+ if (!entityId)
738
+ continue;
739
+ if (!shapes.has(entityId))
740
+ shapes.set(entityId, await entityShape(resolver, entityId));
741
+ const shape = shapes.get(entityId);
742
+ if (!shape)
743
+ continue;
744
+ const label = `${String(grant.permission)} @ ${String(grant.resourceKey)}`;
745
+ const fields = new Set();
746
+ conditionFields(grant.condition, fields);
747
+ for (const field of fields) {
748
+ if (shape.protectedSlugs.has(field))
749
+ protectedHits.push(`${label}: '${field}' korumalı alan`);
750
+ else if (!shape.conditionSlugs.has(field))
751
+ unknownFields.push(`${label}: '${field}' '${shape.slug}' entity'sinin koşulda kullanılabilir alanı değil`);
752
+ }
753
+ if (hasValue(grant.scope) && grant.scope !== 'all' && !shape.hasOwnerFields)
754
+ withoutOwner.push(`${label}: '${shape.slug}' entity'sinde sys_owner_user_id / sys_owner_unit_id yok (scope '${String(grant.scope)}')`);
755
+ if (isMaskGrant(grant)) {
756
+ const field = shape.fieldSlugById.get(String(grant.resourceKey).split(':')[2] ?? '');
757
+ if (field && !field.isProtected)
758
+ unprotectedMasks.push(`${shape.slug}.${field.slug}`);
759
+ }
760
+ }
761
+ return [
762
+ ...(protectedHits.length > 0 ? [{ ruleId: 'ACC-002', severity: 'block', message: `Koşul korumalı alana bakıyor — korumalı alanın ana kolonunda maske durur, koşul SQL'i düz değeri göremez; platform bu alanı koşul alanı saymaz (AccessConditionInvalidException / AccessConditionFieldProtected): ${protectedHits.join(' · ')}. Korumalı alanı sorgulanabilir kılmanın yolu türetilmiş alandır (derivedFromFieldId + derivationRule).`, guide: 'ngapps://guides/access' }] : []),
763
+ ...(unknownFields.length > 0 ? [{ ruleId: 'ACC-002', severity: 'warn', message: `Koşul alanı kaynak kataloğunda görünmüyor (gizli/sistem alanı ya da yanlış slug; platform 'unknown field' ile reddeder): ${unknownFields.join(' · ')}. Alanları access_resource_list { type: 'field' } ile görün.`, guide: 'ngapps://guides/access' }] : []),
764
+ ...(unprotectedMasks.length > 0 ? [{ ruleId: 'ACC-017', severity: 'warn', message: `field.read 'masked' korumasız alanda: ${unprotectedMasks.join(' · ')}. Maske alanın depolama korumasından gelir — alan entity_protection_update ile korunmadıysa politika 'masked' uygulayacak bir maske bulamaz; 38/M-3'te değer düz döndü, kabiliyet yine access: masked dedi ve işleçlerde contains/sıralama açık kaldı. Önce entity_protection_update { field, protection: { mode: 'masked', mask } } ile alanı koruyun ya da bu kullanıcıdan alanı gizlemek için fieldAccess: 'hidden' verin.`, guide: 'ngapps://guides/access' }] : []),
765
+ ...(withoutOwner.length > 0 ? [{ ruleId: 'ACC-003', severity: 'warn', message: `Sahiplik alanı olmayan entity'ye kapsam yazılıyor — platform 400 AccessScopeRequiresOwnerFields döner: ${withoutOwner.join(' · ')}. Kompleks projede yeni entity alanları kendiliğinden alır; eski entity için owner-fields işi (POST /{proje}/DataJobs/owner-fields/{datasourceId}) ya da scope: 'all' + koşul.`, guide: 'ngapps://guides/access' }] : [])
766
+ ];
767
+ }
768
+ /** Korumalı alanda platformun kabul ettiği işleçler (`FieldProtectionService`): eşitlik, üyelik, boşluk. */
769
+ export const protectedFilterOperators = new Set(['eq', 'neq', 'in', 'notin', 'isnull', 'isnotnull']);
770
+ function collectFilters(filters, into) {
771
+ for (const entry of asList(filters)) {
772
+ const filter = asObject(entry);
773
+ if (!filter)
774
+ continue;
775
+ if (typeof filter.field === 'string' && filter.field !== '')
776
+ into.push({ field: filter.field, operator: String(filter.operator ?? '') });
777
+ collectFilters(filter.children, into);
778
+ }
779
+ }
780
+ /**
781
+ * PROT-001 / PROT-002. Entity yalnız GEREKİRSE okunur: bütün filtreler korumalı alanda da geçerli
782
+ * bir işleçle yazılmışsa ve sıralama yoksa hiçbir tur atılmaz. Platform yalnız kök entity'nin
783
+ * alanını denetler (`slug` ya da `<entitySlug>.slug`, TB-24) — gateway aynı sınırı izler.
784
+ */
785
+ export async function protectedFilterFindings(platform, resolver, entityRef, body) {
786
+ if (platform.config.policyMode === 'off')
787
+ return [];
788
+ const filters = [];
789
+ collectFilters(body.filters, filters);
790
+ const order = typeof body.order === 'string' && body.order.trim() !== '' ? body.order.trim() : undefined;
791
+ if (!order && filters.every(filter => protectedFilterOperators.has(filter.operator)))
792
+ return [];
793
+ const shape = await entityShape(resolver, entityRef);
794
+ if (!shape || shape.protectedSlugs.size === 0)
795
+ return [];
796
+ const local = (field) => field.startsWith(`${shape.slug}.`) ? field.slice(shape.slug.length + 1) : field;
797
+ const badFilters = filters.filter(filter => shape.protectedSlugs.has(local(filter.field)) && !protectedFilterOperators.has(filter.operator));
798
+ return [
799
+ ...(badFilters.length > 0 ? [{ ruleId: 'PROT-001', severity: 'block', message: `Korumalı alanda metin araması/karşılaştırma yapılamaz: ${badFilters.map(filter => `'${filter.field}' ${filter.operator}`).join(', ')}. Ana kolonda maske durur (şifreli modda değer hiç yoktur); platform 400 ProtectionOperatorNotAllowed döner. Geçerli işleçler: eq · neq · in · notin · isnull · isnotnull — şifreli modda eşitlik için alanın protection.exactSearch'ü açık olmalı. Kısmi arama gerekiyorsa türetilmiş alan (ör. telefon → operatör kodu) kurun.`, guide: 'ngapps://guides/access' }] : []),
800
+ ...(order && shape.protectedSlugs.has(local(order)) ? [{ ruleId: 'PROT-002', severity: 'block', message: `Korumalı alana göre sıralanamaz: '${order}' — sıra maskeye göre oluşurdu; platform 400 ProtectionSortNotAllowed döner. Başka bir alana ya da türetilmiş alana göre sıralayın.`, guide: 'ngapps://guides/access' }] : [])
801
+ ];
802
+ }