openxiangda-devkit-core 2.0.0-alpha.100

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 (126) hide show
  1. package/README.md +13 -0
  2. package/dist/app-spec.d.ts +210 -0
  3. package/dist/app-spec.d.ts.map +1 -0
  4. package/dist/app-spec.js +1262 -0
  5. package/dist/app-spec.js.map +1 -0
  6. package/dist/application-services.d.ts +600 -0
  7. package/dist/application-services.d.ts.map +1 -0
  8. package/dist/application-services.js +1603 -0
  9. package/dist/application-services.js.map +1 -0
  10. package/dist/backend-image-build.d.ts +23 -0
  11. package/dist/backend-image-build.d.ts.map +1 -0
  12. package/dist/backend-image-build.js +209 -0
  13. package/dist/backend-image-build.js.map +1 -0
  14. package/dist/command-registry.d.ts +177 -0
  15. package/dist/command-registry.d.ts.map +1 -0
  16. package/dist/command-registry.js +27 -0
  17. package/dist/command-registry.js.map +1 -0
  18. package/dist/compiler/ai-catalog.d.ts +4 -0
  19. package/dist/compiler/ai-catalog.d.ts.map +1 -0
  20. package/dist/compiler/ai-catalog.js +353 -0
  21. package/dist/compiler/ai-catalog.js.map +1 -0
  22. package/dist/compiler/authorization-transition.d.ts +7 -0
  23. package/dist/compiler/authorization-transition.d.ts.map +1 -0
  24. package/dist/compiler/authorization-transition.js +27 -0
  25. package/dist/compiler/authorization-transition.js.map +1 -0
  26. package/dist/compiler/bundle.d.ts +34 -0
  27. package/dist/compiler/bundle.d.ts.map +1 -0
  28. package/dist/compiler/bundle.js +1865 -0
  29. package/dist/compiler/bundle.js.map +1 -0
  30. package/dist/compiler/config.d.ts +857 -0
  31. package/dist/compiler/config.d.ts.map +1 -0
  32. package/dist/compiler/config.js +3848 -0
  33. package/dist/compiler/config.js.map +1 -0
  34. package/dist/compiler/data-policy-expression.d.ts +12 -0
  35. package/dist/compiler/data-policy-expression.d.ts.map +1 -0
  36. package/dist/compiler/data-policy-expression.js +43 -0
  37. package/dist/compiler/data-policy-expression.js.map +1 -0
  38. package/dist/compiler/field-codec.d.ts +14 -0
  39. package/dist/compiler/field-codec.d.ts.map +1 -0
  40. package/dist/compiler/field-codec.js +131 -0
  41. package/dist/compiler/field-codec.js.map +1 -0
  42. package/dist/compiler/field-physical-plan.d.ts +10 -0
  43. package/dist/compiler/field-physical-plan.d.ts.map +1 -0
  44. package/dist/compiler/field-physical-plan.js +66 -0
  45. package/dist/compiler/field-physical-plan.js.map +1 -0
  46. package/dist/compiler/field-policy-path.d.ts +10 -0
  47. package/dist/compiler/field-policy-path.d.ts.map +1 -0
  48. package/dist/compiler/field-policy-path.js +283 -0
  49. package/dist/compiler/field-policy-path.js.map +1 -0
  50. package/dist/compiler/field-query-plan.d.ts +13 -0
  51. package/dist/compiler/field-query-plan.d.ts.map +1 -0
  52. package/dist/compiler/field-query-plan.js +122 -0
  53. package/dist/compiler/field-query-plan.js.map +1 -0
  54. package/dist/compiler/field-surface.d.ts +15 -0
  55. package/dist/compiler/field-surface.d.ts.map +1 -0
  56. package/dist/compiler/field-surface.js +51 -0
  57. package/dist/compiler/field-surface.js.map +1 -0
  58. package/dist/compiler/index.d.ts +6 -0
  59. package/dist/compiler/index.d.ts.map +1 -0
  60. package/dist/compiler/index.js +6 -0
  61. package/dist/compiler/index.js.map +1 -0
  62. package/dist/compiler/package-compiler.d.ts +22 -0
  63. package/dist/compiler/package-compiler.d.ts.map +1 -0
  64. package/dist/compiler/package-compiler.js +246 -0
  65. package/dist/compiler/package-compiler.js.map +1 -0
  66. package/dist/compiler/schema-composition.d.ts +21 -0
  67. package/dist/compiler/schema-composition.d.ts.map +1 -0
  68. package/dist/compiler/schema-composition.js +193 -0
  69. package/dist/compiler/schema-composition.js.map +1 -0
  70. package/dist/config.d.ts +2 -0
  71. package/dist/config.d.ts.map +1 -0
  72. package/dist/config.js +2 -0
  73. package/dist/config.js.map +1 -0
  74. package/dist/connected-development.d.ts +62 -0
  75. package/dist/connected-development.d.ts.map +1 -0
  76. package/dist/connected-development.js +393 -0
  77. package/dist/connected-development.js.map +1 -0
  78. package/dist/control-plane-client.d.ts +684 -0
  79. package/dist/control-plane-client.d.ts.map +1 -0
  80. package/dist/control-plane-client.js +860 -0
  81. package/dist/control-plane-client.js.map +1 -0
  82. package/dist/deployment.d.ts +67 -0
  83. package/dist/deployment.d.ts.map +1 -0
  84. package/dist/deployment.js +311 -0
  85. package/dist/deployment.js.map +1 -0
  86. package/dist/index.d.ts +17 -0
  87. package/dist/index.d.ts.map +1 -0
  88. package/dist/index.js +17 -0
  89. package/dist/index.js.map +1 -0
  90. package/dist/internal/workflow.d.ts +83 -0
  91. package/dist/internal/workflow.d.ts.map +1 -0
  92. package/dist/internal/workflow.js +419 -0
  93. package/dist/internal/workflow.js.map +1 -0
  94. package/dist/nest-injection-contract.d.ts +4 -0
  95. package/dist/nest-injection-contract.d.ts.map +1 -0
  96. package/dist/nest-injection-contract.js +116 -0
  97. package/dist/nest-injection-contract.js.map +1 -0
  98. package/dist/package-compiler.d.ts +2 -0
  99. package/dist/package-compiler.d.ts.map +1 -0
  100. package/dist/package-compiler.js +2 -0
  101. package/dist/package-compiler.js.map +1 -0
  102. package/dist/session.d.ts +143 -0
  103. package/dist/session.d.ts.map +1 -0
  104. package/dist/session.js +351 -0
  105. package/dist/session.js.map +1 -0
  106. package/dist/testing.d.ts +77 -0
  107. package/dist/testing.d.ts.map +1 -0
  108. package/dist/testing.js +213 -0
  109. package/dist/testing.js.map +1 -0
  110. package/dist/toolchain-capsule.d.ts +45 -0
  111. package/dist/toolchain-capsule.d.ts.map +1 -0
  112. package/dist/toolchain-capsule.js +165 -0
  113. package/dist/toolchain-capsule.js.map +1 -0
  114. package/dist/version.d.ts +3 -0
  115. package/dist/version.d.ts.map +1 -0
  116. package/dist/version.js +11 -0
  117. package/dist/version.js.map +1 -0
  118. package/dist/workspace-loader.d.ts +17 -0
  119. package/dist/workspace-loader.d.ts.map +1 -0
  120. package/dist/workspace-loader.js +140 -0
  121. package/dist/workspace-loader.js.map +1 -0
  122. package/dist/workspace.d.ts +15 -0
  123. package/dist/workspace.d.ts.map +1 -0
  124. package/dist/workspace.js +69 -0
  125. package/dist/workspace.js.map +1 -0
  126. package/package.json +44 -0
@@ -0,0 +1,3848 @@
1
+ import { SCHEMA_VERSIONS, DATA_FIELD_TYPES, PLATFORM_EVENT_TYPES_V2, nativePlatformCapabilityCatalog, validateDataResource, WORKFLOW_SUMMARY_MAX_FIELDS, WORKFLOW_SUMMARY_TEXT_LONG_MAX_BYTES, isWorkflowSummaryFieldType, } from 'openxiangda-contracts';
2
+ import { Ajv2020 } from 'ajv/dist/2020.js';
3
+ import { CronExpressionParser } from 'cron-parser';
4
+ import { fieldNullable, supportsGeneratedMutation, } from './field-codec.js';
5
+ import { supportsFilter, supportsSearch, supportsSort, } from './field-query-plan.js';
6
+ import { physicalPlanForField } from './field-physical-plan.js';
7
+ import { SEMANTIC_FIELD_PATH_PATTERN, semanticFieldPathRoot, validateAuthzSemanticBindings, } from './field-policy-path.js';
8
+ import { compatibleWidgets, isCompatibleFieldWidget, resolveDataFieldSurfaceWidget, } from './field-surface.js';
9
+ import { DATA_POLICY_EXPRESSION_MAX_DEPTH, DATA_POLICY_EXPRESSION_MAX_GROUP_ITEMS, DATA_POLICY_EXPRESSION_MAX_LEAVES, dataPolicyExpressionToCnf, } from './data-policy-expression.js';
10
+ import { AUTHORIZATION_TRANSITION_CAPABILITY_CODE_PATTERN, AUTHORIZATION_TRANSITION_KEYS, AUTHORIZATION_TRANSITION_ROLE_CODE_PATTERN, exactKeys, isExactStringArray, isPlainRecord, } from './authorization-transition.js';
11
+ // The compiler owns the application source contract. Devkit and every adapter
12
+ // re-export this module instead of maintaining separate configuration models.
13
+ import { validateWorkflowBinding, validateWorkflowDefinition, } from '../internal/workflow.js';
14
+ const DATE_TRIGGER_RESERVED_DATA_FIELDS = new Set([
15
+ 'triggerCode',
16
+ 'resourceCode',
17
+ 'recordId',
18
+ 'recordRevision',
19
+ 'field',
20
+ 'dueAt',
21
+ ]);
22
+ const APP_DATA_FIELD_TYPES = new Set(DATA_FIELD_TYPES);
23
+ function adminNavigationItem(page, options = {}) {
24
+ return Object.freeze({ page: Object.freeze(page), ...options });
25
+ }
26
+ /** Explicitly owns the complete admin menu; the compiler never appends pages. */
27
+ export function defineAdminNavigation(groups) {
28
+ return Object.freeze(groups.map(group => Object.freeze({
29
+ ...group,
30
+ items: Object.freeze(group.items.map(item => Object.freeze({ ...item }))),
31
+ })));
32
+ }
33
+ export function adminNavigationGroup(code, label, items, options = {}) {
34
+ return Object.freeze({ code, label, items: Object.freeze([...items]), ...options });
35
+ }
36
+ export function adminResourcePage(resourceCode, options) {
37
+ return adminNavigationItem({ kind: 'resource', resourceCode }, options);
38
+ }
39
+ export function adminOperationPage(routeCode, options) {
40
+ return adminNavigationItem({ kind: 'operation', routeCode }, options);
41
+ }
42
+ /**
43
+ * Produces a deterministic one-time authoring suggestion for tooling. The
44
+ * compiler never calls this function and never mutates a saved declaration.
45
+ */
46
+ export function suggestAdminNavigation(config) {
47
+ const groups = [];
48
+ const resources = [...(config.data?.resources || [])]
49
+ .filter(resource => resource.surface?.generated?.list !== false)
50
+ .sort((left, right) => left.code < right.code ? -1 : left.code > right.code ? 1 : 0);
51
+ if (resources.length > 0) {
52
+ groups.push(adminNavigationGroup('data-management', '数据管理', resources.map(resource => adminResourcePage(resource.code, { label: resource.name })), { icon: 'database', order: 100 }));
53
+ }
54
+ const routes = [...(config.frontend.routes || [])]
55
+ .filter(route => route.surface === 'admin' && !/[:*]/.test(route.path))
56
+ .sort((left, right) => left.code < right.code ? -1 : left.code > right.code ? 1 : 0);
57
+ if (routes.length > 0) {
58
+ groups.push(adminNavigationGroup('business-operations', '业务操作', routes.map(route => adminOperationPage(route.code, { label: route.label })), { icon: 'operations', order: 200 }));
59
+ }
60
+ return defineAdminNavigation(groups);
61
+ }
62
+ function renderAdminNavigationOptions(options) {
63
+ const entries = [
64
+ ...(options.label !== undefined
65
+ ? [`label: ${JSON.stringify(options.label)}`]
66
+ : []),
67
+ ...(options.icon !== undefined
68
+ ? [`icon: ${JSON.stringify(options.icon)}`]
69
+ : []),
70
+ ...(options.order !== undefined ? [`order: ${options.order}`] : []),
71
+ ];
72
+ return entries.length > 0 ? `{ ${entries.join(', ')} }` : undefined;
73
+ }
74
+ /**
75
+ * Renders the deterministic proposal as a typed expression that an AI author
76
+ * can copy once into frontend.admin.navigation and then edit normally.
77
+ */
78
+ export function renderAdminNavigationSuggestion(config) {
79
+ const navigation = suggestAdminNavigation(config);
80
+ const imports = new Set(['defineAdminNavigation']);
81
+ const groups = navigation.map(group => {
82
+ imports.add('adminNavigationGroup');
83
+ const items = group.items.map(item => {
84
+ const options = renderAdminNavigationOptions(item);
85
+ let expression;
86
+ switch (item.page.kind) {
87
+ case 'resource':
88
+ imports.add('adminResourcePage');
89
+ expression = `adminResourcePage(${JSON.stringify(item.page.resourceCode)}${options ? `, ${options}` : ''})`;
90
+ break;
91
+ case 'operation':
92
+ imports.add('adminOperationPage');
93
+ expression = `adminOperationPage(${JSON.stringify(item.page.routeCode)}${options ? `, ${options}` : ''})`;
94
+ break;
95
+ }
96
+ return ` ${expression},`;
97
+ });
98
+ const options = renderAdminNavigationOptions({
99
+ icon: group.icon,
100
+ order: group.order,
101
+ });
102
+ return [
103
+ ' adminNavigationGroup(',
104
+ ` ${JSON.stringify(group.code)},`,
105
+ ` ${JSON.stringify(group.label)},`,
106
+ ' [',
107
+ ...items,
108
+ ' ],',
109
+ ...(options ? [` ${options}`] : []),
110
+ ' ),',
111
+ ].join('\n');
112
+ });
113
+ return {
114
+ module: 'openxiangda/config',
115
+ imports: [...imports].sort(),
116
+ proposal: navigation,
117
+ expression: ['defineAdminNavigation([', ...groups, '])'].join('\n'),
118
+ groupCount: navigation.length,
119
+ itemCount: navigation.reduce((count, group) => count + group.items.length, 0),
120
+ };
121
+ }
122
+ const authorizationProjectionFieldPathSchema = {
123
+ type: 'string',
124
+ pattern: '^[A-Za-z_][A-Za-z0-9_]{0,127}(?:\\.(?:value|snapshot\\.[A-Za-z_][A-Za-z0-9_]{0,127}))?$',
125
+ };
126
+ const authorizationProjectionControlProperties = {
127
+ enabledField: {
128
+ type: 'string',
129
+ pattern: '^[A-Za-z_][A-Za-z0-9_]{0,127}$',
130
+ },
131
+ effectiveFromField: {
132
+ type: 'string',
133
+ pattern: '^[A-Za-z_][A-Za-z0-9_]{0,127}$',
134
+ },
135
+ effectiveToField: {
136
+ type: 'string',
137
+ pattern: '^[A-Za-z_][A-Za-z0-9_]{0,127}$',
138
+ },
139
+ failureMode: { const: 'strict' },
140
+ };
141
+ const roleMembershipSourceInputSchema = {
142
+ type: 'object',
143
+ additionalProperties: false,
144
+ required: [
145
+ 'code',
146
+ 'name',
147
+ 'resourceCode',
148
+ 'userIdField',
149
+ 'roleCode',
150
+ 'failureMode',
151
+ ],
152
+ properties: {
153
+ code: {
154
+ type: 'string',
155
+ pattern: '^[a-z][a-z0-9]*(?:[-_.][a-z0-9]+)*$',
156
+ maxLength: 100,
157
+ },
158
+ name: { type: 'string', minLength: 1, maxLength: 255 },
159
+ resourceCode: { type: 'string' },
160
+ userIdField: authorizationProjectionFieldPathSchema,
161
+ roleCode: { type: 'string' },
162
+ ...authorizationProjectionControlProperties,
163
+ },
164
+ };
165
+ const relationshipGrantSourceInputSchema = {
166
+ type: 'object',
167
+ additionalProperties: false,
168
+ required: [
169
+ 'code',
170
+ 'name',
171
+ 'resourceCode',
172
+ 'subject',
173
+ 'relationCode',
174
+ 'targetResourceCode',
175
+ 'resourceIdField',
176
+ 'operations',
177
+ 'failureMode',
178
+ ],
179
+ properties: {
180
+ code: {
181
+ type: 'string',
182
+ pattern: '^[a-z][a-z0-9]*(?:[-_.][a-z0-9]+)*$',
183
+ maxLength: 100,
184
+ },
185
+ name: { type: 'string', minLength: 1, maxLength: 255 },
186
+ resourceCode: { type: 'string' },
187
+ subject: {
188
+ oneOf: [
189
+ {
190
+ type: 'object',
191
+ additionalProperties: false,
192
+ required: ['type', 'userIdField'],
193
+ properties: {
194
+ type: { const: 'user' },
195
+ userIdField: authorizationProjectionFieldPathSchema,
196
+ },
197
+ },
198
+ {
199
+ type: 'object',
200
+ additionalProperties: false,
201
+ required: ['type', 'userIdField', 'roleCode'],
202
+ properties: {
203
+ type: { const: 'role_membership' },
204
+ userIdField: authorizationProjectionFieldPathSchema,
205
+ roleCode: { type: 'string' },
206
+ },
207
+ },
208
+ ],
209
+ },
210
+ relationCode: { type: 'string', minLength: 1, maxLength: 128 },
211
+ targetResourceCode: { type: 'string' },
212
+ resourceIdField: authorizationProjectionFieldPathSchema,
213
+ operations: {
214
+ type: 'array',
215
+ minItems: 1,
216
+ maxItems: 20,
217
+ uniqueItems: true,
218
+ items: {
219
+ type: 'string',
220
+ minLength: 1,
221
+ maxLength: 64,
222
+ pattern: '^[A-Za-z0-9][A-Za-z0-9:._*-]{0,63}$',
223
+ },
224
+ },
225
+ ...authorizationProjectionControlProperties,
226
+ },
227
+ };
228
+ const authorizationTransitionInputSchema = {
229
+ type: 'object',
230
+ additionalProperties: false,
231
+ required: ['fromAuthzDigest', 'reason'],
232
+ properties: {
233
+ fromAuthzDigest: {
234
+ type: 'string',
235
+ pattern: '^[0-9a-f]{64}$',
236
+ },
237
+ removeRoleCodes: {
238
+ type: 'array',
239
+ maxItems: 2000,
240
+ uniqueItems: true,
241
+ items: {
242
+ type: 'string',
243
+ pattern: '^[a-z][a-z0-9]*(?:[-_.][a-z0-9]+)*$',
244
+ maxLength: 128,
245
+ },
246
+ },
247
+ removeCapabilityCodes: {
248
+ type: 'array',
249
+ maxItems: 2000,
250
+ uniqueItems: true,
251
+ items: {
252
+ type: 'string',
253
+ pattern: '^[A-Za-z*][A-Za-z0-9:._*-]{0,254}$',
254
+ maxLength: 255,
255
+ },
256
+ },
257
+ reason: {
258
+ type: 'string',
259
+ minLength: 1,
260
+ maxLength: 2000,
261
+ pattern: '\\S',
262
+ },
263
+ },
264
+ };
265
+ export const openXiangdaAppConfigSchema = {
266
+ $id: 'openxiangda.app-config/v3',
267
+ type: 'object',
268
+ additionalProperties: false,
269
+ required: ['schemaVersion', 'app', 'frontend', 'backend', 'platform'],
270
+ properties: {
271
+ schemaVersion: { const: 3 },
272
+ app: {
273
+ type: 'object',
274
+ additionalProperties: false,
275
+ required: ['code', 'name'],
276
+ properties: {
277
+ code: {
278
+ type: 'string',
279
+ pattern: '^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$',
280
+ minLength: 2,
281
+ maxLength: 128,
282
+ },
283
+ name: { type: 'string', minLength: 1, maxLength: 255 },
284
+ },
285
+ },
286
+ frontend: {
287
+ type: 'object',
288
+ additionalProperties: false,
289
+ required: ['root'],
290
+ properties: {
291
+ root: { type: 'string', minLength: 1 },
292
+ routes: { type: 'array', maxItems: 500, items: { type: 'object' } },
293
+ admin: { type: 'object' },
294
+ authentication: { type: 'object' },
295
+ publicAccess: { type: 'object' },
296
+ devicePolicy: {
297
+ type: 'object',
298
+ additionalProperties: false,
299
+ required: ['kind', 'mobileMaxWidthPx', 'desktopMinWidthPx'],
300
+ properties: {
301
+ kind: { const: 'viewport-family' },
302
+ mobileMaxWidthPx: { type: 'integer', minimum: 320, maximum: 1600 },
303
+ desktopMinWidthPx: { type: 'integer', minimum: 320, maximum: 1600 },
304
+ },
305
+ },
306
+ },
307
+ },
308
+ backend: {
309
+ type: 'object',
310
+ additionalProperties: false,
311
+ required: ['root', 'runtime', 'framework'],
312
+ properties: {
313
+ root: { type: 'string', minLength: 1 },
314
+ runtime: { const: 'node' },
315
+ framework: { const: 'nestjs' },
316
+ enabled: { type: 'boolean' },
317
+ isolation: { enum: ['shared', 'dedicated'] },
318
+ resourceProfile: { enum: ['light', 'standard'] },
319
+ secrets: {
320
+ type: 'array',
321
+ maxItems: 100,
322
+ items: {
323
+ type: 'object',
324
+ additionalProperties: false,
325
+ required: ['name', 'env'],
326
+ properties: {
327
+ name: {
328
+ type: 'string',
329
+ pattern: '^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$',
330
+ maxLength: 128,
331
+ },
332
+ env: {
333
+ type: 'string',
334
+ pattern: '^[A-Z][A-Z0-9_]*$',
335
+ maxLength: 128,
336
+ },
337
+ description: { type: 'string', maxLength: 2000 },
338
+ required: { type: 'boolean' },
339
+ },
340
+ },
341
+ },
342
+ operations: {
343
+ type: 'array',
344
+ maxItems: 500,
345
+ items: { type: 'object' },
346
+ },
347
+ },
348
+ },
349
+ platform: {
350
+ type: 'object',
351
+ additionalProperties: false,
352
+ required: ['root'],
353
+ properties: {
354
+ root: { type: 'string', minLength: 1 },
355
+ },
356
+ },
357
+ perspectives: {
358
+ type: 'array',
359
+ maxItems: 100,
360
+ items: {
361
+ type: 'object',
362
+ additionalProperties: false,
363
+ required: ['code', 'name', 'roleCodes'],
364
+ properties: {
365
+ code: {
366
+ type: 'string',
367
+ pattern: '^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$',
368
+ maxLength: 128,
369
+ },
370
+ name: { type: 'string', minLength: 1, maxLength: 255 },
371
+ description: { type: 'string', maxLength: 2000 },
372
+ roleCodes: {
373
+ type: 'array',
374
+ minItems: 1,
375
+ maxItems: 20,
376
+ uniqueItems: true,
377
+ items: { type: 'string' },
378
+ },
379
+ default: { type: 'boolean' },
380
+ },
381
+ },
382
+ },
383
+ data: {
384
+ type: 'object',
385
+ additionalProperties: false,
386
+ required: ['resources'],
387
+ properties: {
388
+ resources: { type: 'array', maxItems: 100, items: { type: 'object' } },
389
+ },
390
+ },
391
+ authz: {
392
+ type: 'object',
393
+ additionalProperties: false,
394
+ required: ['capabilities', 'roles'],
395
+ properties: {
396
+ authenticatedUserRoleCode: {
397
+ type: 'string',
398
+ pattern: '^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$',
399
+ maxLength: 128,
400
+ },
401
+ capabilities: {
402
+ type: 'array',
403
+ maxItems: 2000,
404
+ items: { type: 'object' },
405
+ },
406
+ roles: { type: 'array', maxItems: 100, items: { type: 'object' } },
407
+ scopeDimensions: {
408
+ type: 'array',
409
+ maxItems: 100,
410
+ items: {
411
+ type: 'object',
412
+ additionalProperties: false,
413
+ required: ['code', 'name'],
414
+ properties: {
415
+ code: { type: 'string' },
416
+ name: { type: 'string' },
417
+ resourceCode: { type: 'string' },
418
+ valueType: { enum: ['string', 'uuid'] },
419
+ hierarchyMode: { enum: ['flat', 'self_parent'] },
420
+ valueSource: {
421
+ type: 'object',
422
+ additionalProperties: false,
423
+ required: ['kind', 'resourceCode', 'labelField'],
424
+ properties: {
425
+ kind: { const: 'native_resource' },
426
+ resourceCode: { type: 'string' },
427
+ labelField: { type: 'string' },
428
+ enabledField: { type: 'string' },
429
+ },
430
+ },
431
+ },
432
+ },
433
+ },
434
+ scopeSources: {
435
+ type: 'array',
436
+ maxItems: 100,
437
+ items: { type: 'object' },
438
+ },
439
+ roleMembershipSources: {
440
+ type: 'array',
441
+ maxItems: 100,
442
+ items: roleMembershipSourceInputSchema,
443
+ },
444
+ relationshipGrantSources: {
445
+ type: 'array',
446
+ maxItems: 100,
447
+ items: relationshipGrantSourceInputSchema,
448
+ },
449
+ dataPolicies: {
450
+ type: 'array',
451
+ maxItems: 100,
452
+ items: { type: 'object' },
453
+ },
454
+ authorizationTransitions: {
455
+ type: 'array',
456
+ maxItems: 100,
457
+ items: authorizationTransitionInputSchema,
458
+ },
459
+ },
460
+ },
461
+ events: {
462
+ type: 'object',
463
+ additionalProperties: false,
464
+ required: ['subscriptions'],
465
+ properties: {
466
+ schemas: {
467
+ type: 'array',
468
+ maxItems: 100,
469
+ items: { type: 'object' },
470
+ },
471
+ subscriptions: {
472
+ type: 'array',
473
+ maxItems: 100,
474
+ items: { type: 'object' },
475
+ },
476
+ timers: {
477
+ type: 'array',
478
+ maxItems: 100,
479
+ items: { type: 'object' },
480
+ },
481
+ dateTriggers: {
482
+ type: 'array',
483
+ maxItems: 100,
484
+ items: { type: 'object' },
485
+ },
486
+ },
487
+ },
488
+ workflows: {
489
+ type: 'object',
490
+ additionalProperties: false,
491
+ required: ['definitions', 'bindings', 'activations'],
492
+ properties: {
493
+ definitions: {
494
+ type: 'array',
495
+ maxItems: 100,
496
+ items: { type: 'object' },
497
+ },
498
+ bindings: {
499
+ type: 'array',
500
+ maxItems: 100,
501
+ items: { type: 'object' },
502
+ },
503
+ activations: {
504
+ type: 'array',
505
+ maxItems: 100,
506
+ items: { type: 'object' },
507
+ },
508
+ providers: {
509
+ type: 'array',
510
+ maxItems: 100,
511
+ items: {
512
+ type: 'object',
513
+ additionalProperties: false,
514
+ required: ['code', 'endpointPath'],
515
+ properties: {
516
+ code: {
517
+ type: 'string',
518
+ pattern: '^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$',
519
+ },
520
+ endpointPath: {
521
+ type: 'string',
522
+ pattern: '^/[A-Za-z0-9/_-]+$',
523
+ },
524
+ timeoutMs: { type: 'integer', minimum: 100, maximum: 10000 },
525
+ },
526
+ },
527
+ },
528
+ editableParameters: {
529
+ type: 'array',
530
+ maxItems: 100,
531
+ items: { type: 'object' },
532
+ },
533
+ },
534
+ },
535
+ },
536
+ };
537
+ function diagnostic(code, message, path, remediation) {
538
+ return {
539
+ schemaVersion: SCHEMA_VERSIONS.diagnostic,
540
+ code,
541
+ severity: 'error',
542
+ message,
543
+ path,
544
+ source: 'openxiangda-app.config.ts',
545
+ retryable: false,
546
+ ...(remediation ? { remediation } : {}),
547
+ };
548
+ }
549
+ /**
550
+ * A backend is opt-in for a resource-only application. Explicit backend
551
+ * declarations (operations, secrets, events or workflows) still imply it.
552
+ */
553
+ export function backendRuntimeRequired(config) {
554
+ const backend = config.backend;
555
+ return (backend.enabled === true ||
556
+ (backend.enabled !== false &&
557
+ Boolean(backend.operations?.length ||
558
+ backend.secrets?.length ||
559
+ config.events?.subscriptions?.length ||
560
+ config.events?.timers?.length ||
561
+ config.events?.dateTriggers?.length ||
562
+ config.workflows?.activations?.length ||
563
+ config.workflows?.providers?.length)));
564
+ }
565
+ function object(value) {
566
+ return value && typeof value === 'object' && !Array.isArray(value)
567
+ ? value
568
+ : {};
569
+ }
570
+ function string(value) {
571
+ return typeof value === 'string' ? value.trim() : '';
572
+ }
573
+ function policyRuleNodes(policy, policyPath, diagnostics) {
574
+ const baseRules = (Array.isArray(policy.rules) ? policy.rules : []).map((rule, index) => ({
575
+ rule: object(rule),
576
+ path: `${policyPath}.rules[${index}]`,
577
+ }));
578
+ if (policy.readExpression === undefined)
579
+ return baseRules;
580
+ const leaves = [];
581
+ let structuralError = false;
582
+ const visit = (value, path, depth) => {
583
+ if (depth > DATA_POLICY_EXPRESSION_MAX_DEPTH) {
584
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_POLICY_EXPRESSION_DEPTH_EXCEEDED', `策略表达式深度不能超过 ${DATA_POLICY_EXPRESSION_MAX_DEPTH}`, path));
585
+ structuralError = true;
586
+ return;
587
+ }
588
+ const node = object(value);
589
+ const groupKeys = ['allOf', 'anyOf'].filter(key => key in node);
590
+ if (groupKeys.length === 0) {
591
+ leaves.push({ rule: node, path });
592
+ return;
593
+ }
594
+ const key = groupKeys[0];
595
+ const children = Array.isArray(node[key]) ? node[key] : [];
596
+ if (groupKeys.length !== 1 ||
597
+ Object.keys(node).length !== 1 ||
598
+ !Array.isArray(node[key]) ||
599
+ children.length < 1 ||
600
+ children.length > DATA_POLICY_EXPRESSION_MAX_GROUP_ITEMS) {
601
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_POLICY_EXPRESSION_GROUP_INVALID', `表达式节点只能声明一个 allOf/anyOf,且包含 1 到 ${DATA_POLICY_EXPRESSION_MAX_GROUP_ITEMS} 个子项`, path));
602
+ structuralError = true;
603
+ return;
604
+ }
605
+ children.forEach((child, index) => visit(child, `${path}.${key}[${index}]`, depth + 1));
606
+ };
607
+ visit(policy.readExpression, `${policyPath}.readExpression`, 1);
608
+ if (leaves.length > DATA_POLICY_EXPRESSION_MAX_LEAVES) {
609
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_POLICY_EXPRESSION_LEAVES_EXCEEDED', `策略表达式最多包含 ${DATA_POLICY_EXPRESSION_MAX_LEAVES} 个规则叶子`, `${policyPath}.readExpression`));
610
+ structuralError = true;
611
+ }
612
+ if (!structuralError && leaves.length > 0) {
613
+ try {
614
+ dataPolicyExpressionToCnf(policy.readExpression);
615
+ }
616
+ catch {
617
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_POLICY_EXPRESSION_EXPANSION_EXCEEDED', '策略表达式规范化后超过 50 个 CNF 子句或 100 个规则实例', `${policyPath}.readExpression`));
618
+ }
619
+ }
620
+ return [...baseRules, ...leaves];
621
+ }
622
+ function eventSchemaDeclaresPath(schema, path) {
623
+ let current = schema;
624
+ for (const segment of path.split('.')) {
625
+ if (current.type === 'array')
626
+ current = object(current.items);
627
+ const properties = object(current.properties);
628
+ if (!Object.prototype.hasOwnProperty.call(properties, segment)) {
629
+ return false;
630
+ }
631
+ current = object(properties[segment]);
632
+ }
633
+ return true;
634
+ }
635
+ function eventDataMatchesSchema(schema, data) {
636
+ if (!schema)
637
+ return false;
638
+ try {
639
+ const validator = new Ajv2020({
640
+ allErrors: true,
641
+ strict: false,
642
+ validateFormats: false,
643
+ }).compile(schema);
644
+ return Boolean(validator(data));
645
+ }
646
+ catch {
647
+ return false;
648
+ }
649
+ }
650
+ function validDateTriggerOffset(value) {
651
+ const match = /^([+-])?P(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?)?$/.exec(string(value));
652
+ if (!match || !match.slice(2).some(part => part !== undefined))
653
+ return false;
654
+ const milliseconds = Number(match[2] || 0) * 24 * 60 * 60 * 1000 +
655
+ Number(match[3] || 0) * 60 * 60 * 1000 +
656
+ Number(match[4] || 0) * 60 * 1000 +
657
+ Number(match[5] || 0) * 1000;
658
+ return (Number.isFinite(milliseconds) &&
659
+ milliseconds <= 10 * 365 * 24 * 60 * 60 * 1000);
660
+ }
661
+ const EVENT_TYPE_PATTERN = /^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9_-]*)+\.v[1-9][0-9]*$/;
662
+ const EVENT_DATA_SCHEMA_VERSION_PATTERN = /^[1-9][0-9]*\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)$/;
663
+ const EVENT_FIELD_PATTERN = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
664
+ function eventPredicateValid(value) {
665
+ const predicate = object(value);
666
+ const keys = Object.keys(predicate);
667
+ if (keys.length !== 1 ||
668
+ !['eq', 'ne', 'in', 'notIn', 'exists'].includes(keys[0] || '')) {
669
+ return false;
670
+ }
671
+ const operand = predicate[keys[0]];
672
+ if (keys[0] === 'exists' && typeof operand !== 'boolean')
673
+ return false;
674
+ if ((keys[0] === 'in' || keys[0] === 'notIn') &&
675
+ (!Array.isArray(operand) || operand.length < 1 || operand.length > 20))
676
+ return false;
677
+ try {
678
+ const serialized = JSON.stringify(predicate);
679
+ return (typeof serialized === 'string' &&
680
+ Buffer.byteLength(serialized, 'utf8') <= 4096);
681
+ }
682
+ catch {
683
+ return false;
684
+ }
685
+ }
686
+ function validateEventChangeFilter(value, path, allowedFields, diagnostics) {
687
+ const change = object(value);
688
+ const field = string(change.field);
689
+ const keys = Object.keys(change);
690
+ if (!EVENT_FIELD_PATTERN.test(field) ||
691
+ !allowedFields.has(field) ||
692
+ keys.some(key => !['field', 'before', 'after'].includes(key)) ||
693
+ (!('before' in change) && !('after' in change)) ||
694
+ ('before' in change && !eventPredicateValid(change.before)) ||
695
+ ('after' in change && !eventPredicateValid(change.after))) {
696
+ diagnostics.push(diagnostic('APP_CONFIG_EVENT_CHANGE_FILTER_INVALID', '字段变化条件必须引用已声明字段,并且 before/after 各只使用一个受支持操作符', path));
697
+ }
698
+ }
699
+ function validateEventFilterCondition(value, path, allowedFields, diagnostics, state, depth = 1) {
700
+ const condition = object(value);
701
+ const keys = Object.keys(condition);
702
+ if (depth > 3 || keys.length !== 1) {
703
+ diagnostics.push(diagnostic('APP_CONFIG_EVENT_FILTER_COMPLEXITY_EXCEEDED', '事件过滤条件最多嵌套 3 层,且每层只能声明 change/all/any/not 之一', path));
704
+ return;
705
+ }
706
+ const key = keys[0];
707
+ if (key === 'change') {
708
+ state.atoms += 1;
709
+ validateEventChangeFilter(condition.change, `${path}.change`, allowedFields, diagnostics);
710
+ }
711
+ else if (key === 'all' || key === 'any') {
712
+ const items = Array.isArray(condition[key]) ? condition[key] : [];
713
+ if (items.length < 1 || items.length > 16) {
714
+ diagnostics.push(diagnostic('APP_CONFIG_EVENT_FILTER_GROUP_INVALID', '事件过滤 all/any 必须包含 1 到 16 个条件', `${path}.${key}`));
715
+ }
716
+ items.forEach((item, index) => validateEventFilterCondition(item, `${path}.${key}[${index}]`, allowedFields, diagnostics, state, depth + 1));
717
+ }
718
+ else if (key === 'not') {
719
+ validateEventFilterCondition(condition.not, `${path}.not`, allowedFields, diagnostics, state, depth + 1);
720
+ }
721
+ else {
722
+ diagnostics.push(diagnostic('APP_CONFIG_EVENT_FILTER_OPERATOR_INVALID', '事件过滤条件只支持 change/all/any/not', path));
723
+ }
724
+ }
725
+ export function validateAppConfig(value) {
726
+ const diagnostics = [];
727
+ const config = object(value);
728
+ rejectNativeEnvironmentFields(config, diagnostics);
729
+ if (config.schemaVersion !== 3) {
730
+ diagnostics.push(diagnostic('APP_CONFIG_SCHEMA_UNSUPPORTED', 'schemaVersion 必须是 3', 'schemaVersion', '使用 Native 2.0 模板重新生成 openxiangda-app.config.ts;不支持 v2 自动转换'));
731
+ }
732
+ const app = object(config.app);
733
+ const appCode = string(app.code);
734
+ if (!/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(appCode)) {
735
+ diagnostics.push(diagnostic('APP_CONFIG_CODE_INVALID', 'app.code 必须是 kebab-case 稳定应用代码', 'app.code'));
736
+ }
737
+ if (!string(app.name)) {
738
+ diagnostics.push(diagnostic('APP_CONFIG_NAME_REQUIRED', 'app.name 必须是非空字符串', 'app.name'));
739
+ }
740
+ const platform = object(config.platform);
741
+ for (const key of Object.keys(platform)) {
742
+ if (key !== 'root') {
743
+ diagnostics.push(diagnostic('APP_CONFIG_PLATFORM_PROPERTY_UNSUPPORTED', `platform.${key} 不属于应用声明合同;平台能力要求只能由 compiler 派生`, `platform.${key}`));
744
+ }
745
+ }
746
+ const frontend = object(config.frontend);
747
+ const backend = object(config.backend);
748
+ const backendKeys = new Set([
749
+ 'root',
750
+ 'runtime',
751
+ 'framework',
752
+ 'enabled',
753
+ 'isolation',
754
+ 'resourceProfile',
755
+ 'secrets',
756
+ 'operations',
757
+ ]);
758
+ for (const key of Object.keys(backend)) {
759
+ if (!backendKeys.has(key)) {
760
+ diagnostics.push(diagnostic('APP_CONFIG_BACKEND_FIELD_UNSUPPORTED', `backend.${key} 不属于 2.0 后端合同`, `backend.${key}`));
761
+ }
762
+ }
763
+ for (const [path, root] of [
764
+ ['frontend.root', frontend.root],
765
+ ['backend.root', backend.root],
766
+ ['platform.root', platform.root],
767
+ ]) {
768
+ if (!string(root)) {
769
+ diagnostics.push(diagnostic('APP_CONFIG_ROOT_REQUIRED', `${path} 必须是非空相对路径`, path));
770
+ }
771
+ else if (string(root).startsWith('/') || string(root).includes('..')) {
772
+ diagnostics.push(diagnostic('APP_CONFIG_ROOT_UNSAFE', `${path} 必须位于工作区内`, path));
773
+ }
774
+ }
775
+ if (backend.runtime !== 'node') {
776
+ diagnostics.push(diagnostic('APP_CONFIG_BACKEND_RUNTIME_UNSUPPORTED', 'backend.runtime 首期只支持 node', 'backend.runtime'));
777
+ }
778
+ if (backend.framework !== 'nestjs') {
779
+ diagnostics.push(diagnostic('APP_CONFIG_BACKEND_FRAMEWORK_UNSUPPORTED', 'backend.framework 首期只支持 nestjs', 'backend.framework'));
780
+ }
781
+ if (backend.isolation !== undefined &&
782
+ !['shared', 'dedicated'].includes(String(backend.isolation))) {
783
+ diagnostics.push(diagnostic('APP_CONFIG_BACKEND_ISOLATION_UNSUPPORTED', 'backend.isolation 必须是 shared 或 dedicated', 'backend.isolation'));
784
+ }
785
+ if (backend.resourceProfile !== undefined &&
786
+ !['light', 'standard'].includes(String(backend.resourceProfile))) {
787
+ diagnostics.push(diagnostic('APP_CONFIG_BACKEND_RESOURCE_PROFILE_UNSUPPORTED', 'backend.resourceProfile 必须是 light 或 standard', 'backend.resourceProfile'));
788
+ }
789
+ validateApplicationAuthentication(config, diagnostics);
790
+ validateFrontendDevicePolicy(config, diagnostics);
791
+ validateFrontendRoutes(config, diagnostics);
792
+ validateFrontendUser(config, diagnostics);
793
+ validateAnonymousPublicAccess(config, diagnostics);
794
+ validateAdminAccess(config, diagnostics);
795
+ validateAdminNavigation(config, diagnostics);
796
+ validatePerspectives(config.perspectives, object(config.authz).roles, diagnostics);
797
+ const declaredResources = new Map((Array.isArray(object(config.data).resources)
798
+ ? object(config.data).resources
799
+ : []).map(resource => {
800
+ const item = object(resource);
801
+ const schema = object(item.schema);
802
+ const fields = Array.isArray(schema.fields) ? schema.fields : [];
803
+ return [
804
+ string(item.code),
805
+ new Map(fields.map(field => {
806
+ const declaration = object(field);
807
+ return [string(declaration.code), string(declaration.type)];
808
+ })),
809
+ ];
810
+ }));
811
+ const declaredResourceCodes = new Set(declaredResources.keys());
812
+ const declaredWorkflowCodes = new Set((Array.isArray(object(config.workflows).activations)
813
+ ? object(config.workflows).activations
814
+ : []).map(item => string(object(item).workflowCode)));
815
+ validateBackendOperations(backend.operations, declaredResources, declaredWorkflowCodes, diagnostics);
816
+ const eventConfig = object(config.events);
817
+ const workflowConfig = object(config.workflows);
818
+ if (backend.enabled === false &&
819
+ (Array.isArray(backend.operations) && backend.operations.length > 0 ||
820
+ Array.isArray(backend.secrets) && backend.secrets.length > 0 ||
821
+ Array.isArray(eventConfig.subscriptions) &&
822
+ eventConfig.subscriptions.length > 0 ||
823
+ Array.isArray(eventConfig.timers) && eventConfig.timers.length > 0 ||
824
+ Array.isArray(eventConfig.dateTriggers) &&
825
+ eventConfig.dateTriggers.length > 0 ||
826
+ Array.isArray(workflowConfig.activations) &&
827
+ workflowConfig.activations.length > 0 ||
828
+ Array.isArray(workflowConfig.providers) && workflowConfig.providers.length > 0)) {
829
+ diagnostics.push(diagnostic('APP_CONFIG_BACKEND_DISABLED_WITH_DECLARATIONS', 'backend.enabled=false 时不能声明后端操作、Secret、事件或工作流运行时', 'backend.enabled'));
830
+ }
831
+ const secretNames = new Set();
832
+ const secretEnvs = new Set();
833
+ (Array.isArray(backend.secrets) ? backend.secrets : []).forEach((raw, index) => {
834
+ const secret = object(raw);
835
+ const name = string(secret.name);
836
+ const env = string(secret.env);
837
+ const path = `backend.secrets[${index}]`;
838
+ if (!/^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/.test(name)) {
839
+ diagnostics.push(diagnostic('APP_CONFIG_BACKEND_SECRET_NAME_INVALID', '后端 Secret name 必须是稳定的 kebab/snake-case 逻辑名称', `${path}.name`));
840
+ }
841
+ if (!/^[A-Z][A-Z0-9_]*$/.test(env) ||
842
+ env.startsWith('OPENXIANGDA_') ||
843
+ ['NODE_ENV', 'PORT'].includes(env)) {
844
+ diagnostics.push(diagnostic('APP_CONFIG_BACKEND_SECRET_ENV_INVALID', '后端 Secret env 必须是合法大写环境变量,且不能占用平台保留变量', `${path}.env`));
845
+ }
846
+ if (secretNames.has(name) || secretEnvs.has(env)) {
847
+ diagnostics.push(diagnostic('APP_CONFIG_BACKEND_SECRET_DUPLICATE', '后端 Secret 的 name 和 env 必须各自唯一', path));
848
+ }
849
+ if (secret.required !== undefined &&
850
+ typeof secret.required !== 'boolean') {
851
+ diagnostics.push(diagnostic('APP_CONFIG_BACKEND_SECRET_REQUIRED_INVALID', '后端 Secret required 必须是 boolean', `${path}.required`));
852
+ }
853
+ secretNames.add(name);
854
+ secretEnvs.add(env);
855
+ });
856
+ const data = object(config.data);
857
+ if (config.data !== undefined && !Array.isArray(data.resources)) {
858
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCES_REQUIRED', 'data.resources 必须是数组', 'data.resources'));
859
+ }
860
+ else {
861
+ const resources = Array.isArray(data.resources) ? data.resources : [];
862
+ const resourceCodes = new Set();
863
+ resources.forEach((resource, index) => {
864
+ const resourceDiagnostics = validateDataResource(resource);
865
+ diagnostics.push(...resourceDiagnostics.map(item => ({
866
+ ...item,
867
+ path: `data.resources[${index}]${item.path === '$' ? '' : `.${item.path}`}`,
868
+ source: 'openxiangda-app.config.ts',
869
+ })));
870
+ if (object(resource).appCode &&
871
+ string(object(resource).appCode) !== appCode) {
872
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_APP_MISMATCH', 'DataResource.appCode 必须与 app.code 一致', `data.resources[${index}].appCode`));
873
+ }
874
+ const resourceRecord = object(resource);
875
+ const resourceCode = string(resourceRecord.code);
876
+ if (resourceCode && resourceCodes.has(resourceCode)) {
877
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_DUPLICATE', `数据资源 code ${resourceCode} 不能重复`, `data.resources[${index}].code`));
878
+ }
879
+ if (resourceCode)
880
+ resourceCodes.add(resourceCode);
881
+ });
882
+ resources.forEach((resource, index) => {
883
+ const resourceRecord = object(resource);
884
+ const fields = object(resourceRecord.schema).fields;
885
+ (Array.isArray(fields) ? fields : []).forEach((rawField, fieldIndex) => {
886
+ const field = object(rawField);
887
+ const source = object(field.source);
888
+ if (string(source.kind) !== 'resource')
889
+ return;
890
+ const targetCode = string(source.resourceCode);
891
+ const target = resources.find(candidate => string(object(candidate).code) === targetCode);
892
+ const sourcePath = `data.resources[${index}].schema.fields[${fieldIndex}].source`;
893
+ if (!target) {
894
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_SOURCE_TARGET_NOT_FOUND', `动态选项来源 ${targetCode} 必须在同一应用中声明`, `${sourcePath}.resourceCode`));
895
+ return;
896
+ }
897
+ const targetFields = object(object(target).schema).fields;
898
+ const targetFieldList = Array.isArray(targetFields) ? targetFields : [];
899
+ const labelField = string(source.labelField);
900
+ const label = (Array.isArray(targetFields) ? targetFields : []).find(candidate => string(object(candidate).code) === labelField);
901
+ if (!label ||
902
+ !['text.short', 'text.long'].includes(string(object(label).type))) {
903
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_SOURCE_LABEL_INVALID', `动态选项标签字段 ${labelField} 必须是 text.short 或 text.long`, `${sourcePath}.labelField`));
904
+ }
905
+ for (const key of [
906
+ 'searchFields',
907
+ 'descriptionFields',
908
+ 'snapshotFields',
909
+ ]) {
910
+ const values = Array.isArray(source[key]) ? source[key] : [];
911
+ values.forEach((value, valueIndex) => {
912
+ const targetField = targetFieldList.find(candidate => string(object(candidate).code) === string(value));
913
+ if (!targetField) {
914
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_SOURCE_FIELD_NOT_FOUND', `动态来源字段 ${String(value)} 未在 ${targetCode} 声明`, `${sourcePath}.${key}[${valueIndex}]`));
915
+ }
916
+ else if (key === 'searchFields' &&
917
+ !supportsSearch(string(object(targetField).type))) {
918
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_SOURCE_SEARCH_UNSUPPORTED', `动态来源字段 ${String(value)} 不支持搜索`, `${sourcePath}.${key}[${valueIndex}]`));
919
+ }
920
+ });
921
+ }
922
+ const sourceFilters = Array.isArray(source.filters) ? source.filters : [];
923
+ sourceFilters.forEach((rawFilter, filterIndex) => {
924
+ const filter = object(rawFilter);
925
+ const targetFieldCode = string(filter.field);
926
+ if (!targetFieldList.some(candidate => string(object(candidate).code) === targetFieldCode)) {
927
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_SOURCE_FILTER_FIELD_NOT_FOUND', `动态来源筛选字段 ${targetFieldCode} 未在 ${targetCode} 声明`, `${sourcePath}.filters[${filterIndex}].field`));
928
+ }
929
+ const bindingField = string(object(filter.binding).field);
930
+ if (bindingField &&
931
+ !(Array.isArray(fields) ? fields : []).some(candidate => string(object(candidate).code) === bindingField)) {
932
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_SOURCE_BINDING_FIELD_NOT_FOUND', `动态来源绑定字段 ${bindingField} 未在当前资源声明`, `${sourcePath}.filters[${filterIndex}].binding.field`));
933
+ }
934
+ });
935
+ });
936
+ });
937
+ }
938
+ const authz = object(config.authz);
939
+ const declaredCapabilities = Array.isArray(authz.capabilities)
940
+ ? authz.capabilities
941
+ : [];
942
+ if (config.authz !== undefined) {
943
+ if (!Array.isArray(authz.capabilities)) {
944
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_CAPABILITIES_REQUIRED', 'authz.capabilities 必须是显式 backend/ui capability 数组', 'authz.capabilities'));
945
+ }
946
+ validateCapabilityDeclarations(appCode, declaredCapabilities, diagnostics);
947
+ const roles = Array.isArray(authz.roles) ? authz.roles : [];
948
+ if (!Array.isArray(authz.roles)) {
949
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_ROLES_REQUIRED', 'authz.roles 必须是数组', 'authz.roles'));
950
+ }
951
+ const forbiddenNativeMutationCapabilities = new Map();
952
+ for (const rawResource of Array.isArray(data.resources)
953
+ ? data.resources
954
+ : []) {
955
+ const resource = object(rawResource);
956
+ const mutationOwner = string(object(resource.surface).mutationOwner) || 'native';
957
+ if (mutationOwner === 'native')
958
+ continue;
959
+ for (const operation of ['create', 'update', 'delete']) {
960
+ const capability = string(object(resource.capabilities)[operation]);
961
+ if (capability) {
962
+ forbiddenNativeMutationCapabilities.set(capability, {
963
+ resourceCode: string(resource.code),
964
+ operation,
965
+ });
966
+ }
967
+ }
968
+ }
969
+ const roleCodes = new Set();
970
+ roles.forEach((raw, index) => {
971
+ const role = object(raw);
972
+ const code = string(role.code);
973
+ if (!/^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/.test(code)) {
974
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_ROLE_CODE_INVALID', '角色 code 必须是稳定的小写标识', `authz.roles[${index}].code`));
975
+ }
976
+ else if (roleCodes.has(code)) {
977
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_ROLE_DUPLICATE', '角色 code 不能重复', `authz.roles[${index}].code`));
978
+ }
979
+ roleCodes.add(code);
980
+ if (!string(role.name) ||
981
+ !Array.isArray(role.capabilities) ||
982
+ 'isAppSuperAdmin' in role) {
983
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_ROLE_INVALID', '角色必须声明 name 和 capabilities', `authz.roles[${index}]`));
984
+ }
985
+ const capabilities = Array.isArray(role.capabilities)
986
+ ? role.capabilities
987
+ : [];
988
+ const seenCapabilities = new Set();
989
+ capabilities.forEach((rawCapability, capabilityIndex) => {
990
+ const capability = string(rawCapability);
991
+ if (!/^[A-Za-z0-9][A-Za-z0-9:._*-]{0,254}$/.test(capability) ||
992
+ seenCapabilities.has(capability)) {
993
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_CAPABILITY_INVALID', 'capability 必须是非空稳定标识,且同一角色内不能重复', `authz.roles[${index}].capabilities[${capabilityIndex}]`));
994
+ }
995
+ const forbiddenMutation = forbiddenNativeMutationCapabilities.get(capability);
996
+ if (forbiddenMutation) {
997
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_NATIVE_MUTATION_GRANT_FORBIDDEN', `${forbiddenMutation.resourceCode} 的 ${forbiddenMutation.operation} 由非 Native owner 管理,应用角色不能获得对应 Native Data capability`, `authz.roles[${index}].capabilities[${capabilityIndex}]`));
998
+ }
999
+ seenCapabilities.add(capability);
1000
+ });
1001
+ });
1002
+ const authenticatedUserRoleCode = authz.authenticatedUserRoleCode === undefined
1003
+ ? ''
1004
+ : string(authz.authenticatedUserRoleCode);
1005
+ if (authz.authenticatedUserRoleCode !== undefined &&
1006
+ (!/^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/.test(authenticatedUserRoleCode) ||
1007
+ !roleCodes.has(authenticatedUserRoleCode))) {
1008
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHENTICATED_USER_ROLE_INVALID', 'authenticatedUserRoleCode 必须引用一个已声明的 package role', 'authz.authenticatedUserRoleCode'));
1009
+ }
1010
+ const dimensions = Array.isArray(authz.scopeDimensions)
1011
+ ? authz.scopeDimensions
1012
+ : [];
1013
+ const policies = Array.isArray(authz.dataPolicies)
1014
+ ? authz.dataPolicies
1015
+ : [];
1016
+ for (const [name, values] of [
1017
+ ['scopeDimensions', authz.scopeDimensions],
1018
+ ['scopeSources', authz.scopeSources],
1019
+ ['roleMembershipSources', authz.roleMembershipSources],
1020
+ ['relationshipGrantSources', authz.relationshipGrantSources],
1021
+ ['dataPolicies', authz.dataPolicies],
1022
+ ]) {
1023
+ if (values !== undefined && !Array.isArray(values)) {
1024
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_COLLECTION_INVALID', `authz.${name} 必须是数组`, `authz.${name}`));
1025
+ }
1026
+ }
1027
+ const dataResources = Array.isArray(data.resources) ? data.resources : [];
1028
+ const resourceFieldTypes = new Map(dataResources.map(rawResource => {
1029
+ const resource = object(rawResource);
1030
+ const schema = object(resource.schema);
1031
+ const fields = Array.isArray(schema.fields) ? schema.fields : [];
1032
+ return [
1033
+ string(resource.code),
1034
+ new Map(fields.map(rawField => {
1035
+ const field = object(rawField);
1036
+ return [string(field.code), string(field.type)];
1037
+ })),
1038
+ ];
1039
+ }));
1040
+ const resourceFields = new Map([...resourceFieldTypes].map(([resourceCode, fields]) => [
1041
+ resourceCode,
1042
+ new Set(fields.keys()),
1043
+ ]));
1044
+ const resourceFieldDeclarations = new Map(dataResources.map(rawResource => {
1045
+ const resource = object(rawResource);
1046
+ const fields = Array.isArray(object(resource.schema).fields)
1047
+ ? object(resource.schema).fields
1048
+ : [];
1049
+ return [
1050
+ string(resource.code),
1051
+ new Map(fields.map(rawField => {
1052
+ const field = object(rawField);
1053
+ return [string(field.code), field];
1054
+ })),
1055
+ ];
1056
+ }));
1057
+ const dimensionCodes = new Set();
1058
+ dimensions.forEach((raw, index) => {
1059
+ const dimension = object(raw);
1060
+ const code = string(dimension.code);
1061
+ const dimensionPath = `authz.scopeDimensions[${index}]`;
1062
+ if (!/^[a-z][a-z0-9]*(?:[-_.][a-z0-9]+)*$/.test(code) ||
1063
+ dimensionCodes.has(code) ||
1064
+ !string(dimension.name) ||
1065
+ (dimension.valueType !== undefined &&
1066
+ !['string', 'uuid'].includes(string(dimension.valueType))) ||
1067
+ (dimension.hierarchyMode !== undefined &&
1068
+ !['flat', 'self_parent'].includes(string(dimension.hierarchyMode)))) {
1069
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_DIMENSION_INVALID', 'scope dimension 必须声明唯一 code、name 和受支持的类型', dimensionPath));
1070
+ }
1071
+ if (dimension.valueSource !== undefined) {
1072
+ const valueSource = object(dimension.valueSource);
1073
+ const valueSourcePath = `${dimensionPath}.valueSource`;
1074
+ const valueSourceKeys = Object.keys(valueSource);
1075
+ const allowedKeys = new Set([
1076
+ 'kind',
1077
+ 'resourceCode',
1078
+ 'labelField',
1079
+ 'enabledField',
1080
+ ]);
1081
+ const resourceCode = string(valueSource.resourceCode);
1082
+ const labelField = string(valueSource.labelField);
1083
+ const enabledField = valueSource.enabledField === undefined
1084
+ ? undefined
1085
+ : string(valueSource.enabledField);
1086
+ const declaredFieldTypes = resourceFieldTypes.get(resourceCode);
1087
+ if (string(valueSource.kind) !== 'native_resource' ||
1088
+ string(dimension.valueType) !== 'uuid' ||
1089
+ !/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(resourceCode) ||
1090
+ !/^[A-Za-z][A-Za-z0-9_]{0,62}$/.test(labelField) ||
1091
+ (enabledField !== undefined &&
1092
+ !/^[A-Za-z][A-Za-z0-9_]{0,62}$/.test(enabledField)) ||
1093
+ valueSourceKeys.some(key => !allowedKeys.has(key))) {
1094
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_DIMENSION_VALUE_SOURCE_INVALID', 'native_resource valueSource 必须声明 UUID 维度、资源、标签字段和可选启用字段', valueSourcePath));
1095
+ }
1096
+ if (!declaredFieldTypes) {
1097
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_DIMENSION_VALUE_SOURCE_RESOURCE_INVALID', 'native_resource valueSource 必须引用同一应用内已声明的 Data Resource', `${valueSourcePath}.resourceCode`));
1098
+ }
1099
+ else if (!['text.short', 'text.long', 'serial-number'].includes(declaredFieldTypes.get(labelField) || '') ||
1100
+ (enabledField !== undefined &&
1101
+ declaredFieldTypes.get(enabledField) !== 'boolean')) {
1102
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_DIMENSION_VALUE_SOURCE_FIELD_INVALID', 'valueSource 标签字段必须为文本/流水号,启用字段必须为 boolean', valueSourcePath));
1103
+ }
1104
+ }
1105
+ dimensionCodes.add(code);
1106
+ });
1107
+ const scopeSources = Array.isArray(authz.scopeSources)
1108
+ ? authz.scopeSources
1109
+ : [];
1110
+ const sourceCodes = new Set();
1111
+ scopeSources.forEach((raw, index) => {
1112
+ const source = object(raw);
1113
+ const sourcePath = `authz.scopeSources[${index}]`;
1114
+ const code = string(source.code);
1115
+ const subject = object(source.subject);
1116
+ const grants = Array.isArray(source.grants) ? source.grants : [];
1117
+ const subjectType = string(subject.type);
1118
+ const roleCode = string(subject.roleCode);
1119
+ const resourceCode = string(source.resourceCode);
1120
+ const declaredFields = resourceFields.get(resourceCode);
1121
+ if (!/^[a-z][a-z0-9]*(?:[-_.][a-z0-9]+)*$/.test(code) ||
1122
+ sourceCodes.has(code) ||
1123
+ !string(source.name) ||
1124
+ !/^[a-z][a-z0-9]*(?:[-_.][a-z0-9]+)*$/.test(resourceCode) ||
1125
+ !['user', 'role_membership'].includes(subjectType) ||
1126
+ !SEMANTIC_FIELD_PATH_PATTERN.test(string(subject.userIdField)) ||
1127
+ (subjectType === 'role_membership' && !roleCodes.has(roleCode)) ||
1128
+ (subjectType === 'user' && Boolean(roleCode)) ||
1129
+ grants.length === 0 ||
1130
+ !['strict', 'last_known_good'].includes(string(source.failureMode))) {
1131
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_SCOPE_SOURCE_INVALID', 'scope source 必须声明唯一 code、Data Resource、主体映射、grant 映射与失败策略', sourcePath));
1132
+ }
1133
+ const grantDimensions = new Set();
1134
+ grants.forEach((rawGrant, grantIndex) => {
1135
+ const grant = object(rawGrant);
1136
+ const dimensionCode = string(grant.dimensionCode);
1137
+ if (!dimensionCodes.has(dimensionCode) ||
1138
+ grantDimensions.has(dimensionCode) ||
1139
+ !SEMANTIC_FIELD_PATH_PATTERN.test(string(grant.valueField)) ||
1140
+ (grant.parentValueField !== undefined &&
1141
+ !SEMANTIC_FIELD_PATH_PATTERN.test(string(grant.parentValueField)))) {
1142
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_SCOPE_SOURCE_GRANT_INVALID', 'scope source grant 必须引用唯一已声明维度及有效字段', `${sourcePath}.grants[${grantIndex}]`));
1143
+ }
1144
+ grantDimensions.add(dimensionCode);
1145
+ });
1146
+ for (const field of [
1147
+ 'operationField',
1148
+ 'enabledField',
1149
+ 'effectiveFromField',
1150
+ 'effectiveToField',
1151
+ ]) {
1152
+ if (source[field] !== undefined &&
1153
+ !/^[A-Za-z_][A-Za-z0-9_]{0,127}$/.test(string(source[field]))) {
1154
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_SCOPE_SOURCE_FIELD_INVALID', 'scope source 可选字段必须是有效 Data Resource 字段名', `${sourcePath}.${field}`));
1155
+ }
1156
+ }
1157
+ const referencedFields = [
1158
+ string(subject.userIdField),
1159
+ ...grants.flatMap(rawGrant => {
1160
+ const grant = object(rawGrant);
1161
+ return [
1162
+ string(grant.valueField),
1163
+ ...(grant.parentValueField === undefined
1164
+ ? []
1165
+ : [string(grant.parentValueField)]),
1166
+ ];
1167
+ }),
1168
+ ...[
1169
+ source.operationField,
1170
+ source.enabledField,
1171
+ source.effectiveFromField,
1172
+ source.effectiveToField,
1173
+ ]
1174
+ .filter(value => value !== undefined)
1175
+ .map(string),
1176
+ ];
1177
+ if (!declaredFields ||
1178
+ referencedFields.some(field => !declaredFields.has(semanticFieldPathRoot(field)))) {
1179
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_SCOPE_SOURCE_RESOURCE_INVALID', 'scope source 必须引用已声明 Data Resource 及其真实字段', sourcePath));
1180
+ }
1181
+ sourceCodes.add(code);
1182
+ });
1183
+ const projectionSourceCodes = new Set();
1184
+ const roleMembershipSources = Array.isArray(authz.roleMembershipSources)
1185
+ ? authz.roleMembershipSources
1186
+ : [];
1187
+ const relationshipGrantSources = Array.isArray(authz.relationshipGrantSources)
1188
+ ? authz.relationshipGrantSources
1189
+ : [];
1190
+ const projectionUserPathValid = (path, fields) => {
1191
+ const root = semanticFieldPathRoot(path);
1192
+ return (SEMANTIC_FIELD_PATH_PATTERN.test(path) &&
1193
+ path === `${root}.value` &&
1194
+ string(fields?.get(root)?.type) === 'user.single');
1195
+ };
1196
+ const projectionControlFieldsValid = (source, fields) => source.failureMode === 'strict' &&
1197
+ (source.enabledField === undefined ||
1198
+ string(fields?.get(string(source.enabledField))?.type) === 'boolean') &&
1199
+ (source.effectiveFromField === undefined ||
1200
+ ['date', 'datetime'].includes(string(fields?.get(string(source.effectiveFromField))?.type))) &&
1201
+ (source.effectiveToField === undefined ||
1202
+ ['date', 'datetime'].includes(string(fields?.get(string(source.effectiveToField))?.type)));
1203
+ roleMembershipSources.forEach((raw, index) => {
1204
+ const source = object(raw);
1205
+ const sourcePath = `authz.roleMembershipSources[${index}]`;
1206
+ const unknownKeys = Object.keys(source).filter(key => ![
1207
+ 'code',
1208
+ 'name',
1209
+ 'resourceCode',
1210
+ 'userIdField',
1211
+ 'roleCode',
1212
+ 'enabledField',
1213
+ 'effectiveFromField',
1214
+ 'effectiveToField',
1215
+ 'failureMode',
1216
+ ].includes(key));
1217
+ if (unknownKeys.length > 0) {
1218
+ diagnostics.push(diagnostic('APP_CONFIG_SCHEMA_INVALID', `role membership source 包含未知属性 ${unknownKeys.join(', ')}`, sourcePath));
1219
+ }
1220
+ const code = string(source.code);
1221
+ const resourceCode = string(source.resourceCode);
1222
+ const fields = resourceFieldDeclarations.get(resourceCode);
1223
+ if (!/^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/.test(code) ||
1224
+ code.length > 100 ||
1225
+ projectionSourceCodes.has(code) ||
1226
+ !string(source.name) ||
1227
+ !fields ||
1228
+ !projectionUserPathValid(string(source.userIdField), fields) ||
1229
+ !roleCodes.has(string(source.roleCode)) ||
1230
+ !projectionControlFieldsValid(source, fields)) {
1231
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_ROLE_MEMBERSHIP_SOURCE_INVALID', 'role membership source 必须声明唯一来源、user.single.value、已声明 package role 与严格控制字段', sourcePath));
1232
+ }
1233
+ if (authenticatedUserRoleCode &&
1234
+ string(source.roleCode) === authenticatedUserRoleCode) {
1235
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHENTICATED_USER_ROLE_SOURCE_CONFLICT', '登录用户默认角色不能同时由 roleMembershipSource 管理', `${sourcePath}.roleCode`));
1236
+ }
1237
+ projectionSourceCodes.add(code);
1238
+ });
1239
+ relationshipGrantSources.forEach((raw, index) => {
1240
+ const source = object(raw);
1241
+ const sourcePath = `authz.relationshipGrantSources[${index}]`;
1242
+ const unknownKeys = Object.keys(source).filter(key => ![
1243
+ 'code',
1244
+ 'name',
1245
+ 'resourceCode',
1246
+ 'subject',
1247
+ 'relationCode',
1248
+ 'targetResourceCode',
1249
+ 'resourceIdField',
1250
+ 'operations',
1251
+ 'enabledField',
1252
+ 'effectiveFromField',
1253
+ 'effectiveToField',
1254
+ 'failureMode',
1255
+ ].includes(key));
1256
+ if (unknownKeys.length > 0) {
1257
+ diagnostics.push(diagnostic('APP_CONFIG_SCHEMA_INVALID', `relationship grant source 包含未知属性 ${unknownKeys.join(', ')}`, sourcePath));
1258
+ }
1259
+ const code = string(source.code);
1260
+ const resourceCode = string(source.resourceCode);
1261
+ const targetResourceCode = string(source.targetResourceCode);
1262
+ const fields = resourceFieldDeclarations.get(resourceCode);
1263
+ const subject = object(source.subject);
1264
+ const subjectType = string(subject.type);
1265
+ const subjectUnknownKeys = Object.keys(subject).filter(key => !['type', 'userIdField', 'roleCode'].includes(key));
1266
+ if (subjectUnknownKeys.length > 0) {
1267
+ diagnostics.push(diagnostic('APP_CONFIG_SCHEMA_INVALID', `relationship grant subject 包含未知属性 ${subjectUnknownKeys.join(', ')}`, `${sourcePath}.subject`));
1268
+ }
1269
+ const resourceIdPath = string(source.resourceIdField);
1270
+ const resourceIdRoot = semanticFieldPathRoot(resourceIdPath);
1271
+ const resourceIdField = fields?.get(resourceIdRoot);
1272
+ const resourceReference = object(resourceIdField?.source);
1273
+ const resourceIdValid = (resourceIdPath === 'id' && targetResourceCode === resourceCode) ||
1274
+ (resourceIdPath === `${resourceIdRoot}.value` &&
1275
+ string(resourceIdField?.type) === 'resource-ref.single' &&
1276
+ string(resourceReference.resourceCode) === targetResourceCode);
1277
+ const operations = Array.isArray(source.operations)
1278
+ ? source.operations.map(string)
1279
+ : [];
1280
+ if (!/^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/.test(code) ||
1281
+ code.length > 100 ||
1282
+ projectionSourceCodes.has(code) ||
1283
+ !string(source.name) ||
1284
+ !fields ||
1285
+ !resourceFieldDeclarations.has(targetResourceCode) ||
1286
+ !['user', 'role_membership'].includes(subjectType) ||
1287
+ !projectionUserPathValid(string(subject.userIdField), fields) ||
1288
+ (subjectType === 'role_membership' &&
1289
+ !roleCodes.has(string(subject.roleCode))) ||
1290
+ !/^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/.test(string(source.relationCode)) ||
1291
+ !resourceIdValid ||
1292
+ operations.length === 0 ||
1293
+ operations.length > 20 ||
1294
+ new Set(operations).size !== operations.length ||
1295
+ operations.some(operation => !/^[A-Za-z0-9][A-Za-z0-9:._*-]{0,63}$/.test(operation)) ||
1296
+ !projectionControlFieldsValid(source, fields)) {
1297
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_RELATIONSHIP_GRANT_SOURCE_INVALID', 'relationship grant source 必须声明唯一来源、有效主体/目标关系、常量操作与严格控制字段', sourcePath));
1298
+ }
1299
+ projectionSourceCodes.add(code);
1300
+ });
1301
+ const policyCodes = new Set();
1302
+ policies.forEach((raw, index) => {
1303
+ const policy = object(raw);
1304
+ const policyPath = `authz.dataPolicies[${index}]`;
1305
+ const unknownPolicyKeys = Object.keys(policy).filter(key => ![
1306
+ 'code',
1307
+ 'name',
1308
+ 'resourceCode',
1309
+ 'unrestrictedRoleCodes',
1310
+ 'operations',
1311
+ 'matchMode',
1312
+ 'rules',
1313
+ 'readExpression',
1314
+ 'writeBoundary',
1315
+ ].includes(key));
1316
+ if (unknownPolicyKeys.length > 0) {
1317
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_POLICY_KEYS_INVALID', 'data policy 包含未声明字段', policyPath));
1318
+ }
1319
+ const code = string(policy.code);
1320
+ const rules = Array.isArray(policy.rules) ? policy.rules : [];
1321
+ const hasReadExpression = policy.readExpression !== undefined;
1322
+ const operations = Array.isArray(policy.operations)
1323
+ ? policy.operations.map(string)
1324
+ : [];
1325
+ const operationsValid = policy.operations === undefined ||
1326
+ (Array.isArray(policy.operations) &&
1327
+ operations.length > 0 &&
1328
+ operations.length <= 4 &&
1329
+ new Set(operations).size === operations.length &&
1330
+ operations.every(operation => ['read', 'create', 'update', 'delete'].includes(operation)));
1331
+ const capabilityOnlyBoundary = policy.writeBoundary === 'capability_only' &&
1332
+ policy.matchMode === 'AND' &&
1333
+ rules.length === 0 &&
1334
+ hasReadExpression;
1335
+ const scopedBoundary = rules.length > 0 && policy.writeBoundary === undefined;
1336
+ const validPolicyForm = ['AND', 'OR'].includes(string(policy.matchMode)) &&
1337
+ (scopedBoundary || capabilityOnlyBoundary) &&
1338
+ (!hasReadExpression || policy.operations === undefined);
1339
+ if (!/^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/.test(code) ||
1340
+ policyCodes.has(code) ||
1341
+ !string(policy.name) ||
1342
+ !validPolicyForm) {
1343
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_POLICY_INVALID', 'data policy 必须声明基础 matchMode/rules;readExpression 只叠加到读取,空基础规则必须显式声明 writeBoundary=capability_only', policyPath));
1344
+ }
1345
+ if (!operationsValid) {
1346
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_POLICY_OPERATIONS_INVALID', 'operations 必须是不重复的 read/create/update/delete 非空数组', `${policyPath}.operations`));
1347
+ }
1348
+ policyCodes.add(code);
1349
+ if (!/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(string(policy.resourceCode))) {
1350
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_POLICY_RESOURCE_REQUIRED', 'data policy 必须声明唯一目标 resourceCode', `authz.dataPolicies[${index}].resourceCode`));
1351
+ }
1352
+ if (policy.unrestrictedRoleCodes !== undefined) {
1353
+ const unrestrictedRoleCodes = Array.isArray(policy.unrestrictedRoleCodes)
1354
+ ? policy.unrestrictedRoleCodes
1355
+ : [];
1356
+ const seenUnrestrictedRoleCodes = new Set();
1357
+ if (unrestrictedRoleCodes.length === 0) {
1358
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_POLICY_UNRESTRICTED_ROLES_INVALID', 'unrestrictedRoleCodes 必须是非空的已声明角色数组', `authz.dataPolicies[${index}].unrestrictedRoleCodes`));
1359
+ }
1360
+ unrestrictedRoleCodes.forEach((rawRoleCode, roleIndex) => {
1361
+ const roleCode = string(rawRoleCode);
1362
+ if (!roleCodes.has(roleCode) ||
1363
+ seenUnrestrictedRoleCodes.has(roleCode)) {
1364
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_POLICY_UNRESTRICTED_ROLE_INVALID', 'unrestrictedRoleCodes 只能引用已声明的不重复角色', `authz.dataPolicies[${index}].unrestrictedRoleCodes[${roleIndex}]`));
1365
+ }
1366
+ seenUnrestrictedRoleCodes.add(roleCode);
1367
+ });
1368
+ }
1369
+ const policyFields = resourceFieldTypes.get(string(policy.resourceCode));
1370
+ policyRuleNodes(policy, policyPath, diagnostics).forEach(({ rule, path: rulePath }) => {
1371
+ const unknownRuleKeys = Object.keys(rule).filter(key => ![
1372
+ 'subject',
1373
+ 'dimensionCode',
1374
+ 'relationCode',
1375
+ 'resourceCode',
1376
+ 'field',
1377
+ 'roleCodes',
1378
+ 'operation',
1379
+ 'valuePath',
1380
+ 'emptyMatchesAll',
1381
+ 'operator',
1382
+ 'value',
1383
+ 'operand',
1384
+ ].includes(key));
1385
+ if (unknownRuleKeys.length > 0) {
1386
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_POLICY_RULE_KEYS_INVALID', 'data policy 规则包含未声明字段', rulePath));
1387
+ }
1388
+ const subject = string(rule.subject);
1389
+ const dimensionCode = string(rule.dimensionCode);
1390
+ const relationCode = string(rule.relationCode);
1391
+ const operator = string(rule.operator);
1392
+ const dbNow = rule.operand === 'db_now';
1393
+ const nullPredicate = ['is_null', 'is_not_null'].includes(operator);
1394
+ const constantPredicate = ['eq', 'not_eq', 'in', 'not_in'].includes(operator);
1395
+ const modes = [
1396
+ subject,
1397
+ dimensionCode,
1398
+ relationCode,
1399
+ dbNow ? 'db_now' : '',
1400
+ constantPredicate || nullPredicate ? 'constant' : '',
1401
+ ].filter(Boolean);
1402
+ const validCurrentUser = subject === 'current_user';
1403
+ const validDimension = Boolean(dimensionCode) && dimensionCodes.has(dimensionCode);
1404
+ const validRelationship = Boolean(relationCode) && Boolean(string(rule.resourceCode));
1405
+ const validConstant = constantPredicate &&
1406
+ (['in', 'not_in'].includes(operator)
1407
+ ? Array.isArray(rule.value) &&
1408
+ rule.value.length > 0 &&
1409
+ rule.value.length <= 100 &&
1410
+ rule.value.every(value => typeof value === 'string' && value.length <= 2048) &&
1411
+ new Set(rule.value).size === rule.value.length
1412
+ : typeof rule.value === 'string' &&
1413
+ rule.value.length > 0 &&
1414
+ rule.value.length <= 2048);
1415
+ const validNull = nullPredicate &&
1416
+ rule.value === undefined &&
1417
+ rule.operand === undefined;
1418
+ const validDbNow = dbNow &&
1419
+ ['lt', 'lte', 'gt', 'gte'].includes(operator) &&
1420
+ rule.value === undefined &&
1421
+ policyFields?.get(string(rule.field)) === 'datetime';
1422
+ if (!/^[A-Za-z_][A-Za-z0-9_]{0,127}$/.test(string(rule.field)) ||
1423
+ modes.length !== 1 ||
1424
+ (!validCurrentUser &&
1425
+ !validDimension &&
1426
+ !validRelationship &&
1427
+ !validConstant &&
1428
+ !validNull &&
1429
+ !validDbNow)) {
1430
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_POLICY_RULE_INVALID', '策略规则必须且只能声明 current_user、dimension、relationship、constant/null 或 datetime db_now 中的一种', rulePath));
1431
+ }
1432
+ if (dbNow && !validDbNow) {
1433
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_POLICY_DB_NOW_FIELD_INVALID', 'db_now 只支持 datetime 字段及 lt/lte/gt/gte 操作符', `${rulePath}.field`));
1434
+ }
1435
+ if (rule.roleCodes !== undefined) {
1436
+ const scopedRoleCodes = Array.isArray(rule.roleCodes)
1437
+ ? rule.roleCodes
1438
+ : [];
1439
+ const seenRuleRoleCodes = new Set();
1440
+ if (scopedRoleCodes.length === 0) {
1441
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_POLICY_ROLE_CODES_INVALID', 'roleCodes 必须是非空的已声明角色数组', `${rulePath}.roleCodes`));
1442
+ }
1443
+ scopedRoleCodes.forEach((rawRoleCode, roleIndex) => {
1444
+ const roleCode = string(rawRoleCode);
1445
+ if (!roleCode ||
1446
+ !roleCodes.has(roleCode) ||
1447
+ seenRuleRoleCodes.has(roleCode)) {
1448
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_POLICY_ROLE_CODE_INVALID', '数据策略的 roleCodes 只能引用已声明的不重复角色', `${rulePath}.roleCodes[${roleIndex}]`));
1449
+ }
1450
+ seenRuleRoleCodes.add(roleCode);
1451
+ });
1452
+ }
1453
+ });
1454
+ });
1455
+ scopeSources.forEach((rawSource, sourceIndex) => {
1456
+ const source = object(rawSource);
1457
+ if (string(source.failureMode) !== 'last_known_good')
1458
+ return;
1459
+ const sourceDimensions = new Set((Array.isArray(source.grants) ? source.grants : []).map(rawGrant => string(object(rawGrant).dimensionCode)));
1460
+ const matchingRules = policies.flatMap(rawPolicy => {
1461
+ return policyRuleNodes(object(rawPolicy), 'authz.dataPolicies', []).map(entry => entry.rule).filter(rawRule => sourceDimensions.has(string(rawRule.dimensionCode)));
1462
+ });
1463
+ if (matchingRules.length === 0 ||
1464
+ matchingRules.some(rawRule => string(object(rawRule).operation) !== 'read')) {
1465
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_SCOPE_SOURCE_LAST_KNOWN_GOOD_UNSAFE', 'last_known_good 只能用于全部显式声明 operation=read 的低风险数据策略;其他场景使用 strict', `authz.scopeSources[${sourceIndex}].failureMode`));
1466
+ }
1467
+ });
1468
+ dataResources.forEach((raw, index) => {
1469
+ const policyCode = string(object(raw).dataPolicyCode);
1470
+ if (policyCode && !policyCodes.has(policyCode)) {
1471
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_DATA_POLICY_NOT_FOUND', `DataResource 引用了未声明的数据策略: ${policyCode}`, `data.resources[${index}].dataPolicyCode`));
1472
+ }
1473
+ });
1474
+ diagnostics.push(...validateAuthzSemanticBindings({
1475
+ resources: dataResources,
1476
+ dimensions,
1477
+ scopeSources,
1478
+ policies,
1479
+ }));
1480
+ validateAuthorizationTransitions(authz.authorizationTransitions, roleCodes, diagnostics);
1481
+ }
1482
+ validateCapabilityClosure(appCode, config, declaredCapabilities, diagnostics);
1483
+ const events = object(config.events);
1484
+ const eventDataResources = Array.isArray(object(config.data).resources)
1485
+ ? object(config.data).resources
1486
+ : [];
1487
+ const eventResourceFields = new Map(eventDataResources.map(rawResource => {
1488
+ const resource = object(rawResource);
1489
+ const fields = Array.isArray(object(resource.schema).fields)
1490
+ ? object(resource.schema).fields
1491
+ : [];
1492
+ return [
1493
+ string(resource.code),
1494
+ new Map(fields.map(rawField => {
1495
+ const field = object(rawField);
1496
+ return [string(field.code), string(field.type)];
1497
+ })),
1498
+ ];
1499
+ }));
1500
+ const workflowSummaryResourceFields = new Map(eventDataResources.map(rawResource => {
1501
+ const resource = object(rawResource);
1502
+ const fields = Array.isArray(object(resource.schema).fields)
1503
+ ? object(resource.schema).fields
1504
+ : [];
1505
+ const surfaceFields = object(object(resource.surface).fields);
1506
+ return [
1507
+ string(resource.code),
1508
+ new Map(fields.map(rawField => {
1509
+ const field = object(rawField);
1510
+ const code = string(field.code);
1511
+ const surface = object(surfaceFields[code]);
1512
+ const maxLength = typeof field.maxLength === 'number' &&
1513
+ Number.isFinite(field.maxLength)
1514
+ ? field.maxLength
1515
+ : undefined;
1516
+ return [
1517
+ code,
1518
+ {
1519
+ type: string(field.type),
1520
+ system: surface.system === true || field.system === true,
1521
+ ...(maxLength !== undefined ? { maxLength } : {}),
1522
+ },
1523
+ ];
1524
+ })),
1525
+ ];
1526
+ }));
1527
+ const platformEventTypes = new Set(PLATFORM_EVENT_TYPES_V2);
1528
+ const declaredEventTypes = new Set();
1529
+ const applicationEventSchemas = new Map();
1530
+ const schemaKeys = new Set();
1531
+ (Array.isArray(events.schemas) ? events.schemas : []).forEach((item, index) => {
1532
+ const schema = object(item);
1533
+ const path = `events.schemas[${index}]`;
1534
+ const eventType = string(schema.eventType);
1535
+ const dataSchemaVersion = string(schema.dataSchemaVersion);
1536
+ const jsonSchema = object(schema.jsonSchema);
1537
+ const sensitiveFields = Array.isArray(schema.sensitiveFields)
1538
+ ? schema.sensitiveFields.map(value => string(value))
1539
+ : [];
1540
+ const sensitiveFieldsValid = (schema.sensitiveFields === undefined ||
1541
+ Array.isArray(schema.sensitiveFields)) &&
1542
+ sensitiveFields.length <= 100 &&
1543
+ new Set(sensitiveFields).size === sensitiveFields.length &&
1544
+ sensitiveFields.every(field => /^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*){0,7}$/.test(field) && eventSchemaDeclaresPath(jsonSchema, field));
1545
+ const schemaKey = `${eventType}:${dataSchemaVersion}`;
1546
+ let schemaBytes = Number.POSITIVE_INFINITY;
1547
+ try {
1548
+ schemaBytes = Buffer.byteLength(JSON.stringify(schema.jsonSchema), 'utf8');
1549
+ }
1550
+ catch {
1551
+ // Reported by the combined validation below.
1552
+ }
1553
+ const schemaValid = !(!EVENT_TYPE_PATTERN.test(eventType) ||
1554
+ eventType.startsWith('openxiangda.') ||
1555
+ !eventType.startsWith(`${appCode}.`) ||
1556
+ !EVENT_DATA_SCHEMA_VERSION_PATTERN.test(dataSchemaVersion) ||
1557
+ object(schema.jsonSchema) !== schema.jsonSchema ||
1558
+ jsonSchema.type !== 'object' ||
1559
+ schemaBytes > 256 * 1024 ||
1560
+ !sensitiveFieldsValid ||
1561
+ schemaKeys.has(schemaKey) ||
1562
+ declaredEventTypes.has(eventType));
1563
+ if (!schemaValid) {
1564
+ diagnostics.push(diagnostic('APP_CONFIG_EVENT_SCHEMA_INVALID', '应用事件 Schema 必须使用 appCode 命名空间、major 事件类型、SemVer dataSchemaVersion、不超过 256 KiB 的 object JSON Schema;sensitiveFields 最多 100 个、路径规范且必须指向已声明字段,且版本不可重复', path));
1565
+ }
1566
+ if (schemaValid) {
1567
+ declaredEventTypes.add(eventType);
1568
+ applicationEventSchemas.set(eventType, jsonSchema);
1569
+ }
1570
+ schemaKeys.add(schemaKey);
1571
+ });
1572
+ const allowedEventTypes = new Set([...platformEventTypes, ...declaredEventTypes]);
1573
+ const captureFieldsByResource = new Map();
1574
+ const subscriptionCodes = new Set();
1575
+ if (config.events !== undefined && !Array.isArray(events.subscriptions)) {
1576
+ diagnostics.push(diagnostic('APP_CONFIG_EVENT_SUBSCRIPTIONS_REQUIRED', 'events.subscriptions 必须是数组', 'events.subscriptions'));
1577
+ }
1578
+ else {
1579
+ (Array.isArray(events.subscriptions) ? events.subscriptions : []).forEach((item, index) => {
1580
+ const subscription = object(item);
1581
+ const path = `events.subscriptions[${index}]`;
1582
+ if ('environmentKey' in subscription) {
1583
+ diagnostics.push(diagnostic('APP_CONFIG_ENVIRONMENT_OVERRIDE_FORBIDDEN', '事件声明属于环境中立 AppVersion,不能声明 environmentKey', `${path}.environmentKey`));
1584
+ }
1585
+ const subscriptionCode = string(subscription.code);
1586
+ if (!/^[a-z][a-z0-9-]*$/.test(subscriptionCode) ||
1587
+ subscriptionCodes.has(subscriptionCode)) {
1588
+ diagnostics.push(diagnostic('APP_CONFIG_EVENT_SUBSCRIPTION_CODE_INVALID', '事件订阅 code 必须是唯一的 kebab-case', `${path}.code`));
1589
+ }
1590
+ if (!Array.isArray(subscription.eventTypes) ||
1591
+ subscription.eventTypes.length === 0 ||
1592
+ subscription.eventTypes.length > 20 ||
1593
+ new Set(subscription.eventTypes.map(string)).size !==
1594
+ subscription.eventTypes.length ||
1595
+ subscription.eventTypes.some(eventType => !allowedEventTypes.has(string(eventType)))) {
1596
+ diagnostics.push(diagnostic('APP_CONFIG_EVENT_TYPES_INVALID', '事件订阅必须声明 1 到 20 个不重复且已注册的 v2 eventTypes', `${path}.eventTypes`));
1597
+ }
1598
+ if ('endpointPath' in subscription) {
1599
+ diagnostics.push(diagnostic('APP_CONFIG_EVENT_ENDPOINT_DECLARATION_FORBIDDEN', '事件订阅 endpointPath 由 code 生成,应用不能重复声明', `${path}.endpointPath`));
1600
+ }
1601
+ subscriptionCodes.add(subscriptionCode);
1602
+ const filter = object(subscription.filter);
1603
+ const filterKeys = Object.keys(filter);
1604
+ const resourceCodes = Array.isArray(filter.resourceCodes)
1605
+ ? filter.resourceCodes.map(string)
1606
+ : [];
1607
+ if (filterKeys.some(key => !['resourceCodes', 'subject', 'changedFields', 'changes', 'where'].includes(key)) ||
1608
+ resourceCodes.length > 20 ||
1609
+ new Set(resourceCodes).size !== resourceCodes.length ||
1610
+ resourceCodes.some(code => !eventResourceFields.has(code))) {
1611
+ diagnostics.push(diagnostic('APP_CONFIG_EVENT_FILTER_RESOURCE_INVALID', '事件过滤 resourceCodes 必须引用最多 20 个不重复的已声明资源', `${path}.filter`));
1612
+ }
1613
+ const allowedFields = new Set();
1614
+ if (resourceCodes.length > 0) {
1615
+ const fieldSets = resourceCodes
1616
+ .map(code => eventResourceFields.get(code))
1617
+ .filter(Boolean);
1618
+ for (const field of fieldSets[0]?.keys() || []) {
1619
+ if (fieldSets.every(fields => fields.has(field)))
1620
+ allowedFields.add(field);
1621
+ }
1622
+ }
1623
+ const subject = object(filter.subject);
1624
+ if (Object.keys(subject).length > 1 ||
1625
+ Object.keys(subject).some(key => !['equals', 'prefix'].includes(key)) ||
1626
+ Object.values(subject).some(value => typeof value !== 'string')) {
1627
+ diagnostics.push(diagnostic('APP_CONFIG_EVENT_FILTER_SUBJECT_INVALID', 'subject 过滤只能声明 equals 或 prefix 之一', `${path}.filter.subject`));
1628
+ }
1629
+ const referencedFields = new Set();
1630
+ const changedFields = object(filter.changedFields);
1631
+ const filterComplexity = {
1632
+ atoms: (resourceCodes.length > 0 ? 1 : 0) +
1633
+ (Object.keys(subject).length > 0 ? 1 : 0),
1634
+ };
1635
+ for (const [group, rawFields] of Object.entries(changedFields)) {
1636
+ const fields = Array.isArray(rawFields) ? rawFields.map(string) : [];
1637
+ fields.forEach(field => referencedFields.add(field));
1638
+ if (['anyOf', 'allOf', 'noneOf'].includes(group)) {
1639
+ filterComplexity.atoms += 1;
1640
+ }
1641
+ if (!['anyOf', 'allOf', 'noneOf'].includes(group) ||
1642
+ fields.length < 1 ||
1643
+ fields.length > 32 ||
1644
+ new Set(fields).size !== fields.length) {
1645
+ diagnostics.push(diagnostic('APP_CONFIG_EVENT_CHANGED_FIELDS_INVALID', 'changedFields 只支持 anyOf/allOf/noneOf,每组 1 到 32 个不重复字段', `${path}.filter.changedFields.${group}`));
1646
+ }
1647
+ }
1648
+ const changes = Array.isArray(filter.changes) ? filter.changes : [];
1649
+ filterComplexity.atoms += changes.length;
1650
+ if (changes.length > 16) {
1651
+ diagnostics.push(diagnostic('APP_CONFIG_EVENT_CHANGE_FILTERS_EXCEEDED', '单个订阅最多声明 16 个字段变化条件', `${path}.filter.changes`));
1652
+ }
1653
+ changes.forEach((change, changeIndex) => {
1654
+ referencedFields.add(string(object(change).field));
1655
+ validateEventChangeFilter(change, `${path}.filter.changes[${changeIndex}]`, allowedFields, diagnostics);
1656
+ });
1657
+ if (filter.where !== undefined) {
1658
+ validateEventFilterCondition(filter.where, `${path}.filter.where`, allowedFields, diagnostics, filterComplexity);
1659
+ }
1660
+ if (filterComplexity.atoms > 16) {
1661
+ diagnostics.push(diagnostic('APP_CONFIG_EVENT_FILTER_ATOMS_EXCEEDED', '单个订阅最多包含 16 个原子过滤条件', `${path}.filter`));
1662
+ }
1663
+ const payload = object(subscription.payload);
1664
+ const projectionFields = Array.isArray(payload.fields)
1665
+ ? payload.fields.map(string)
1666
+ : [];
1667
+ projectionFields.forEach(field => referencedFields.add(field));
1668
+ if (Object.keys(payload).some(key => !['includeChanges', 'fields'].includes(key)) ||
1669
+ (payload.includeChanges !== undefined &&
1670
+ typeof payload.includeChanges !== 'boolean') ||
1671
+ projectionFields.length > 32 ||
1672
+ new Set(projectionFields).size !== projectionFields.length ||
1673
+ referencedFields.size > 0 && resourceCodes.length === 0 ||
1674
+ [...referencedFields].some(field => !allowedFields.has(field))) {
1675
+ diagnostics.push(diagnostic('APP_CONFIG_EVENT_PROJECTION_INVALID', '字段过滤和 payload.fields 必须绑定 resourceCodes、引用各目标资源共有字段,且单订阅投影最多 32 个字段', `${path}.payload`));
1676
+ }
1677
+ for (const resourceCode of resourceCodes) {
1678
+ const captured = captureFieldsByResource.get(resourceCode) || new Set();
1679
+ referencedFields.forEach(field => captured.add(field));
1680
+ captureFieldsByResource.set(resourceCode, captured);
1681
+ }
1682
+ if (subscription.platformAccess !== undefined) {
1683
+ const access = object(subscription.platformAccess);
1684
+ const notification = object(access.notification);
1685
+ const copies = Array.isArray(access.managedFileCopies)
1686
+ ? access.managedFileCopies
1687
+ : [];
1688
+ let copiesInvalid = access.managedFileCopies !== undefined &&
1689
+ (copies.length === 0 || copies.length > 16);
1690
+ const copyKeys = new Set();
1691
+ for (const rawCopy of copies) {
1692
+ const copy = object(rawCopy);
1693
+ const sourceResourceCode = string(copy.sourceResourceCode);
1694
+ const targetResourceCode = string(copy.targetResourceCode);
1695
+ const sourceFields = Array.isArray(copy.sourceFieldCodes)
1696
+ ? copy.sourceFieldCodes.map(string)
1697
+ : [];
1698
+ const targetFields = Array.isArray(copy.targetFieldCodes)
1699
+ ? copy.targetFieldCodes.map(string)
1700
+ : [];
1701
+ const source = eventResourceFields.get(sourceResourceCode);
1702
+ const target = eventResourceFields.get(targetResourceCode);
1703
+ copiesInvalid ||=
1704
+ copy.mode !== 'copy' ||
1705
+ !source ||
1706
+ !target ||
1707
+ sourceFields.length === 0 ||
1708
+ targetFields.length === 0 ||
1709
+ sourceFields.length > 16 ||
1710
+ targetFields.length > 16 ||
1711
+ new Set(sourceFields).size !== sourceFields.length ||
1712
+ new Set(targetFields).size !== targetFields.length ||
1713
+ sourceFields.some(field => !['file', 'image'].includes(source?.get(field) || '')) ||
1714
+ targetFields.some(field => !['file', 'image'].includes(target?.get(field) || '')) ||
1715
+ sourceFields.length !== targetFields.length ||
1716
+ sourceFields.some((field, i) => source?.get(field) !== target?.get(targetFields[i] || '')) ||
1717
+ Object.keys(copy).some(key => ![
1718
+ 'mode',
1719
+ 'sourceResourceCode',
1720
+ 'sourceFieldCodes',
1721
+ 'targetResourceCode',
1722
+ 'targetFieldCodes',
1723
+ ].includes(key));
1724
+ const key = `${sourceResourceCode}:${sourceFields.join(',')}=>${targetResourceCode}:${targetFields.join(',')}`;
1725
+ if (copyKeys.has(key))
1726
+ copiesInvalid = true;
1727
+ copyKeys.add(key);
1728
+ }
1729
+ if (Object.keys(access).some(key => !['notification', 'managedFileCopies'].includes(key)) ||
1730
+ (access.notification !== undefined &&
1731
+ (notification.mode !== 'business-standard' ||
1732
+ Object.keys(notification).some(key => key !== 'mode'))) ||
1733
+ (access.notification === undefined && access.managedFileCopies === undefined) ||
1734
+ copiesInvalid) {
1735
+ diagnostics.push(diagnostic('APP_CONFIG_EVENT_PLATFORM_ACCESS_INVALID', '事件订阅 platformAccess 只能显式声明标准业务通知或有界托管文件复制依赖', `${path}.platformAccess`));
1736
+ }
1737
+ }
1738
+ const delivery = {
1739
+ timeoutMs: 10_000,
1740
+ maxAttempts: 8,
1741
+ initialBackoffMs: 1_000,
1742
+ maxBackoffMs: 300_000,
1743
+ ordering: 'none',
1744
+ concurrency: 10,
1745
+ ...object(subscription.delivery),
1746
+ };
1747
+ if (!Number.isInteger(delivery.timeoutMs) ||
1748
+ Number(delivery.timeoutMs) < 1000 ||
1749
+ Number(delivery.timeoutMs) > 30_000 ||
1750
+ !Number.isInteger(delivery.maxAttempts) ||
1751
+ Number(delivery.maxAttempts) < 1 ||
1752
+ Number(delivery.maxAttempts) > 12 ||
1753
+ !Number.isInteger(delivery.initialBackoffMs) ||
1754
+ Number(delivery.initialBackoffMs) < 1000 ||
1755
+ !Number.isInteger(delivery.maxBackoffMs) ||
1756
+ Number(delivery.maxBackoffMs) < Number(delivery.initialBackoffMs) ||
1757
+ Number(delivery.maxBackoffMs) > 1_800_000 ||
1758
+ !['none', 'record', 'workflow-instance'].includes(string(delivery.ordering)) ||
1759
+ !Number.isInteger(delivery.concurrency) ||
1760
+ Number(delivery.concurrency) < 1 ||
1761
+ Number(delivery.concurrency) > 50) {
1762
+ diagnostics.push(diagnostic('APP_CONFIG_EVENT_DELIVERY_POLICY_INVALID', 'delivery 超出 timeout、attempt、backoff、ordering 或 concurrency 的平台上限', `${path}.delivery`));
1763
+ }
1764
+ });
1765
+ }
1766
+ for (const [resourceCode, fields] of captureFieldsByResource) {
1767
+ if (fields.size > 64) {
1768
+ diagnostics.push(diagnostic('APP_CONFIG_EVENT_CAPTURE_PLAN_EXCEEDED', `资源 ${resourceCode} 的订阅捕获字段并集超过 64 个`, 'events.subscriptions'));
1769
+ }
1770
+ }
1771
+ const timerCodes = new Set();
1772
+ (Array.isArray(events.timers) ? events.timers : []).forEach((item, index) => {
1773
+ const timer = object(item);
1774
+ const path = `events.timers[${index}]`;
1775
+ if ('environmentKey' in timer) {
1776
+ diagnostics.push(diagnostic('APP_CONFIG_ENVIRONMENT_OVERRIDE_FORBIDDEN', '定时声明属于环境中立 AppVersion,不能声明 environmentKey', `${path}.environmentKey`));
1777
+ }
1778
+ if (!/^[a-z][a-z0-9-]*$/.test(string(timer.code)) ||
1779
+ timerCodes.has(string(timer.code))) {
1780
+ diagnostics.push(diagnostic('APP_CONFIG_TIMER_CODE_INVALID', '定时订阅 code 必须是 kebab-case', `${path}.code`));
1781
+ }
1782
+ timerCodes.add(string(timer.code));
1783
+ if (!declaredEventTypes.has(string(timer.eventType)) ||
1784
+ !string(timer.cronExpression) ||
1785
+ !string(timer.timezone) ||
1786
+ (timer.misfirePolicy !== undefined &&
1787
+ timer.misfirePolicy !== 'coalesce_one')) {
1788
+ diagnostics.push(diagnostic('APP_CONFIG_TIMER_SCHEDULE_REQUIRED', '定时订阅必须引用应用事件 Schema,并声明六段 cronExpression、IANA timezone 和 coalesce_one misfire policy', path));
1789
+ }
1790
+ const cronExpression = string(timer.cronExpression);
1791
+ const timezone = string(timer.timezone);
1792
+ if (cronExpression && timezone) {
1793
+ try {
1794
+ const first = CronExpressionParser.parse(cronExpression, {
1795
+ currentDate: '2026-01-01T00:00:00.000Z',
1796
+ tz: timezone,
1797
+ strict: true,
1798
+ hashSeed: string(timer.code),
1799
+ });
1800
+ const firstDueAt = first.next().toDate();
1801
+ const secondDueAt = first.next().toDate();
1802
+ if (secondDueAt.getTime() - firstDueAt.getTime() < 60_000) {
1803
+ diagnostics.push(diagnostic('APP_CONFIG_TIMER_FREQUENCY_TOO_HIGH', '定时订阅最短间隔为 60 秒', `${path}.cronExpression`));
1804
+ }
1805
+ }
1806
+ catch (error) {
1807
+ diagnostics.push(diagnostic('APP_CONFIG_TIMER_CRON_INVALID', `定时订阅必须使用明确的六段 Cron 与有效 IANA 时区:${error instanceof Error ? error.message : String(error)}`, cronExpression ? `${path}.cronExpression` : `${path}.timezone`));
1808
+ }
1809
+ }
1810
+ if (timer.payload === null ||
1811
+ typeof timer.payload !== 'object' ||
1812
+ Array.isArray(timer.payload)) {
1813
+ diagnostics.push(diagnostic('APP_CONFIG_TIMER_PAYLOAD_INVALID', '定时事件 payload 必须是对象', `${path}.payload`));
1814
+ }
1815
+ else if (Buffer.byteLength(JSON.stringify(timer.payload || {}), 'utf8') > 64 * 1024) {
1816
+ diagnostics.push(diagnostic('APP_CONFIG_TIMER_PAYLOAD_TOO_LARGE', '定时事件 payload 不能超过 64 KiB', `${path}.payload`));
1817
+ }
1818
+ else if (!eventDataMatchesSchema(applicationEventSchemas.get(string(timer.eventType)), timer.payload)) {
1819
+ diagnostics.push(diagnostic('APP_CONFIG_TIMER_PAYLOAD_SCHEMA_INVALID', '定时事件 payload 必须完整满足所引用的应用事件 JSON Schema', `${path}.payload`));
1820
+ }
1821
+ });
1822
+ const dateTriggerCodes = new Set();
1823
+ (Array.isArray(events.dateTriggers) ? events.dateTriggers : []).forEach((item, index) => {
1824
+ const trigger = object(item);
1825
+ const path = `events.dateTriggers[${index}]`;
1826
+ const code = string(trigger.code);
1827
+ const resourceCode = string(trigger.resourceCode);
1828
+ const field = string(trigger.field);
1829
+ const fieldType = eventResourceFields.get(resourceCode)?.get(field);
1830
+ const payload = object(trigger.payload);
1831
+ let payloadBytes = Number.POSITIVE_INFINITY;
1832
+ try {
1833
+ payloadBytes = Buffer.byteLength(JSON.stringify(trigger.payload || {}), 'utf8');
1834
+ }
1835
+ catch {
1836
+ // Reported by the combined validation below.
1837
+ }
1838
+ if (!/^[a-z][a-z0-9-]*$/.test(code) ||
1839
+ dateTriggerCodes.has(code) ||
1840
+ !eventResourceFields.has(resourceCode) ||
1841
+ !['date', 'datetime'].includes(fieldType || '') ||
1842
+ !validDateTriggerOffset(trigger.offset) ||
1843
+ !declaredEventTypes.has(string(trigger.eventType)) ||
1844
+ trigger.payload === null ||
1845
+ typeof trigger.payload !== 'object' ||
1846
+ Array.isArray(trigger.payload) ||
1847
+ payloadBytes > 64 * 1024 ||
1848
+ Object.keys(payload).some(key => DATE_TRIGGER_RESERVED_DATA_FIELDS.has(key)) ||
1849
+ !eventDataMatchesSchema(applicationEventSchemas.get(string(trigger.eventType)), {
1850
+ ...payload,
1851
+ triggerCode: code,
1852
+ resourceCode,
1853
+ recordId: '00000000-0000-4000-8000-000000000000',
1854
+ recordRevision: 1,
1855
+ field,
1856
+ dueAt: '2026-01-01T00:00:00.000Z',
1857
+ })) {
1858
+ diagnostics.push(diagnostic('APP_CONFIG_DATE_TRIGGER_INVALID', 'dateTrigger 必须使用唯一 code、date/datetime 字段、十年内 ISO-8601 offset、已注册应用事件、无保留字段且完整生成数据满足 JSON Schema', path));
1859
+ }
1860
+ dateTriggerCodes.add(code);
1861
+ });
1862
+ const workflows = object(config.workflows);
1863
+ if (config.workflows !== undefined &&
1864
+ (!Array.isArray(workflows.definitions) ||
1865
+ !Array.isArray(workflows.bindings) ||
1866
+ !Array.isArray(workflows.activations))) {
1867
+ diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_DECLARATIONS_REQUIRED', 'workflows 必须同时声明 definitions、bindings 和 activations 数组', 'workflows'));
1868
+ }
1869
+ else {
1870
+ const definitions = Array.isArray(workflows.definitions)
1871
+ ? workflows.definitions
1872
+ : [];
1873
+ const bindings = Array.isArray(workflows.bindings) ? workflows.bindings : [];
1874
+ const activations = Array.isArray(workflows.activations)
1875
+ ? workflows.activations
1876
+ : [];
1877
+ const providers = Array.isArray(workflows.providers)
1878
+ ? workflows.providers
1879
+ : [];
1880
+ const editableParameters = Array.isArray(workflows.editableParameters)
1881
+ ? workflows.editableParameters
1882
+ : [];
1883
+ const frontendRoutesByCode = new Map((Array.isArray(frontend.routes) ? frontend.routes : []).map(routeValue => {
1884
+ const route = object(routeValue);
1885
+ return [string(route.code), route];
1886
+ }));
1887
+ const definitionByKey = new Map();
1888
+ const bindingByKey = new Map();
1889
+ const providerKeys = new Set();
1890
+ providers.forEach((item, index) => {
1891
+ const provider = object(item);
1892
+ const path = `workflows.providers[${index}]`;
1893
+ const code = string(provider.code);
1894
+ const endpointPath = string(provider.endpointPath);
1895
+ const key = code;
1896
+ if ('environmentKey' in provider) {
1897
+ diagnostics.push(diagnostic('APP_CONFIG_ENVIRONMENT_OVERRIDE_FORBIDDEN', 'Workflow Provider 属于环境中立 AppVersion,不能声明 environmentKey', `${path}.environmentKey`));
1898
+ }
1899
+ if (!/^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/.test(code) ||
1900
+ !/^\/[A-Za-z0-9/_-]+$/.test(endpointPath) ||
1901
+ endpointPath.split('/').includes('..')) {
1902
+ diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_PROVIDER_INVALID', 'App Provider 必须声明合法 code 和应用内 endpointPath', path));
1903
+ }
1904
+ if (providerKeys.has(key)) {
1905
+ diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_PROVIDER_DUPLICATE', `App Provider 重复: ${key}`, path));
1906
+ }
1907
+ providerKeys.add(key);
1908
+ });
1909
+ const editableParameterCodes = new Set();
1910
+ editableParameters.forEach((item, index) => {
1911
+ const parameter = object(item);
1912
+ const code = string(parameter.code);
1913
+ const path = `workflows.editableParameters[${index}]`;
1914
+ const allowedRoleCodes = Array.isArray(parameter.allowedRoleCodes)
1915
+ ? parameter.allowedRoleCodes.map(string)
1916
+ : [];
1917
+ const allowedProviderCodes = Array.isArray(parameter.allowedProviderCodes)
1918
+ ? parameter.allowedProviderCodes.map(string)
1919
+ : [];
1920
+ if (!OPERATION_CODE_PATTERN.test(code) ||
1921
+ editableParameterCodes.has(code) ||
1922
+ !string(parameter.workflowCode) ||
1923
+ !string(parameter.bindingKey) ||
1924
+ !string(parameter.label) ||
1925
+ !['role_code', 'user_ids', 'provider_code'].includes(string(parameter.valueType)) ||
1926
+ new Set(allowedRoleCodes).size !== allowedRoleCodes.length ||
1927
+ new Set(allowedProviderCodes).size !== allowedProviderCodes.length) {
1928
+ diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_EDITABLE_PARAMETER_INVALID', 'Workflow editable parameter 必须声明唯一 code、workflow/binding、label 和受支持的 valueType', path));
1929
+ }
1930
+ editableParameterCodes.add(code);
1931
+ });
1932
+ const backendOperationsByCode = new Map((Array.isArray(backend.operations) ? backend.operations : []).map(raw => {
1933
+ const operation = object(raw);
1934
+ return [string(operation.code), operation];
1935
+ }));
1936
+ const dataResourcesByCode = new Map((Array.isArray(data.resources) ? data.resources : []).map(raw => {
1937
+ const resource = object(raw);
1938
+ return [string(resource.code), resource];
1939
+ }));
1940
+ const validateNamedOperationIntent = (rawIntent, intentKind, definition, path) => {
1941
+ const intent = object(rawIntent);
1942
+ const operationCode = string(intent.operationCode);
1943
+ const operation = backendOperationsByCode.get(operationCode);
1944
+ const inputs = object(intent.inputs);
1945
+ const output = object(intent.output);
1946
+ const subjectResource = dataResourcesByCode.get(definition.subject?.resourceCode);
1947
+ const subjectSchema = object(subjectResource?.schema);
1948
+ const subjectFields = new Set((Array.isArray(subjectSchema.fields) ? subjectSchema.fields : []).map(field => string(object(field).code)));
1949
+ const requestSchema = object(operation?.requestSchema);
1950
+ const requestProperties = object(requestSchema.properties);
1951
+ const requestRequired = new Set(Array.isArray(requestSchema.required)
1952
+ ? requestSchema.required.map(string)
1953
+ : []);
1954
+ const responseProperties = object(object(operation?.responseSchema).properties);
1955
+ const operationWorkflow = object(object(operation?.platformAccess).workflow);
1956
+ const operationWorkflowCodes = Array.isArray(operationWorkflow.codes)
1957
+ ? operationWorkflow.codes.map(string)
1958
+ : [];
1959
+ if (Object.keys(intent).some(key => !['operationCode', 'inputs', 'output'].includes(key)) ||
1960
+ !OPERATION_CODE_PATTERN.test(operationCode) ||
1961
+ !operation ||
1962
+ string(operation.method) !== 'POST' ||
1963
+ !operationWorkflowCodes.includes(definition.code)) {
1964
+ diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_NAMED_OPERATION_INVALID', '标准 Workflow named-operation 必须引用同一应用中声明目标 Workflow platformAccess 的 POST operation', path));
1965
+ }
1966
+ const inputEntries = Object.entries(inputs);
1967
+ const propertyCodePattern = /^[A-Za-z][A-Za-z0-9_]{0,127}$/;
1968
+ const fieldCodes = new Set();
1969
+ const sourceCounts = new Map();
1970
+ let inputsInvalid = !isRecord(intent.inputs) ||
1971
+ inputEntries.length === 0 ||
1972
+ inputEntries.length > 64;
1973
+ for (const [inputCode, rawBinding] of inputEntries) {
1974
+ const binding = object(rawBinding);
1975
+ const source = string(binding.source);
1976
+ const allowedKeys = source === 'field' ? ['source', 'fieldCode'] : ['source'];
1977
+ inputsInvalid ||=
1978
+ !propertyCodePattern.test(inputCode) ||
1979
+ !(inputCode in requestProperties) ||
1980
+ ![
1981
+ 'field',
1982
+ 'idempotency-key',
1983
+ 'current-user-reference',
1984
+ 'subject-id',
1985
+ 'subject-revision',
1986
+ 'requested-at',
1987
+ ].includes(source) ||
1988
+ Object.keys(binding).some(key => !allowedKeys.includes(key));
1989
+ sourceCounts.set(source, (sourceCounts.get(source) || 0) + 1);
1990
+ if (source === 'field') {
1991
+ const fieldCode = string(binding.fieldCode);
1992
+ inputsInvalid ||=
1993
+ !propertyCodePattern.test(fieldCode) ||
1994
+ !subjectFields.has(fieldCode) ||
1995
+ fieldCodes.has(fieldCode);
1996
+ fieldCodes.add(fieldCode);
1997
+ }
1998
+ }
1999
+ inputsInvalid ||=
2000
+ [...requestRequired].some(inputCode => !(inputCode in inputs)) ||
2001
+ (sourceCounts.get('idempotency-key') || 0) !== 1 ||
2002
+ ![...inputEntries].some(([inputCode, rawBinding]) => string(object(rawBinding).source) === 'idempotency-key' &&
2003
+ requestRequired.has(inputCode));
2004
+ if (intentKind === 'create') {
2005
+ inputsInvalid ||=
2006
+ (sourceCounts.get('subject-id') || 0) > 0 ||
2007
+ (sourceCounts.get('subject-revision') || 0) > 0;
2008
+ }
2009
+ else {
2010
+ inputsInvalid ||=
2011
+ (sourceCounts.get('subject-id') || 0) !== 1 ||
2012
+ (sourceCounts.get('subject-revision') || 0) !== 1;
2013
+ }
2014
+ if (inputsInvalid) {
2015
+ diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_NAMED_OPERATION_INPUT_INVALID', 'named-operation 必须以唯一且完整的顶层 input binding 覆盖请求必填字段、平台幂等键及对应 create/existing 身份来源', `${path}.inputs`));
2016
+ }
2017
+ const subjectIdOutput = string(output.subjectId);
2018
+ const subjectRevisionOutput = string(output.subjectRevision);
2019
+ const processCommandOutput = string(output.processCommand);
2020
+ if (!isRecord(intent.output) ||
2021
+ Object.keys(output).some(key => !['subjectId', 'subjectRevision', 'processCommand'].includes(key)) ||
2022
+ !propertyCodePattern.test(subjectIdOutput) ||
2023
+ !(subjectIdOutput in responseProperties) ||
2024
+ (output.subjectRevision !== undefined &&
2025
+ (!propertyCodePattern.test(subjectRevisionOutput) ||
2026
+ !(subjectRevisionOutput in responseProperties))) ||
2027
+ (output.processCommand !== undefined &&
2028
+ (!propertyCodePattern.test(processCommandOutput) ||
2029
+ !(processCommandOutput in responseProperties)))) {
2030
+ diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_NAMED_OPERATION_OUTPUT_INVALID', 'named-operation output 必须引用 responseSchema 中的顶层 subjectId、可选 revision 和可选 processCommand 属性', `${path}.output`));
2031
+ }
2032
+ return fieldCodes;
2033
+ };
2034
+ definitions.forEach((item, index) => {
2035
+ const declaration = object(item);
2036
+ const definition = object(declaration.definition);
2037
+ const version = Number(declaration.version);
2038
+ const path = `workflows.definitions[${index}]`;
2039
+ const launch = object(declaration.launch);
2040
+ if (Object.keys(declaration).some(key => !['version', 'definition', 'launch', 'detailRouteCode'].includes(key))) {
2041
+ diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_DEFINITION_DECLARATION_INVALID', 'Workflow definition declaration 只能声明 version、definition、launch 和 detailRouteCode', path));
2042
+ }
2043
+ if (declaration.launch !== undefined &&
2044
+ (Object.keys(launch).some(key => !['mode', 'submission'].includes(key)) ||
2045
+ ![
2046
+ 'standalone',
2047
+ 'custom-page',
2048
+ 'hidden-handoff',
2049
+ 'work-center-only',
2050
+ ].includes(string(launch.mode)))) {
2051
+ diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_LAUNCH_MODE_INVALID', 'Workflow launch.mode 必须是 standalone、custom-page、hidden-handoff 或 work-center-only', `${path}.launch.mode`));
2052
+ }
2053
+ if (launch.submission !== undefined) {
2054
+ const submission = object(launch.submission);
2055
+ const createFields = submission.create
2056
+ ? validateNamedOperationIntent(submission.create, 'create', definition, `${path}.launch.submission.create`)
2057
+ : new Set();
2058
+ const existingFields = submission.existing
2059
+ ? validateNamedOperationIntent(submission.existing, 'existing', definition, `${path}.launch.submission.existing`)
2060
+ : new Set();
2061
+ const contexts = Array.isArray(submission.context)
2062
+ ? submission.context
2063
+ : [];
2064
+ const contextKeys = new Set();
2065
+ let contextInvalid = contexts.length > 16;
2066
+ for (const rawContext of contexts) {
2067
+ const context = object(rawContext);
2068
+ const queryParameter = string(context.queryParameter);
2069
+ const fieldCode = string(context.fieldCode);
2070
+ contextInvalid ||=
2071
+ Object.keys(context).some(key => !['queryParameter', 'fieldCode'].includes(key)) ||
2072
+ !/^[A-Za-z][A-Za-z0-9]{0,63}$/.test(queryParameter) ||
2073
+ contextKeys.has(queryParameter) ||
2074
+ (!createFields.has(fieldCode) && !existingFields.has(fieldCode));
2075
+ contextKeys.add(queryParameter);
2076
+ }
2077
+ if (!isRecord(launch.submission) ||
2078
+ submission.kind !== 'named-operation' ||
2079
+ Object.keys(submission).some(key => !['kind', 'create', 'existing', 'context'].includes(key)) ||
2080
+ (!['standalone', 'hidden-handoff'].includes(string(launch.mode))) ||
2081
+ (!submission.create && !submission.existing)) {
2082
+ diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_NAMED_OPERATION_SUBMISSION_INVALID', 'named-operation submission 只适用于 standalone/hidden-handoff,且必须声明 create 或 existing intent', `${path}.launch.submission`));
2083
+ }
2084
+ if (contextInvalid) {
2085
+ diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_LAUNCH_CONTEXT_INVALID', 'Workflow launch context 必须唯一、受限并指向已绑定的 subject form field', `${path}.launch.submission.context`));
2086
+ }
2087
+ }
2088
+ if (declaration.detailRouteCode !== undefined) {
2089
+ const detailRouteCode = object(declaration.detailRouteCode);
2090
+ const desktopCode = string(detailRouteCode.desktop);
2091
+ const mobileCode = string(detailRouteCode.mobile);
2092
+ if (Object.keys(detailRouteCode).some(key => !['desktop', 'mobile'].includes(key)) ||
2093
+ !OPERATION_CODE_PATTERN.test(desktopCode) ||
2094
+ !OPERATION_CODE_PATTERN.test(mobileCode) ||
2095
+ desktopCode === mobileCode) {
2096
+ diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_DETAIL_ROUTE_INVALID', 'detailRouteCode 必须声明两个不同且合法的 desktop/mobile route code', `${path}.detailRouteCode`));
2097
+ }
2098
+ else {
2099
+ for (const [device, code, surface] of [
2100
+ ['desktop', desktopCode, 'admin'],
2101
+ ['mobile', mobileCode, 'user'],
2102
+ ]) {
2103
+ const route = frontendRoutesByCode.get(code);
2104
+ const routePointer = `${path}.detailRouteCode.${device}`;
2105
+ if (!route) {
2106
+ diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_DETAIL_ROUTE_REFERENCE_MISSING', `detailRouteCode.${device} 必须引用已声明 frontend route`, routePointer));
2107
+ continue;
2108
+ }
2109
+ if (string(route.surface) !== surface) {
2110
+ diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_DETAIL_ROUTE_SURFACE_INVALID', `detailRouteCode.${device} 必须引用 ${surface} surface`, routePointer));
2111
+ }
2112
+ const parameters = [
2113
+ ...string(route.path).matchAll(/:([A-Za-z][A-Za-z0-9_]*)/g),
2114
+ ].map(match => match[1]);
2115
+ if (string(route.path).includes('*') ||
2116
+ parameters.length !== 1 ||
2117
+ parameters[0] !== 'instanceId') {
2118
+ diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_DETAIL_ROUTE_PATH_INVALID', `detailRouteCode.${device} 路由必须且只能声明 :instanceId 动态参数`, routePointer));
2119
+ }
2120
+ }
2121
+ }
2122
+ }
2123
+ if (!Number.isInteger(version) || version < 1) {
2124
+ diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_VERSION_INVALID', 'Workflow definition version 必须是正整数', `${path}.version`));
2125
+ }
2126
+ if (string(object(declaration.definition).title) ===
2127
+ string(object(declaration.definition).code)) {
2128
+ diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_USER_TITLE_REQUIRED', 'Workflow title 必须是用户可读本地化名称,不能等于内部 code', `${path}.definition.title`));
2129
+ }
2130
+ const subject = object(definition.subject);
2131
+ const factProjection = object(subject.factProjection);
2132
+ const factEntries = Object.entries(factProjection);
2133
+ const subjectFields = eventResourceFields.get(string(subject.resourceCode));
2134
+ const summaryFieldsValue = subject.summaryFields;
2135
+ const summaryFields = Array.isArray(summaryFieldsValue)
2136
+ ? summaryFieldsValue
2137
+ : [];
2138
+ const summaryResourceFields = workflowSummaryResourceFields.get(string(subject.resourceCode));
2139
+ const summaryFieldsInvalid = summaryFieldsValue !== undefined &&
2140
+ (!Array.isArray(summaryFieldsValue) ||
2141
+ summaryFields.length > WORKFLOW_SUMMARY_MAX_FIELDS ||
2142
+ new Set(summaryFields).size !== summaryFields.length ||
2143
+ summaryFields.some(field => {
2144
+ if (typeof field !== 'string' ||
2145
+ !/^[A-Za-z][A-Za-z0-9_]{0,62}$/.test(field)) {
2146
+ return true;
2147
+ }
2148
+ const metadata = summaryResourceFields?.get(field);
2149
+ return (!metadata ||
2150
+ metadata.system ||
2151
+ !isWorkflowSummaryFieldType(metadata.type) ||
2152
+ (metadata.type === 'text.long' &&
2153
+ metadata.maxLength !== undefined &&
2154
+ metadata.maxLength > WORKFLOW_SUMMARY_TEXT_LONG_MAX_BYTES));
2155
+ }));
2156
+ if (Object.keys(subject).some(key => !['resourceCode', 'factProjection', 'summaryFields'].includes(key)) ||
2157
+ !subjectFields ||
2158
+ !isRecord(subject.factProjection) ||
2159
+ factEntries.length < 1 ||
2160
+ factEntries.length > 64 ||
2161
+ factEntries.some(([factKey, fieldCode]) => !/^[A-Za-z][A-Za-z0-9_.]{0,127}$/.test(factKey) ||
2162
+ !/^[A-Za-z][A-Za-z0-9_]{0,62}$/.test(string(fieldCode)) ||
2163
+ !subjectFields.has(string(fieldCode)))) {
2164
+ diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_SUBJECT_INVALID', 'Workflow subject 必须绑定已声明资源,并将 1 到 64 个 fact key 映射到该资源的已声明字段', `${path}.definition.subject`));
2165
+ }
2166
+ if (summaryFieldsInvalid) {
2167
+ diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_SUMMARY_FIELDS_INVALID', `Workflow summaryFields 最多 ${WORKFLOW_SUMMARY_MAX_FIELDS} 个,必须是不重复的已声明 scalar/reference 字段;禁止 system/rich/json/subtable/file/image/signature,且 text.long 不超过 ${WORKFLOW_SUMMARY_TEXT_LONG_MAX_BYTES} bytes`, `${path}.definition.subject.summaryFields`));
2168
+ }
2169
+ for (const error of validateWorkflowDefinition(definition)) {
2170
+ diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_DEFINITION_INVALID', error, path));
2171
+ }
2172
+ const key = `${definition.code}:${version}`;
2173
+ if (definitionByKey.has(key)) {
2174
+ diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_DEFINITION_DUPLICATE', `Workflow definition 重复: ${key}`, path));
2175
+ }
2176
+ definitionByKey.set(key, definition);
2177
+ });
2178
+ bindings.forEach((item, index) => {
2179
+ const declaration = object(item);
2180
+ const binding = object(declaration.binding);
2181
+ const version = Number(declaration.version);
2182
+ const path = `workflows.bindings[${index}]`;
2183
+ if (!Number.isInteger(version) || version < 1) {
2184
+ diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_VERSION_INVALID', 'Workflow binding version 必须是正整数', `${path}.version`));
2185
+ }
2186
+ const key = `${binding.workflowCode}:${version}`;
2187
+ if (bindingByKey.has(key)) {
2188
+ diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_BINDING_DUPLICATE', `Workflow binding 重复: ${key}`, path));
2189
+ }
2190
+ bindingByKey.set(key, binding);
2191
+ });
2192
+ const activationWorkflowCodes = new Set();
2193
+ activations.forEach((item, index) => {
2194
+ const activation = object(item);
2195
+ const path = `workflows.activations[${index}]`;
2196
+ if (Object.keys(activation).some(key => ![
2197
+ 'workflowCode',
2198
+ 'definitionVersion',
2199
+ 'bindingVersion',
2200
+ 'acceptedCommandDeactivationPolicy',
2201
+ ].includes(key))) {
2202
+ diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_ACTIVATION_INVALID', 'Workflow activation 只能声明固定版本与停用策略', path));
2203
+ }
2204
+ const workflowCode = string(activation.workflowCode);
2205
+ if (activationWorkflowCodes.has(workflowCode)) {
2206
+ diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_ACTIVATION_DUPLICATE', `Workflow activation 重复: ${workflowCode}`, path));
2207
+ }
2208
+ activationWorkflowCodes.add(workflowCode);
2209
+ if ('environmentKey' in activation) {
2210
+ diagnostics.push(diagnostic('APP_CONFIG_ENVIRONMENT_OVERRIDE_FORBIDDEN', 'Workflow activation 属于环境中立 AppVersion,不能声明 environmentKey', `${path}.environmentKey`));
2211
+ }
2212
+ const definitionVersion = Number(activation.definitionVersion);
2213
+ const bindingVersion = Number(activation.bindingVersion);
2214
+ const definition = definitionByKey.get(`${workflowCode}:${definitionVersion}`);
2215
+ const binding = bindingByKey.get(`${workflowCode}:${bindingVersion}`);
2216
+ if (!definition || !binding) {
2217
+ diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_ACTIVATION_VERSION_NOT_FOUND', 'Workflow activation 引用的 definition 或 binding 版本不存在', path));
2218
+ return;
2219
+ }
2220
+ if (activation.acceptedCommandDeactivationPolicy !==
2221
+ definition.acceptedCommandDeactivationPolicy) {
2222
+ diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_ACTIVATION_DEACTIVATION_POLICY_MISMATCH', 'Workflow activation 停用策略必须等于所选 definition', `${path}.acceptedCommandDeactivationPolicy`));
2223
+ }
2224
+ for (const error of validateWorkflowBinding(definition, binding)) {
2225
+ diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_BINDING_INVALID', error, path));
2226
+ }
2227
+ for (const entry of Object.values(binding.bindings || {})) {
2228
+ if (entry.provider === 'application_provider' &&
2229
+ !providerKeys.has(entry.providerCode || '')) {
2230
+ diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_PROVIDER_NOT_FOUND', `Workflow binding 引用了未声明的 App Provider: ${entry.providerCode || ''}`, path));
2231
+ }
2232
+ }
2233
+ });
2234
+ editableParameters.forEach((item, index) => {
2235
+ const parameter = object(item);
2236
+ const workflowCode = string(parameter.workflowCode);
2237
+ const bindingKey = string(parameter.bindingKey);
2238
+ const active = activations.find(item => string(object(item).workflowCode) === workflowCode);
2239
+ const binding = active
2240
+ ? bindingByKey.get(`${workflowCode}:${Number(object(active).bindingVersion)}`)
2241
+ : undefined;
2242
+ if (!binding || !binding.bindings[bindingKey]) {
2243
+ diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_EDITABLE_PARAMETER_BINDING_NOT_FOUND', 'Workflow editable parameter 必须引用活动版本中的 binding key', `workflows.editableParameters[${index}]`));
2244
+ }
2245
+ for (const roleCode of Array.isArray(parameter.allowedRoleCodes)
2246
+ ? parameter.allowedRoleCodes.map(string)
2247
+ : []) {
2248
+ if (!new Set((Array.isArray(authz.roles) ? authz.roles : []).map(item => string(object(item).code))).has(roleCode)) {
2249
+ diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_EDITABLE_PARAMETER_ROLE_NOT_FOUND', `editable parameter 引用了未声明 role: ${roleCode}`, `workflows.editableParameters[${index}].allowedRoleCodes`));
2250
+ }
2251
+ }
2252
+ for (const providerCode of Array.isArray(parameter.allowedProviderCodes)
2253
+ ? parameter.allowedProviderCodes.map(string)
2254
+ : []) {
2255
+ if (!providerKeys.has(providerCode)) {
2256
+ diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_EDITABLE_PARAMETER_PROVIDER_NOT_FOUND', `editable parameter 引用了未声明 provider: ${providerCode}`, `workflows.editableParameters[${index}].allowedProviderCodes`));
2257
+ }
2258
+ }
2259
+ });
2260
+ }
2261
+ return diagnostics;
2262
+ }
2263
+ function validatePerspectives(rawPerspectives, rawRoles, diagnostics) {
2264
+ if (rawPerspectives !== undefined && !Array.isArray(rawPerspectives)) {
2265
+ diagnostics.push(diagnostic('APP_CONFIG_PERSPECTIVES_INVALID', 'perspectives 必须是数组', 'perspectives'));
2266
+ return;
2267
+ }
2268
+ const roleCodes = new Set((Array.isArray(rawRoles) ? rawRoles : []).map(role => string(object(role).code)));
2269
+ const codes = new Set();
2270
+ let defaults = 0;
2271
+ (Array.isArray(rawPerspectives) ? rawPerspectives : []).forEach((raw, index) => {
2272
+ const perspective = object(raw);
2273
+ const path = `perspectives[${index}]`;
2274
+ const code = string(perspective.code);
2275
+ const basis = Array.isArray(perspective.roleCodes)
2276
+ ? perspective.roleCodes.map(string)
2277
+ : [];
2278
+ if (!/^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/.test(code) ||
2279
+ codes.has(code) ||
2280
+ !string(perspective.name) ||
2281
+ basis.length < 1 ||
2282
+ basis.length > 20 ||
2283
+ new Set(basis).size !== basis.length ||
2284
+ basis.some(roleCode => !roleCodes.has(roleCode)) ||
2285
+ Object.keys(perspective).some(key => !['code', 'name', 'description', 'roleCodes', 'default'].includes(key)) ||
2286
+ (perspective.default !== undefined &&
2287
+ typeof perspective.default !== 'boolean')) {
2288
+ diagnostics.push(diagnostic('APP_CONFIG_PERSPECTIVE_INVALID', 'Perspective 必须声明唯一 code/name,并引用 1 到 20 个已声明角色', path));
2289
+ }
2290
+ if (perspective.default === true)
2291
+ defaults += 1;
2292
+ codes.add(code);
2293
+ });
2294
+ if (defaults > 1) {
2295
+ diagnostics.push(diagnostic('APP_CONFIG_PERSPECTIVE_DEFAULT_DUPLICATE', '最多只能声明一个默认 Perspective', 'perspectives'));
2296
+ }
2297
+ }
2298
+ const CAPABILITY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9:._*-]{0,254}$/;
2299
+ const OPERATION_CODE_PATTERN = /^[a-z][a-z0-9]*(?:[-_.][a-z0-9]+)*$/;
2300
+ const HTTP_METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']);
2301
+ const RESERVED_NATIVE_KEYS = new Map([
2302
+ ['environmentKey', 'environmentKey'],
2303
+ ['environmentId', 'environmentId'],
2304
+ ['secretValue', 'Secret value'],
2305
+ ['ciphertext', 'Secret ciphertext'],
2306
+ ['oauthClient', 'OAuth client'],
2307
+ ['oauthClientId', 'OAuth client'],
2308
+ ['oauthClientSecret', 'OAuth client Secret'],
2309
+ ['signingSecret', 'signing Secret'],
2310
+ ['kubernetesNamespace', 'Kubernetes namespace'],
2311
+ ['nodePort', 'Kubernetes NodePort'],
2312
+ ['productionUrl', 'production URL'],
2313
+ ]);
2314
+ function rejectNativeEnvironmentFields(value, diagnostics, path = '$', seen = new Set()) {
2315
+ if (!value || typeof value !== 'object')
2316
+ return;
2317
+ if (seen.has(value))
2318
+ return;
2319
+ seen.add(value);
2320
+ if (Array.isArray(value)) {
2321
+ value.forEach((item, index) => rejectNativeEnvironmentFields(item, diagnostics, `${path}[${index}]`, seen));
2322
+ return;
2323
+ }
2324
+ for (const [key, child] of Object.entries(value)) {
2325
+ const reserved = RESERVED_NATIVE_KEYS.get(key);
2326
+ if (reserved) {
2327
+ diagnostics.push(diagnostic('APP_CONFIG_NATIVE_RUNTIME_FIELD_FORBIDDEN', `${reserved} 属于环境运行态,不能进入环境中立 AppVersion`, path === '$' ? key : `${path}.${key}`));
2328
+ continue;
2329
+ }
2330
+ rejectNativeEnvironmentFields(child, diagnostics, path === '$' ? key : `${path}.${key}`, seen);
2331
+ }
2332
+ }
2333
+ function validateCapabilityDeclarations(appCode, declarations, diagnostics) {
2334
+ const seen = new Set();
2335
+ declarations.forEach((raw, index) => {
2336
+ const capability = object(raw);
2337
+ const code = string(capability.code);
2338
+ const path = `authz.capabilities[${index}]`;
2339
+ if (!CAPABILITY_PATTERN.test(code) ||
2340
+ !code.startsWith(`app:${appCode}:`) ||
2341
+ seen.has(code) ||
2342
+ !['backend', 'ui'].includes(string(capability.kind)) ||
2343
+ !string(capability.name)) {
2344
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_CAPABILITY_DECLARATION_INVALID', '显式 capability 必须属于当前应用、code 唯一,并声明 backend/ui kind 与 name', path));
2345
+ }
2346
+ if (platformCapabilityCodes(appCode).has(code)) {
2347
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_PLATFORM_CAPABILITY_RESERVED', '平台 capability 由平台保留 catalog 提供,应用不能重复声明', `${path}.code`));
2348
+ }
2349
+ seen.add(code);
2350
+ });
2351
+ }
2352
+ function validateBackendOperations(rawOperations, declaredResources, declaredWorkflowCodes, diagnostics) {
2353
+ const declaredResourceCodes = new Set(declaredResources.keys());
2354
+ if (rawOperations !== undefined && !Array.isArray(rawOperations)) {
2355
+ diagnostics.push(diagnostic('APP_CONFIG_BACKEND_OPERATIONS_INVALID', 'backend.operations 必须是数组', 'backend.operations'));
2356
+ return;
2357
+ }
2358
+ const codes = new Set();
2359
+ const routes = new Set();
2360
+ (Array.isArray(rawOperations) ? rawOperations : []).forEach((raw, index) => {
2361
+ const operation = object(raw);
2362
+ const code = string(operation.code);
2363
+ const method = string(operation.method).toUpperCase();
2364
+ const route = string(operation.path);
2365
+ const path = `backend.operations[${index}]`;
2366
+ const routeKey = `${method} ${route}`;
2367
+ if (!OPERATION_CODE_PATTERN.test(code) ||
2368
+ codes.has(code) ||
2369
+ !HTTP_METHODS.has(method) ||
2370
+ !validAppPath(route) ||
2371
+ route.startsWith('/__platform/') ||
2372
+ routes.has(routeKey) ||
2373
+ !CAPABILITY_PATTERN.test(string(operation.capability)) ||
2374
+ !isRecord(operation.requestSchema) ||
2375
+ !isRecord(operation.responseSchema)) {
2376
+ diagnostics.push(diagnostic('APP_CONFIG_BACKEND_OPERATION_INVALID', 'App API operation 必须声明唯一 code/route、静态 HTTP path、capability 与请求响应 JSON Schema', path));
2377
+ }
2378
+ if ('resources' in operation) {
2379
+ diagnostics.push(diagnostic('APP_CONFIG_BACKEND_OPERATION_RESOURCES_REMOVED', '资源范围只能在显式 backend.operations[].ai 中声明', `${path}.resources`));
2380
+ }
2381
+ if (operation.platformAccess !== undefined) {
2382
+ const access = object(operation.platformAccess);
2383
+ const allowedAccessKeys = new Set([
2384
+ 'directory',
2385
+ 'managedFiles',
2386
+ 'managedFileCopies',
2387
+ 'notification',
2388
+ 'workflow',
2389
+ ]);
2390
+ let invalid = !isRecord(operation.platformAccess) ||
2391
+ Object.keys(access).length === 0 ||
2392
+ Object.keys(access).some(key => !allowedAccessKeys.has(key));
2393
+ if (access.directory !== undefined) {
2394
+ const directory = object(access.directory);
2395
+ const fields = Array.isArray(directory.fields) ? directory.fields : [];
2396
+ const allowedFields = new Set([
2397
+ 'displayName',
2398
+ 'employeeNumber',
2399
+ 'primaryDepartment',
2400
+ 'departments',
2401
+ ]);
2402
+ invalid ||=
2403
+ directory.mode !== 'current-initiator' ||
2404
+ fields.length === 0 ||
2405
+ fields.length > allowedFields.size ||
2406
+ new Set(fields).size !== fields.length ||
2407
+ fields.some(field => !allowedFields.has(string(field))) ||
2408
+ Object.keys(directory).some(key => !['mode', 'fields'].includes(key));
2409
+ }
2410
+ if (access.managedFiles !== undefined) {
2411
+ const managedFiles = Array.isArray(access.managedFiles)
2412
+ ? access.managedFiles
2413
+ : [];
2414
+ invalid ||= managedFiles.length === 0 || managedFiles.length > 16;
2415
+ const resources = new Set();
2416
+ for (const rawManagedFile of managedFiles) {
2417
+ const managedFile = object(rawManagedFile);
2418
+ const resourceCode = string(managedFile.resourceCode);
2419
+ const fieldCodes = Array.isArray(managedFile.fieldCodes)
2420
+ ? managedFile.fieldCodes.map(string)
2421
+ : [];
2422
+ const intents = Array.isArray(managedFile.intents)
2423
+ ? managedFile.intents.map(string)
2424
+ : [];
2425
+ const resourceFields = declaredResources.get(resourceCode);
2426
+ invalid ||=
2427
+ !resourceFields ||
2428
+ resources.has(resourceCode) ||
2429
+ fieldCodes.length === 0 ||
2430
+ fieldCodes.length > 16 ||
2431
+ new Set(fieldCodes).size !== fieldCodes.length ||
2432
+ fieldCodes.some(fieldCode => !['file', 'image', 'signature'].includes(string(resourceFields?.get(fieldCode)))) ||
2433
+ intents.length === 0 ||
2434
+ intents.length > 2 ||
2435
+ new Set(intents).size !== intents.length ||
2436
+ intents.some(intent => !['create', 'update'].includes(intent)) ||
2437
+ Object.keys(managedFile).some(key => !['resourceCode', 'fieldCodes', 'intents'].includes(key));
2438
+ resources.add(resourceCode);
2439
+ }
2440
+ }
2441
+ if (access.managedFileCopies !== undefined) {
2442
+ const copies = Array.isArray(access.managedFileCopies)
2443
+ ? access.managedFileCopies
2444
+ : [];
2445
+ invalid ||= copies.length === 0 || copies.length > 16;
2446
+ const copyKeys = new Set();
2447
+ for (const rawCopy of copies) {
2448
+ const copy = object(rawCopy);
2449
+ const sourceResourceCode = string(copy.sourceResourceCode);
2450
+ const targetResourceCode = string(copy.targetResourceCode);
2451
+ const sourceFields = Array.isArray(copy.sourceFieldCodes)
2452
+ ? copy.sourceFieldCodes.map(string)
2453
+ : [];
2454
+ const targetFields = Array.isArray(copy.targetFieldCodes)
2455
+ ? copy.targetFieldCodes.map(string)
2456
+ : [];
2457
+ const source = declaredResources.get(sourceResourceCode);
2458
+ const target = declaredResources.get(targetResourceCode);
2459
+ invalid ||=
2460
+ copy.mode !== 'copy' ||
2461
+ !source ||
2462
+ !target ||
2463
+ sourceFields.length === 0 ||
2464
+ targetFields.length === 0 ||
2465
+ sourceFields.length > 16 ||
2466
+ targetFields.length > 16 ||
2467
+ new Set(sourceFields).size !== sourceFields.length ||
2468
+ new Set(targetFields).size !== targetFields.length ||
2469
+ sourceFields.some(field => !['file', 'image'].includes(source?.get(field) || '')) ||
2470
+ targetFields.some(field => !['file', 'image'].includes(target?.get(field) || '')) ||
2471
+ sourceFields.length !== targetFields.length ||
2472
+ sourceFields.some((field, i) => source?.get(field) !== target?.get(targetFields[i] || '')) ||
2473
+ Object.keys(copy).some(key => ![
2474
+ 'mode',
2475
+ 'sourceResourceCode',
2476
+ 'sourceFieldCodes',
2477
+ 'targetResourceCode',
2478
+ 'targetFieldCodes',
2479
+ ].includes(key));
2480
+ const key = `${sourceResourceCode}:${sourceFields.join(',')}=>${targetResourceCode}:${targetFields.join(',')}`;
2481
+ if (copyKeys.has(key))
2482
+ invalid = true;
2483
+ copyKeys.add(key);
2484
+ }
2485
+ }
2486
+ if (access.notification !== undefined) {
2487
+ const notification = object(access.notification);
2488
+ invalid ||=
2489
+ notification.mode !== 'business-standard' ||
2490
+ Object.keys(notification).some(key => key !== 'mode');
2491
+ }
2492
+ if (access.workflow !== undefined) {
2493
+ const workflow = object(access.workflow);
2494
+ const workflowCodes = Array.isArray(workflow.codes)
2495
+ ? workflow.codes.map(string)
2496
+ : [];
2497
+ invalid ||=
2498
+ workflowCodes.length === 0 ||
2499
+ workflowCodes.length > 16 ||
2500
+ new Set(workflowCodes).size !== workflowCodes.length ||
2501
+ workflowCodes.some(code => !declaredWorkflowCodes.has(code)) ||
2502
+ Object.keys(workflow).some(key => key !== 'codes');
2503
+ }
2504
+ if (invalid) {
2505
+ diagnostics.push(diagnostic('APP_CONFIG_BACKEND_OPERATION_PLATFORM_ACCESS_INVALID', 'operation.platformAccess 必须只声明有界的发起人目录、托管文件、托管文件复制、标准通知或现有工作流依赖', `${path}.platformAccess`));
2506
+ }
2507
+ }
2508
+ if (operation.ai !== undefined) {
2509
+ const ai = object(operation.ai);
2510
+ const aiResources = Array.isArray(ai.resources) ? ai.resources : [];
2511
+ const sideEffects = Array.isArray(ai.sideEffects)
2512
+ ? ai.sideEffects
2513
+ : [];
2514
+ const risk = string(ai.risk);
2515
+ const aiKeys = Object.keys(ai);
2516
+ const allowedAiKeys = new Set([
2517
+ 'name',
2518
+ 'description',
2519
+ 'risk',
2520
+ 'resources',
2521
+ 'sideEffects',
2522
+ 'concurrency',
2523
+ 'timeoutMs',
2524
+ ]);
2525
+ if (!/^[a-z][a-z0-9]*(?:[-.][a-z0-9]+)*$/.test(code) ||
2526
+ !string(ai.name) ||
2527
+ !string(ai.description) ||
2528
+ !['read', 'write', 'destructive', 'external'].includes(risk) ||
2529
+ (risk === 'read') !== (method === 'GET') ||
2530
+ (method === 'DELETE' && !['destructive', 'external'].includes(risk)) ||
2531
+ aiResources.length < 1 ||
2532
+ aiResources.length > 16 ||
2533
+ new Set(aiResources).size !== aiResources.length ||
2534
+ aiResources.some(item => !/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(String(item)) ||
2535
+ !declaredResourceCodes.has(String(item))) ||
2536
+ sideEffects.length > 20 ||
2537
+ new Set(sideEffects).size !== sideEffects.length ||
2538
+ sideEffects.some(item => !string(item)) ||
2539
+ (risk === 'read' ? sideEffects.length !== 0 : sideEffects.length < 1) ||
2540
+ (ai.concurrency !== undefined &&
2541
+ !['none', 'revision'].includes(string(ai.concurrency))) ||
2542
+ (ai.timeoutMs !== undefined &&
2543
+ (!Number.isSafeInteger(ai.timeoutMs) ||
2544
+ Number(ai.timeoutMs) < 100 ||
2545
+ Number(ai.timeoutMs) > 30000)) ||
2546
+ aiKeys.some(key => !allowedAiKeys.has(key))) {
2547
+ diagnostics.push(diagnostic('APP_CONFIG_BACKEND_OPERATION_AI_INVALID', 'AI 操作必须显式声明名称、说明、风险、现有资源、副作用和有界执行策略;GET 只能只读,写操作必须确认', `${path}.ai`));
2548
+ }
2549
+ }
2550
+ codes.add(code);
2551
+ routes.add(routeKey);
2552
+ });
2553
+ }
2554
+ function canonicalFrontendRouteShape(path) {
2555
+ const normalized = path.replace(/\/{2,}/g, '/').replace(/\/$/, '') || '/';
2556
+ return normalized
2557
+ .split('/')
2558
+ .map(segment => segment.startsWith(':') ? ':' : segment.includes('*') ? '*' : segment)
2559
+ .join('/');
2560
+ }
2561
+ function generatedPlatformRouteClaims(config) {
2562
+ const claims = [
2563
+ { owner: 'platform:admin-entry', path: '/' },
2564
+ { owner: 'platform:admin-entry', path: '/admin' },
2565
+ {
2566
+ owner: 'platform:file-preview',
2567
+ path: '/files/:resourceCode/:fileId/preview',
2568
+ },
2569
+ ];
2570
+ const data = object(config.data);
2571
+ const resources = Array.isArray(data.resources) ? data.resources : [];
2572
+ for (const rawResource of resources) {
2573
+ const resource = object(rawResource);
2574
+ const code = string(resource.code);
2575
+ if (!code)
2576
+ continue;
2577
+ const surface = object(resource.surface);
2578
+ const generated = object(surface.generated);
2579
+ const native = !surface.mutationOwner || surface.mutationOwner === 'native';
2580
+ const operations = {
2581
+ list: generated.list === undefined ? true : generated.list === true,
2582
+ detail: generated.detail === undefined ? true : generated.detail === true,
2583
+ create: generated.create === undefined ? native : generated.create === true,
2584
+ update: generated.update === undefined ? native : generated.update === true,
2585
+ };
2586
+ const base = `/admin/resources/${code}`;
2587
+ const candidates = [
2588
+ ['list', base],
2589
+ ['detail', `${base}/:id`],
2590
+ ['create', `${base}/new`],
2591
+ ['update', `${base}/:id/edit`],
2592
+ ];
2593
+ for (const [operation, path] of candidates) {
2594
+ if (!operations[operation])
2595
+ continue;
2596
+ claims.push({ owner: `resource:${code}:${operation}`, path }, { owner: `resource:${code}:${operation}:mobile`, path: `/m${path}` });
2597
+ }
2598
+ }
2599
+ const frontend = object(config.frontend);
2600
+ const user = object(frontend.user);
2601
+ const hasTodoCenter = user.applicationTodoCenter === true;
2602
+ if (hasTodoCenter) {
2603
+ claims.push({ owner: 'application:todo-center', path: '/todos' }, { owner: 'application:todo-center:mobile', path: '/m/todos' });
2604
+ }
2605
+ const workflows = object(config.workflows);
2606
+ const definitions = Array.isArray(workflows.definitions)
2607
+ ? workflows.definitions
2608
+ : [];
2609
+ if (definitions.length > 0) {
2610
+ claims.push({ owner: 'workflow:work-center', path: '/work-center' }, { owner: 'workflow:work-center:mobile', path: '/m/work-center' }, { owner: 'workflow:task', path: '/tasks/:taskId' }, { owner: 'workflow:task:mobile', path: '/m/tasks/:taskId' }, { owner: 'workflow:instance', path: '/workflows/:instanceId' }, {
2611
+ owner: 'workflow:instance:mobile',
2612
+ path: '/m/workflows/:instanceId',
2613
+ });
2614
+ const launchCodes = new Set();
2615
+ for (const rawDefinition of definitions) {
2616
+ const declaration = object(rawDefinition);
2617
+ if (!['standalone', 'hidden-handoff'].includes(string(object(declaration.launch).mode)))
2618
+ continue;
2619
+ const workflowCode = string(object(declaration.definition).code);
2620
+ if (workflowCode)
2621
+ launchCodes.add(workflowCode);
2622
+ }
2623
+ for (const workflowCode of launchCodes) {
2624
+ claims.push({
2625
+ owner: `workflow:${workflowCode}:launch`,
2626
+ path: '/workflows/:workflowCode/start',
2627
+ });
2628
+ }
2629
+ if (launchCodes.size > 0) {
2630
+ claims.push({
2631
+ owner: 'workflow:launch:mobile',
2632
+ path: '/m/workflows/:workflowCode/start',
2633
+ });
2634
+ }
2635
+ }
2636
+ return claims;
2637
+ }
2638
+ function validateFrontendDevicePolicy(config, diagnostics) {
2639
+ const frontend = object(config.frontend);
2640
+ if (frontend.devicePolicy === undefined)
2641
+ return;
2642
+ const policy = object(frontend.devicePolicy);
2643
+ const mobileMaxWidthPx = policy.mobileMaxWidthPx;
2644
+ const desktopMinWidthPx = policy.desktopMinWidthPx;
2645
+ const invalid = Object.keys(policy).some(key => !['kind', 'mobileMaxWidthPx', 'desktopMinWidthPx'].includes(key)) ||
2646
+ policy.kind !== 'viewport-family' ||
2647
+ !Number.isInteger(mobileMaxWidthPx) ||
2648
+ !Number.isInteger(desktopMinWidthPx) ||
2649
+ Number(mobileMaxWidthPx) < 320 ||
2650
+ Number(mobileMaxWidthPx) > 1600 ||
2651
+ Number(desktopMinWidthPx) < 320 ||
2652
+ Number(desktopMinWidthPx) > 1600 ||
2653
+ Number(desktopMinWidthPx) !== Number(mobileMaxWidthPx) + 1;
2654
+ if (invalid) {
2655
+ diagnostics.push(diagnostic('APP_CONFIG_FRONTEND_DEVICE_POLICY_INVALID', 'frontend.devicePolicy 必须是 viewport-family,移动最大宽度和桌面最小宽度为 320-1600 的相邻整数', 'frontend.devicePolicy'));
2656
+ }
2657
+ }
2658
+ function validateFrontendRoutes(config, diagnostics) {
2659
+ const frontend = object(config.frontend);
2660
+ const rawRoutes = frontend.routes;
2661
+ if (rawRoutes !== undefined && !Array.isArray(rawRoutes)) {
2662
+ diagnostics.push(diagnostic('APP_CONFIG_FRONTEND_ROUTES_INVALID', 'frontend.routes 必须是数组', 'frontend.routes'));
2663
+ return;
2664
+ }
2665
+ const routes = Array.isArray(rawRoutes) ? rawRoutes : [];
2666
+ const codes = new Set();
2667
+ const pathClaims = new Map();
2668
+ for (const claim of generatedPlatformRouteClaims(config)) {
2669
+ pathClaims.set(canonicalFrontendRouteShape(claim.path), claim);
2670
+ }
2671
+ const authentication = object(frontend.authentication);
2672
+ const authenticationSurfaces = object(authentication.surfaces);
2673
+ for (const device of ['desktop', 'mobile']) {
2674
+ const surface = object(authenticationSurfaces[device]);
2675
+ const surfacePath = string(surface.path);
2676
+ if (surfacePath) {
2677
+ pathClaims.set(canonicalFrontendRouteShape(surfacePath), {
2678
+ owner: `authentication:${device}`,
2679
+ path: surfacePath,
2680
+ });
2681
+ }
2682
+ }
2683
+ routes.forEach((raw, index) => {
2684
+ const route = object(raw);
2685
+ const code = string(route.code);
2686
+ const routePath = string(route.path);
2687
+ const path = `frontend.routes[${index}]`;
2688
+ const access = object(route.access);
2689
+ const allowedKeys = new Set([
2690
+ 'code',
2691
+ 'path',
2692
+ 'label',
2693
+ 'surface',
2694
+ 'parentCode',
2695
+ 'capability',
2696
+ 'access',
2697
+ 'pinned',
2698
+ 'tabPersistence',
2699
+ 'keepAlive',
2700
+ ]);
2701
+ const accessKeys = Object.keys(access);
2702
+ const allOf = Array.isArray(access.allOf) ? access.allOf : [];
2703
+ const anyOf = Array.isArray(access.anyOf) ? access.anyOf : [];
2704
+ const accessInvalid = route.access !== undefined &&
2705
+ (route.capability !== undefined ||
2706
+ !route.access ||
2707
+ typeof route.access !== 'object' ||
2708
+ Array.isArray(route.access) ||
2709
+ accessKeys.some(key => !['allOf', 'anyOf'].includes(key)) ||
2710
+ (allOf.length === 0 && anyOf.length === 0) ||
2711
+ allOf.length > 50 ||
2712
+ anyOf.length > 50 ||
2713
+ allOf.some(value => !CAPABILITY_PATTERN.test(string(value))) ||
2714
+ anyOf.some(value => !CAPABILITY_PATTERN.test(string(value))) ||
2715
+ new Set(allOf.map(string)).size !== allOf.length ||
2716
+ new Set(anyOf.map(string)).size !== anyOf.length);
2717
+ const dynamicRoute = /[:*]/.test(routePath);
2718
+ const routePolicyInvalid = Object.keys(route).some(key => !allowedKeys.has(key)) ||
2719
+ (route.pinned !== undefined && typeof route.pinned !== 'boolean') ||
2720
+ (route.tabPersistence !== undefined &&
2721
+ !['session', 'none'].includes(string(route.tabPersistence))) ||
2722
+ (route.keepAlive !== undefined &&
2723
+ !['none', 'memory'].includes(string(route.keepAlive))) ||
2724
+ (dynamicRoute && route.pinned === true) ||
2725
+ (dynamicRoute && route.tabPersistence === 'session') ||
2726
+ (dynamicRoute && route.keepAlive === 'memory');
2727
+ if (!OPERATION_CODE_PATTERN.test(code) ||
2728
+ codes.has(code) ||
2729
+ !validAppPath(routePath) ||
2730
+ !string(route.label) ||
2731
+ !['admin', 'user'].includes(string(route.surface)) ||
2732
+ (route.surface === 'admin' &&
2733
+ routePath !== '/admin' &&
2734
+ !routePath.startsWith('/admin/') &&
2735
+ routePath !== '/m/admin' &&
2736
+ !routePath.startsWith('/m/admin/')) ||
2737
+ (route.surface === 'user' &&
2738
+ (routePath === '/admin' ||
2739
+ routePath.startsWith('/admin/') ||
2740
+ routePath === '/m/admin' ||
2741
+ routePath.startsWith('/m/admin/'))) ||
2742
+ (route.capability !== undefined &&
2743
+ !CAPABILITY_PATTERN.test(string(route.capability))) ||
2744
+ accessInvalid ||
2745
+ routePolicyInvalid) {
2746
+ diagnostics.push(diagnostic('APP_CONFIG_FRONTEND_ROUTE_INVALID', '前端 route 必须声明唯一 code/path、label、surface、有效访问表达式和有界标签策略;动态 route 不能固定、恢复或保活', path));
2747
+ }
2748
+ const shape = canonicalFrontendRouteShape(routePath);
2749
+ const existing = pathClaims.get(shape);
2750
+ if (routePath && existing) {
2751
+ diagnostics.push(diagnostic('APP_CONFIG_FRONTEND_ROUTE_PATH_CONFLICT', `前端 route path 与 ${existing.owner} 冲突: ${routePath}`, `${path}.path`));
2752
+ }
2753
+ else if (routePath) {
2754
+ pathClaims.set(shape, {
2755
+ owner: `frontend:${code || index}`,
2756
+ path: routePath,
2757
+ });
2758
+ }
2759
+ codes.add(code);
2760
+ });
2761
+ const parentByCode = new Map();
2762
+ routes.forEach((raw, index) => {
2763
+ const route = object(raw);
2764
+ const code = string(route.code);
2765
+ const parentCode = string(route.parentCode);
2766
+ if (parentCode) {
2767
+ parentByCode.set(code, parentCode);
2768
+ if (!codes.has(parentCode) || parentCode === code) {
2769
+ diagnostics.push(diagnostic('APP_CONFIG_FRONTEND_ROUTE_PARENT_INVALID', 'parentCode 必须引用另一个已声明 route', `frontend.routes[${index}].parentCode`));
2770
+ }
2771
+ }
2772
+ });
2773
+ for (const code of codes) {
2774
+ const visited = new Set();
2775
+ let current = code;
2776
+ while (current && parentByCode.has(current)) {
2777
+ if (visited.has(current)) {
2778
+ diagnostics.push(diagnostic('APP_CONFIG_FRONTEND_ROUTE_CYCLE', `route parentCode 形成循环: ${code}`, 'frontend.routes'));
2779
+ break;
2780
+ }
2781
+ visited.add(current);
2782
+ current = parentByCode.get(current);
2783
+ }
2784
+ }
2785
+ }
2786
+ const ANONYMOUS_PUBLIC_OPERATIONS = new Set([
2787
+ 'draft.read',
2788
+ 'draft.update',
2789
+ 'validate',
2790
+ 'create',
2791
+ 'own.list',
2792
+ 'own.read',
2793
+ ]);
2794
+ function validateAnonymousPublicAccess(config, diagnostics) {
2795
+ const frontend = object(config.frontend);
2796
+ if (frontend.publicAccess === undefined)
2797
+ return;
2798
+ const publicAccess = object(frontend.publicAccess);
2799
+ const policies = Array.isArray(publicAccess.policies)
2800
+ ? publicAccess.policies
2801
+ : [];
2802
+ if (Object.keys(publicAccess).some(key => key !== 'policies') ||
2803
+ !Array.isArray(publicAccess.policies) ||
2804
+ policies.length < 1 ||
2805
+ policies.length > 16) {
2806
+ diagnostics.push(diagnostic('APP_CONFIG_ANONYMOUS_PUBLIC_ACCESS_INVALID', 'frontend.publicAccess 只接受 1 到 16 个 policies', 'frontend.publicAccess'));
2807
+ return;
2808
+ }
2809
+ const routes = new Map((Array.isArray(frontend.routes) ? frontend.routes : []).map(raw => {
2810
+ const route = object(raw);
2811
+ return [string(route.code), route];
2812
+ }));
2813
+ const resourceMap = new Map((Array.isArray(object(config.data).resources)
2814
+ ? object(config.data).resources
2815
+ : []).map(raw => {
2816
+ const resource = object(raw);
2817
+ const fields = Array.isArray(object(resource.schema).fields)
2818
+ ? object(resource.schema).fields
2819
+ : [];
2820
+ const fieldMap = new Map(fields.map(field => {
2821
+ const declaration = object(field);
2822
+ return [string(declaration.code), string(declaration.type)];
2823
+ }));
2824
+ const requiredCreateFields = new Set(fields
2825
+ .map(object)
2826
+ .filter(field => {
2827
+ const type = string(field.type);
2828
+ return (field.nullable === false &&
2829
+ APP_DATA_FIELD_TYPES.has(type) &&
2830
+ supportsGeneratedMutation({ type }));
2831
+ })
2832
+ .map(field => string(field.code)));
2833
+ const surface = object(resource.surface);
2834
+ const generated = object(surface.generated);
2835
+ const nativeCreate = (string(surface.mutationOwner) || 'native') === 'native' &&
2836
+ generated.create !== false;
2837
+ return [
2838
+ string(resource.code),
2839
+ { fields: fieldMap, requiredCreateFields, nativeCreate },
2840
+ ];
2841
+ }));
2842
+ const policyCodes = new Set();
2843
+ const routeCodes = new Set();
2844
+ policies.forEach((raw, index) => {
2845
+ const policy = object(raw);
2846
+ const path = `frontend.publicAccess.policies[${index}]`;
2847
+ const code = string(policy.code);
2848
+ const routeCode = string(policy.routeCode);
2849
+ const resourceCode = string(policy.resourceCode);
2850
+ const route = routes.get(routeCode);
2851
+ const resource = resourceMap.get(resourceCode);
2852
+ const resourceFields = resource?.fields;
2853
+ const operations = Array.isArray(policy.operations)
2854
+ ? policy.operations.map(string)
2855
+ : [];
2856
+ const fields = Array.isArray(policy.fields) ? policy.fields.map(string) : [];
2857
+ const requiredFields = Array.isArray(policy.requiredFields)
2858
+ ? policy.requiredFields.map(string)
2859
+ : [];
2860
+ const ownRecordFields = Array.isArray(policy.ownRecordFields)
2861
+ ? policy.ownRecordFields.map(string)
2862
+ : fields;
2863
+ const draft = object(policy.draft);
2864
+ const validations = Array.isArray(policy.validations)
2865
+ ? policy.validations
2866
+ : [];
2867
+ const exactRootKeys = [
2868
+ 'code',
2869
+ 'routeCode',
2870
+ 'mode',
2871
+ 'resourceCode',
2872
+ 'operations',
2873
+ 'fields',
2874
+ 'requiredFields',
2875
+ 'ownRecordFields',
2876
+ 'draft',
2877
+ 'validations',
2878
+ ];
2879
+ const invalid = Object.keys(policy).some(key => !exactRootKeys.includes(key)) ||
2880
+ !OPERATION_CODE_PATTERN.test(code) ||
2881
+ policyCodes.has(code) ||
2882
+ routeCodes.has(routeCode) ||
2883
+ policy.mode !== 'anonymous' ||
2884
+ !route ||
2885
+ route.surface !== 'user' ||
2886
+ route.capability !== undefined ||
2887
+ route.access !== undefined ||
2888
+ /[:*]/.test(string(route.path)) ||
2889
+ !resourceFields ||
2890
+ operations.length < 1 ||
2891
+ operations.length > ANONYMOUS_PUBLIC_OPERATIONS.size ||
2892
+ new Set(operations).size !== operations.length ||
2893
+ operations.some(operation => !ANONYMOUS_PUBLIC_OPERATIONS.has(operation)) ||
2894
+ fields.length < 1 ||
2895
+ fields.length > 64 ||
2896
+ new Set(fields).size !== fields.length ||
2897
+ fields.some(field => !resourceFields?.has(field)) ||
2898
+ new Set(requiredFields).size !== requiredFields.length ||
2899
+ requiredFields.some(field => !fields.includes(field)) ||
2900
+ (operations.includes('create') &&
2901
+ (!resource?.nativeCreate ||
2902
+ [...(resource?.requiredCreateFields || [])].some(field => !fields.includes(field) || !requiredFields.includes(field)))) ||
2903
+ new Set(ownRecordFields).size !== ownRecordFields.length ||
2904
+ ownRecordFields.some(field => !fields.includes(field)) ||
2905
+ (operations.includes('draft.read') !== operations.includes('draft.update')) ||
2906
+ (operations.some(operation => operation.startsWith('draft.')) &&
2907
+ (policy.draft === undefined || draft.enabled !== true)) ||
2908
+ (policy.draft !== undefined &&
2909
+ (Object.keys(draft).some(key => !['enabled', 'inactivityTtlSeconds', 'maxBytes'].includes(key)) ||
2910
+ draft.enabled !== true ||
2911
+ (draft.inactivityTtlSeconds !== undefined &&
2912
+ (!Number.isSafeInteger(draft.inactivityTtlSeconds) ||
2913
+ Number(draft.inactivityTtlSeconds) < 3600 ||
2914
+ Number(draft.inactivityTtlSeconds) > 7_776_000)) ||
2915
+ (draft.maxBytes !== undefined &&
2916
+ (!Number.isSafeInteger(draft.maxBytes) ||
2917
+ Number(draft.maxBytes) < 4096 ||
2918
+ Number(draft.maxBytes) > 262_144)))) ||
2919
+ validations.length > 16;
2920
+ if (invalid) {
2921
+ diagnostics.push(diagnostic('APP_CONFIG_ANONYMOUS_PUBLIC_POLICY_INVALID', '匿名公开策略必须绑定唯一静态 user route、已声明资源/字段和有界操作', path));
2922
+ }
2923
+ const validationCodes = new Set();
2924
+ validations.forEach((rawValidation, validationIndex) => {
2925
+ const validation = object(rawValidation);
2926
+ const validationCode = string(validation.code);
2927
+ const validationFields = Array.isArray(validation.fields)
2928
+ ? validation.fields.map(string)
2929
+ : [];
2930
+ if (Object.keys(validation).some(key => !['code', 'kind', 'fields', 'result'].includes(key)) ||
2931
+ !OPERATION_CODE_PATTERN.test(validationCode) ||
2932
+ validationCodes.has(validationCode) ||
2933
+ validation.kind !== 'duplicate' ||
2934
+ validation.result !== 'availability' ||
2935
+ validationFields.length < 1 ||
2936
+ validationFields.length > 8 ||
2937
+ new Set(validationFields).size !== validationFields.length ||
2938
+ validationFields.some(field => !fields.includes(field)) ||
2939
+ validationFields.some(field => ['file', 'subtable'].includes(resourceFields?.get(field) || ''))) {
2940
+ diagnostics.push(diagnostic('APP_CONFIG_ANONYMOUS_PUBLIC_VALIDATION_INVALID', '匿名公开校验必须是只使用已授权字段的有界 duplicate availability 校验', `${path}.validations[${validationIndex}]`));
2941
+ }
2942
+ validationCodes.add(validationCode);
2943
+ });
2944
+ policyCodes.add(code);
2945
+ routeCodes.add(routeCode);
2946
+ });
2947
+ }
2948
+ function validateApplicationAuthentication(config, diagnostics) {
2949
+ const frontend = object(config.frontend);
2950
+ if (frontend.authentication === undefined)
2951
+ return;
2952
+ const authentication = object(frontend.authentication);
2953
+ const allowedRootKeys = new Set([
2954
+ 'accountMode',
2955
+ 'registration',
2956
+ 'methods',
2957
+ 'surfaces',
2958
+ ]);
2959
+ const registration = object(authentication.registration);
2960
+ const surfaces = object(authentication.surfaces);
2961
+ const methods = Array.isArray(authentication.methods)
2962
+ ? authentication.methods
2963
+ : [];
2964
+ if (!frontend.authentication ||
2965
+ typeof frontend.authentication !== 'object' ||
2966
+ Array.isArray(frontend.authentication) ||
2967
+ Object.keys(authentication).some(key => !allowedRootKeys.has(key)) ||
2968
+ authentication.accountMode !== 'existing-platform-users-only' ||
2969
+ Object.keys(registration).some(key => key !== 'mode') ||
2970
+ registration.mode !== 'reject' ||
2971
+ !Array.isArray(authentication.methods) ||
2972
+ methods.length < 1 ||
2973
+ methods.length > 8 ||
2974
+ !authentication.surfaces ||
2975
+ typeof authentication.surfaces !== 'object' ||
2976
+ Array.isArray(authentication.surfaces) ||
2977
+ Object.keys(surfaces).some(key => !['desktop', 'mobile'].includes(key)) ||
2978
+ !surfaces.desktop ||
2979
+ !surfaces.mobile) {
2980
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHENTICATION_INVALID', 'frontend.authentication 只支持现有平台用户、拒绝注册、1-8 个方法和独立 desktop/mobile surface', 'frontend.authentication'));
2981
+ }
2982
+ const methodCodes = new Set();
2983
+ const methodTypes = new Set();
2984
+ let primaryCount = 0;
2985
+ methods.forEach((raw, index) => {
2986
+ const method = object(raw);
2987
+ const type = string(method.type);
2988
+ const code = string(method.code);
2989
+ const commonKeys = ['code', 'type', 'label', 'presentation', 'required'];
2990
+ const allowedKeys = new Set(type === 'sso'
2991
+ ? [...commonKeys, 'provider']
2992
+ : type === 'dingtalk'
2993
+ ? [...commonKeys, 'flow']
2994
+ : commonKeys);
2995
+ if (method.presentation === 'primary')
2996
+ primaryCount += 1;
2997
+ const invalid = Object.keys(method).some(key => !allowedKeys.has(key)) ||
2998
+ !OPERATION_CODE_PATTERN.test(code) ||
2999
+ methodCodes.has(code) ||
3000
+ methodTypes.has(type) ||
3001
+ !['sso', 'password', 'dingtalk'].includes(type) ||
3002
+ !string(method.label) ||
3003
+ !['primary', 'secondary'].includes(string(method.presentation)) ||
3004
+ typeof method.required !== 'boolean' ||
3005
+ (type === 'sso' && method.provider !== 'tenant-default') ||
3006
+ (type === 'dingtalk' &&
3007
+ !['auto', 'jsapi', 'oauth'].includes(string(method.flow)));
3008
+ if (invalid) {
3009
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHENTICATION_METHOD_INVALID', '登录方法必须使用唯一 code、受支持类型和不含 Secret 的有界公开描述', `frontend.authentication.methods[${index}]`));
3010
+ }
3011
+ methodCodes.add(code);
3012
+ methodTypes.add(type);
3013
+ });
3014
+ if (methods.length > 0 && primaryCount !== 1) {
3015
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHENTICATION_PRIMARY_METHOD_INVALID', '登录方法必须且只能有一个 primary', 'frontend.authentication.methods'));
3016
+ }
3017
+ const routes = Array.isArray(frontend.routes) ? frontend.routes : [];
3018
+ const routeByCode = new Map(routes.map(raw => {
3019
+ const route = object(raw);
3020
+ return [string(route.code), route];
3021
+ }));
3022
+ const routeCodes = new Set(routeByCode.keys());
3023
+ const claimedShapes = new Map(generatedPlatformRouteClaims(config).map(claim => [
3024
+ canonicalFrontendRouteShape(claim.path),
3025
+ claim,
3026
+ ]));
3027
+ const surfaceCodes = new Set();
3028
+ const surfaceShapes = new Set();
3029
+ for (const device of ['desktop', 'mobile']) {
3030
+ const surface = object(surfaces[device]);
3031
+ const pointer = `frontend.authentication.surfaces.${device}`;
3032
+ const routeCode = string(surface.routeCode);
3033
+ const routePath = string(surface.path);
3034
+ const defaultRouteCode = string(surface.defaultRouteCode);
3035
+ const defaultRoute = routeByCode.get(defaultRouteCode);
3036
+ const shape = canonicalFrontendRouteShape(routePath);
3037
+ const expectedPath = device === 'mobile' ? '/m/login' : '/login';
3038
+ const invalid = Object.keys(surface).some(key => !['routeCode', 'path', 'defaultRouteCode'].includes(key)) ||
3039
+ !OPERATION_CODE_PATTERN.test(routeCode) ||
3040
+ surfaceCodes.has(routeCode) ||
3041
+ routeCodes.has(routeCode) ||
3042
+ !validAppPath(routePath) ||
3043
+ /[:*?#\\\\]/.test(routePath) ||
3044
+ routePath !== expectedPath ||
3045
+ surfaceShapes.has(shape) ||
3046
+ /(?:^|\/)(?:auth|oauth)[^/]*callback(?:\/|$)/i.test(routePath);
3047
+ if (invalid) {
3048
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHENTICATION_SURFACE_INVALID', '登录 surface 必须使用独立静态 routeCode,desktop path=/login、mobile path=/m/login', pointer));
3049
+ }
3050
+ const generatedClaim = claimedShapes.get(shape);
3051
+ if (routePath && generatedClaim) {
3052
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHENTICATION_ROUTE_CONFLICT', `登录 surface path 与 ${generatedClaim.owner} 冲突: ${routePath}`, `${pointer}.path`));
3053
+ }
3054
+ if (!defaultRoute ||
3055
+ defaultRoute.surface !== 'user' ||
3056
+ (device === 'mobile'
3057
+ ? !string(defaultRoute.path).startsWith('/m/')
3058
+ : string(defaultRoute.path).startsWith('/m/')) ||
3059
+ canonicalFrontendRouteShape(string(defaultRoute.path)) === shape) {
3060
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHENTICATION_DEFAULT_ROUTE_INVALID', 'defaultRouteCode 必须引用同设备的受保护 user route,且不能指回登录 surface', `${pointer}.defaultRouteCode`));
3061
+ }
3062
+ surfaceCodes.add(routeCode);
3063
+ surfaceShapes.add(shape);
3064
+ }
3065
+ }
3066
+ const ADMIN_NAVIGATION_ICONS = new Set([
3067
+ 'overview',
3068
+ 'database',
3069
+ 'operations',
3070
+ 'workflow',
3071
+ 'members',
3072
+ 'calendar',
3073
+ 'document',
3074
+ 'folder',
3075
+ 'settings',
3076
+ ]);
3077
+ function validAdminOrder(value) {
3078
+ return (value === undefined ||
3079
+ (Number.isSafeInteger(value) && Math.abs(Number(value)) <= 10_000));
3080
+ }
3081
+ function validateFrontendUser(config, diagnostics) {
3082
+ const frontend = object(config.frontend);
3083
+ if (frontend.user === undefined)
3084
+ return;
3085
+ const user = object(frontend.user);
3086
+ if (!frontend.user ||
3087
+ typeof frontend.user !== 'object' ||
3088
+ Array.isArray(frontend.user) ||
3089
+ Object.keys(user).some(key => key !== 'applicationTodoCenter') ||
3090
+ (user.applicationTodoCenter !== undefined &&
3091
+ typeof user.applicationTodoCenter !== 'boolean')) {
3092
+ diagnostics.push(diagnostic('APP_CONFIG_FRONTEND_USER_INVALID', 'frontend.user 只接受 applicationTodoCenter 布尔值', 'frontend.user'));
3093
+ }
3094
+ }
3095
+ function validateAdminAccess(config, diagnostics) {
3096
+ const accessValue = config.frontend.admin?.access;
3097
+ if (accessValue === undefined)
3098
+ return;
3099
+ const access = object(accessValue);
3100
+ const allOf = Array.isArray(access.allOf) ? access.allOf : [];
3101
+ const anyOf = Array.isArray(access.anyOf) ? access.anyOf : [];
3102
+ const invalid = !accessValue ||
3103
+ typeof accessValue !== 'object' ||
3104
+ Array.isArray(accessValue) ||
3105
+ Object.keys(access).some(key => !['allOf', 'anyOf'].includes(key)) ||
3106
+ (allOf.length === 0 && anyOf.length === 0) ||
3107
+ allOf.length > 50 ||
3108
+ anyOf.length > 50 ||
3109
+ allOf.some(value => !CAPABILITY_PATTERN.test(string(value))) ||
3110
+ anyOf.some(value => !CAPABILITY_PATTERN.test(string(value))) ||
3111
+ new Set(allOf.map(string)).size !== allOf.length ||
3112
+ new Set(anyOf.map(string)).size !== anyOf.length;
3113
+ if (invalid) {
3114
+ diagnostics.push(diagnostic('APP_CONFIG_ADMIN_ACCESS_INVALID', 'frontend.admin.access 必须包含非空、无重复且有效的 allOf 或 anyOf capability 数组', 'frontend.admin.access'));
3115
+ }
3116
+ }
3117
+ function validateAdminNavigation(config, diagnostics) {
3118
+ const frontend = object(config.frontend);
3119
+ const admin = object(frontend.admin);
3120
+ const rawNavigation = admin.navigation;
3121
+ if (frontend.admin !== undefined &&
3122
+ (Object.keys(admin).some(key => !['access', 'navigation'].includes(key)) ||
3123
+ !Array.isArray(rawNavigation))) {
3124
+ diagnostics.push(diagnostic('APP_CONFIG_ADMIN_NAVIGATION_INVALID', 'frontend.admin 只接受 access 和 navigation 数组', 'frontend.admin.navigation'));
3125
+ return;
3126
+ }
3127
+ const groups = Array.isArray(rawNavigation) ? rawNavigation : [];
3128
+ if (groups.length > 100) {
3129
+ diagnostics.push(diagnostic('APP_CONFIG_ADMIN_NAVIGATION_LIMIT_EXCEEDED', '后台导航最多包含 100 个业务分组', 'frontend.admin.navigation'));
3130
+ }
3131
+ const resources = new Map((config.data?.resources || []).map(resource => [resource.code, resource]));
3132
+ const routes = new Map((config.frontend.routes || []).map(route => [route.code, route]));
3133
+ const groupCodes = new Set();
3134
+ const pageKeys = new Set();
3135
+ let itemCount = 0;
3136
+ groups.forEach((rawGroup, groupIndex) => {
3137
+ const group = object(rawGroup);
3138
+ const path = `frontend.admin.navigation[${groupIndex}]`;
3139
+ const code = string(group.code);
3140
+ const label = string(group.label);
3141
+ const items = Array.isArray(group.items) ? group.items : [];
3142
+ if (Object.keys(group).some(key => !['code', 'label', 'icon', 'order', 'items'].includes(key)) ||
3143
+ !OPERATION_CODE_PATTERN.test(code) ||
3144
+ groupCodes.has(code) ||
3145
+ !label ||
3146
+ label === code ||
3147
+ (group.icon !== undefined &&
3148
+ !ADMIN_NAVIGATION_ICONS.has(string(group.icon))) ||
3149
+ !validAdminOrder(group.order)) {
3150
+ diagnostics.push(diagnostic('APP_CONFIG_ADMIN_GROUP_INVALID', '导航分组必须声明唯一 code、用户 label、受支持 icon/order 和 items', path));
3151
+ }
3152
+ if (items.length === 0) {
3153
+ diagnostics.push(diagnostic('APP_CONFIG_ADMIN_GROUP_ORPHANED', '导航分组必须至少引用一个可见页面', `${path}.items`));
3154
+ }
3155
+ groupCodes.add(code);
3156
+ items.forEach((rawItem, itemIndex) => {
3157
+ itemCount += 1;
3158
+ const item = object(rawItem);
3159
+ const itemPath = `${path}.items[${itemIndex}]`;
3160
+ const page = object(item.page);
3161
+ const kind = string(page.kind);
3162
+ let pageKey = '';
3163
+ let defaultLabel = '';
3164
+ let internalCode = '';
3165
+ let exists = false;
3166
+ if (kind === 'resource') {
3167
+ const resourceCode = string(page.resourceCode);
3168
+ const resource = resources.get(resourceCode);
3169
+ pageKey = `resource:${resourceCode}:list`;
3170
+ defaultLabel = resource?.name || '';
3171
+ internalCode = resourceCode;
3172
+ exists = Boolean(resource?.surface?.generated?.list);
3173
+ }
3174
+ else if (kind === 'operation') {
3175
+ const routeCode = string(page.routeCode);
3176
+ const route = routes.get(routeCode);
3177
+ pageKey = `operation:${routeCode}`;
3178
+ defaultLabel = route?.label || '';
3179
+ internalCode = routeCode;
3180
+ exists = Boolean(route &&
3181
+ route.surface === 'admin' &&
3182
+ !route.path.startsWith('/m/admin') &&
3183
+ !/[:*]/.test(route.path));
3184
+ }
3185
+ if (Object.keys(item).some(key => !['page', 'label', 'icon', 'order'].includes(key)) ||
3186
+ ![
3187
+ 'resource',
3188
+ 'operation',
3189
+ ].includes(kind) ||
3190
+ (item.icon !== undefined &&
3191
+ !ADMIN_NAVIGATION_ICONS.has(string(item.icon))) ||
3192
+ !validAdminOrder(item.order)) {
3193
+ diagnostics.push(diagnostic('APP_CONFIG_ADMIN_ITEM_INVALID', '导航项必须使用受支持的页面 helper、label、icon 和 order', itemPath));
3194
+ }
3195
+ if (!exists) {
3196
+ diagnostics.push(diagnostic('APP_CONFIG_ADMIN_PAGE_NOT_FOUND', '导航引用必须指向存在且可进入菜单的 page registry 页面', `${itemPath}.page`));
3197
+ }
3198
+ if (pageKey && pageKeys.has(pageKey)) {
3199
+ diagnostics.push(diagnostic('APP_CONFIG_ADMIN_PAGE_DUPLICATE', `同一页面不能重复出现在导航中: ${pageKey}`, `${itemPath}.page`));
3200
+ }
3201
+ if (pageKey)
3202
+ pageKeys.add(pageKey);
3203
+ const label = string(item.label) || defaultLabel;
3204
+ if (!label || label === internalCode || label === pageKey) {
3205
+ diagnostics.push(diagnostic('APP_CONFIG_ADMIN_USER_LABEL_REQUIRED', '导航页面必须解析为用户可读 label,不能泄漏内部 code', item.label === undefined ? `${itemPath}.page` : `${itemPath}.label`));
3206
+ }
3207
+ });
3208
+ });
3209
+ if (itemCount > 500) {
3210
+ diagnostics.push(diagnostic('APP_CONFIG_ADMIN_NAVIGATION_LIMIT_EXCEEDED', '后台导航最多包含 500 个页面引用', 'frontend.admin.navigation'));
3211
+ }
3212
+ }
3213
+ function validateAuthorizationTransitions(rawTransitions, _roleCodes, diagnostics) {
3214
+ if (rawTransitions !== undefined && !Array.isArray(rawTransitions)) {
3215
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_TRANSITIONS_INVALID', 'authz.authorizationTransitions 必须是数组', 'authz.authorizationTransitions'));
3216
+ return;
3217
+ }
3218
+ const digests = new Set();
3219
+ (Array.isArray(rawTransitions) ? rawTransitions : []).forEach((raw, index) => {
3220
+ const path = `authz.authorizationTransitions[${index}]`;
3221
+ if (!isPlainRecord(raw)) {
3222
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_TRANSITION_INVALID', '授权 transition 必须是只含 canonical 字段的对象', path));
3223
+ return;
3224
+ }
3225
+ if (!exactKeys(raw, AUTHORIZATION_TRANSITION_KEYS)) {
3226
+ const unknownKey = Object.keys(raw).find(key => !AUTHORIZATION_TRANSITION_KEYS.includes(key));
3227
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_TRANSITION_INVALID', '授权 transition 只允许 fromAuthzDigest、removeRoleCodes、removeCapabilityCodes、reason', unknownKey ? `${path}.${unknownKey}` : path));
3228
+ return;
3229
+ }
3230
+ const transition = raw;
3231
+ const digest = transition.fromAuthzDigest;
3232
+ const removeRolesValid = transition.removeRoleCodes === undefined ||
3233
+ isExactStringArray(transition.removeRoleCodes, AUTHORIZATION_TRANSITION_ROLE_CODE_PATTERN, 128);
3234
+ const removeCapabilitiesValid = transition.removeCapabilityCodes === undefined ||
3235
+ isExactStringArray(transition.removeCapabilityCodes, AUTHORIZATION_TRANSITION_CAPABILITY_CODE_PATTERN, 255);
3236
+ if (typeof digest !== 'string' ||
3237
+ !/^[0-9a-f]{64}$/.test(digest) ||
3238
+ digests.has(digest) ||
3239
+ typeof transition.reason !== 'string' ||
3240
+ !transition.reason.trim() ||
3241
+ transition.reason.length > 2000 ||
3242
+ !removeRolesValid ||
3243
+ !removeCapabilitiesValid) {
3244
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_TRANSITION_INVALID', '授权 transition 必须按来源摘要唯一,并使用有界、不重复的 canonical 移除项和 reason', path));
3245
+ }
3246
+ if (typeof digest === 'string' && /^[0-9a-f]{64}$/.test(digest)) {
3247
+ digests.add(digest);
3248
+ }
3249
+ });
3250
+ }
3251
+ function validateCapabilityClosure(appCode, config, declarations, diagnostics) {
3252
+ const catalog = new Set(platformCapabilityCodes(appCode));
3253
+ for (const raw of declarations)
3254
+ catalog.add(string(object(raw).code));
3255
+ const data = object(config.data);
3256
+ const catalogOwners = new Map();
3257
+ for (const raw of declarations) {
3258
+ const code = string(object(raw).code);
3259
+ if (code)
3260
+ catalogOwners.set(code, 'authz.capabilities');
3261
+ }
3262
+ for (const rawResource of Array.isArray(data.resources) ? data.resources : []) {
3263
+ const resource = object(rawResource);
3264
+ const resourceOwner = `data.resources.${string(resource.code)}`;
3265
+ Object.values(object(resource.capabilities)).forEach(value => {
3266
+ const code = string(value);
3267
+ registerCapabilityOwner(code, resourceOwner, catalogOwners, diagnostics);
3268
+ catalog.add(code);
3269
+ });
3270
+ Object.values(object(resource.fieldPolicies)).forEach(rawPolicy => {
3271
+ const policy = object(rawPolicy);
3272
+ for (const key of ['read', 'create', 'update']) {
3273
+ for (const value of Array.isArray(policy[key]) ? policy[key] : []) {
3274
+ const code = string(value);
3275
+ registerCapabilityOwner(code, resourceOwner, catalogOwners, diagnostics);
3276
+ catalog.add(code);
3277
+ }
3278
+ }
3279
+ });
3280
+ }
3281
+ const references = [];
3282
+ const authz = object(config.authz);
3283
+ (Array.isArray(authz.roles) ? authz.roles : []).forEach((rawRole, index) => {
3284
+ const role = object(rawRole);
3285
+ (Array.isArray(role.capabilities) ? role.capabilities : []).forEach((value, capabilityIndex) => references.push({
3286
+ capability: string(value),
3287
+ path: `authz.roles[${index}].capabilities[${capabilityIndex}]`,
3288
+ }));
3289
+ });
3290
+ const backend = object(config.backend);
3291
+ (Array.isArray(backend.operations) ? backend.operations : []).forEach((rawOperation, index) => references.push({
3292
+ capability: string(object(rawOperation).capability),
3293
+ path: `backend.operations[${index}].capability`,
3294
+ }));
3295
+ const frontend = object(config.frontend);
3296
+ (Array.isArray(frontend.routes) ? frontend.routes : []).forEach((rawRoute, index) => {
3297
+ const route = object(rawRoute);
3298
+ const capability = string(route.capability);
3299
+ if (capability) {
3300
+ references.push({
3301
+ capability,
3302
+ path: `frontend.routes[${index}].capability`,
3303
+ });
3304
+ }
3305
+ const access = object(route.access);
3306
+ for (const key of ['allOf', 'anyOf']) {
3307
+ const values = Array.isArray(access[key]) ? access[key] : [];
3308
+ values.forEach((value, capabilityIndex) => references.push({
3309
+ capability: string(value),
3310
+ path: `frontend.routes[${index}].access.${key}[${capabilityIndex}]`,
3311
+ }));
3312
+ }
3313
+ });
3314
+ const adminAccess = object(object(frontend.admin).access);
3315
+ for (const key of ['allOf', 'anyOf']) {
3316
+ const values = Array.isArray(adminAccess[key]) ? adminAccess[key] : [];
3317
+ values.forEach((value, capabilityIndex) => references.push({
3318
+ capability: string(value),
3319
+ path: `frontend.admin.access.${key}[${capabilityIndex}]`,
3320
+ }));
3321
+ }
3322
+ for (const reference of references) {
3323
+ if (!catalog.has(reference.capability) ||
3324
+ (!reference.capability.startsWith(`app:${appCode}:`) &&
3325
+ !platformCapabilityCodes(appCode).has(reference.capability))) {
3326
+ diagnostics.push(diagnostic('APP_CONFIG_CAPABILITY_CLOSURE_FAILED', `引用了未声明、跨应用或非平台保留 capability: ${reference.capability}`, reference.path));
3327
+ }
3328
+ }
3329
+ }
3330
+ function registerCapabilityOwner(code, owner, owners, diagnostics) {
3331
+ if (!code)
3332
+ return;
3333
+ const existing = owners.get(code);
3334
+ if (existing && existing !== owner) {
3335
+ diagnostics.push(diagnostic('APP_CONFIG_CAPABILITY_OWNER_CONFLICT', `capability 只能由一个 catalog owner 定义: ${code}`, owner));
3336
+ return;
3337
+ }
3338
+ owners.set(code, owner);
3339
+ }
3340
+ function platformCapabilityCodes(appCode) {
3341
+ return new Set(nativePlatformCapabilityCatalog(appCode).map(item => item.code));
3342
+ }
3343
+ function validAppPath(value) {
3344
+ return (value.startsWith('/') &&
3345
+ !value.startsWith('//') &&
3346
+ !value.includes('..') &&
3347
+ !value.includes('?') &&
3348
+ !value.includes('#') &&
3349
+ value.length <= 2048);
3350
+ }
3351
+ function isRecord(value) {
3352
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
3353
+ }
3354
+ export class AppConfigValidationError extends Error {
3355
+ diagnostics;
3356
+ code = 'OPENXIANGDA_APP_CONFIG_INVALID';
3357
+ constructor(diagnostics) {
3358
+ super(diagnostics.map(item => `${item.path}: ${item.message}`).join('; '));
3359
+ this.diagnostics = diagnostics;
3360
+ this.name = 'AppConfigValidationError';
3361
+ }
3362
+ }
3363
+ export function resourceCapabilityCodes(appCode, resourceCode) {
3364
+ const prefix = `app:${appCode}:data:${resourceCode}`;
3365
+ return {
3366
+ read: `${prefix}:read`,
3367
+ create: `${prefix}:create`,
3368
+ update: `${prefix}:update`,
3369
+ delete: `${prefix}:delete`,
3370
+ };
3371
+ }
3372
+ export function currentUserDataPolicy(input) {
3373
+ return {
3374
+ code: input.code,
3375
+ name: input.name,
3376
+ resourceCode: input.resourceCode,
3377
+ ...(input.unrestrictedRoleCodes
3378
+ ? { unrestrictedRoleCodes: [...input.unrestrictedRoleCodes] }
3379
+ : {}),
3380
+ matchMode: 'AND',
3381
+ rules: [
3382
+ {
3383
+ subject: 'current_user',
3384
+ field: input.field,
3385
+ roleCodes: [...input.roleCodes],
3386
+ },
3387
+ ],
3388
+ };
3389
+ }
3390
+ export const dataPolicyExpression = {
3391
+ allOf: (...values) => ({ allOf: values }),
3392
+ anyOf: (...values) => ({ anyOf: values }),
3393
+ constant: (input) => ({ ...input }),
3394
+ null: (input) => ({ ...input }),
3395
+ databaseNow: (input) => ({ ...input, operand: 'db_now' }),
3396
+ currentUser: (input) => ({ ...input, subject: 'current_user' }),
3397
+ dimension: (input) => ({ ...input }),
3398
+ relation: (input) => ({ ...input }),
3399
+ };
3400
+ export function resourceReadPolicy(input) {
3401
+ const common = {
3402
+ code: input.code,
3403
+ name: input.name,
3404
+ resourceCode: input.resourceCode,
3405
+ ...(input.unrestrictedRoleCodes
3406
+ ? { unrestrictedRoleCodes: [...input.unrestrictedRoleCodes] }
3407
+ : {}),
3408
+ readExpression: input.expression,
3409
+ };
3410
+ if (input.writeBoundary === 'capability_only') {
3411
+ return {
3412
+ ...common,
3413
+ matchMode: 'AND',
3414
+ rules: [],
3415
+ writeBoundary: 'capability_only',
3416
+ };
3417
+ }
3418
+ return {
3419
+ ...common,
3420
+ matchMode: input.matchMode,
3421
+ rules: input.rules,
3422
+ };
3423
+ }
3424
+ export function materializeDataResource(appCode, declaration) {
3425
+ const capabilities = resourceCapabilityCodes(appCode, declaration.code);
3426
+ const mutationOwner = declaration.mutationOwner || 'native';
3427
+ const nativeMutations = mutationOwner === 'native';
3428
+ const generated = {
3429
+ list: declaration.generated?.list ?? true,
3430
+ detail: declaration.generated?.detail ?? true,
3431
+ create: declaration.generated?.create ?? nativeMutations,
3432
+ update: declaration.generated?.update ?? nativeMutations,
3433
+ delete: declaration.generated?.delete ?? nativeMutations,
3434
+ };
3435
+ const fields = declaration.fields.map(field => {
3436
+ const definition = {
3437
+ code: field.code,
3438
+ type: field.type,
3439
+ nullable: fieldNullable({
3440
+ type: field.type,
3441
+ nullable: field.required !== true,
3442
+ }),
3443
+ indexed: field.indexed ??
3444
+ Boolean(field.filter || field.searchable || field.sortable),
3445
+ ...(field.options ? { options: field.options } : {}),
3446
+ ...(field.source ? { source: field.source } : {}),
3447
+ ...(field.maxLength !== undefined ? { maxLength: field.maxLength } : {}),
3448
+ ...(field.precision !== undefined ? { precision: field.precision } : {}),
3449
+ ...(field.scale !== undefined ? { scale: field.scale } : {}),
3450
+ ...(field.min !== undefined ? { min: field.min } : {}),
3451
+ ...(field.max !== undefined ? { max: field.max } : {}),
3452
+ ...(field.rangeBoundary
3453
+ ? { rangeBoundary: field.rangeBoundary }
3454
+ : {}),
3455
+ ...(field.timePrecision ? { timePrecision: field.timePrecision } : {}),
3456
+ ...(field.file ? { file: field.file } : {}),
3457
+ ...(field.serial ? { serial: field.serial } : {}),
3458
+ ...(field.subtable ? { subtable: field.subtable } : {}),
3459
+ };
3460
+ physicalPlanForField(definition, field.searchable === true);
3461
+ return definition;
3462
+ });
3463
+ const visible = declaration.fields
3464
+ .filter(field => field.list !== false)
3465
+ .slice(0, 8);
3466
+ const searchableFields = declaration.fields
3467
+ .filter(field => field.searchable === true)
3468
+ .map(field => field.code);
3469
+ const filterFields = declaration.fields
3470
+ .filter(field => field.filter === true)
3471
+ .map(field => field.code);
3472
+ const access = (field, operation) => {
3473
+ if (operation !== 'read' &&
3474
+ !supportsGeneratedMutation({
3475
+ type: field.type,
3476
+ system: field.system,
3477
+ })) {
3478
+ return [];
3479
+ }
3480
+ const override = field.access?.[operation];
3481
+ if (override === false)
3482
+ return [];
3483
+ if (Array.isArray(override))
3484
+ return [...override];
3485
+ return [capabilities[operation]];
3486
+ };
3487
+ const surface = {
3488
+ mutationOwner,
3489
+ generated,
3490
+ fields: Object.fromEntries(declaration.fields.map(field => [
3491
+ field.code,
3492
+ {
3493
+ label: field.label,
3494
+ type: field.type,
3495
+ widget: resolveDataFieldSurfaceWidget(field),
3496
+ ...(field.section ? { section: field.section } : {}),
3497
+ ...(field.required === true ? { requiredHint: true } : {}),
3498
+ ...(field.system !== undefined ? { system: field.system } : {}),
3499
+ readCapabilities: access(field, 'read'),
3500
+ createCapabilities: access(field, 'create'),
3501
+ updateCapabilities: access(field, 'update'),
3502
+ ...(field.maxLength !== undefined ? { maxLength: field.maxLength } : {}),
3503
+ ...(field.precision !== undefined ? { precision: field.precision } : {}),
3504
+ ...(field.scale !== undefined ? { scale: field.scale } : {}),
3505
+ ...(field.min !== undefined ? { min: field.min } : {}),
3506
+ ...(field.max !== undefined ? { max: field.max } : {}),
3507
+ ...(field.rangeBoundary
3508
+ ? { rangeBoundary: field.rangeBoundary }
3509
+ : {}),
3510
+ ...(field.file?.maxCount !== undefined
3511
+ ? { maxCount: field.file.maxCount }
3512
+ : {}),
3513
+ ...(field.file?.maxSizeMb !== undefined
3514
+ ? { maxSizeMb: field.file.maxSizeMb }
3515
+ : {}),
3516
+ ...(field.file?.accept ? { accept: field.file.accept } : {}),
3517
+ ...(field.options ? { options: field.options } : {}),
3518
+ ...(field.source ? { source: field.source } : {}),
3519
+ ...(field.timePrecision ? { timePrecision: field.timePrecision } : {}),
3520
+ ...(field.serial ? { serial: field.serial } : {}),
3521
+ ...(field.subtable ? { subtable: field.subtable } : {}),
3522
+ ...(visible.some(item => item.code === field.code)
3523
+ ? { list: true }
3524
+ : { list: false }),
3525
+ ...(field.searchable !== undefined
3526
+ ? { searchable: field.searchable }
3527
+ : {}),
3528
+ ...(field.sortable !== undefined ? { sortable: field.sortable } : {}),
3529
+ },
3530
+ ])),
3531
+ list: {
3532
+ defaultPageSize: declaration.list?.defaultPageSize || 20,
3533
+ ...(searchableFields.length ? { searchableFields } : {}),
3534
+ ...(filterFields.length ? { filterFields } : {}),
3535
+ ...(declaration.list?.defaultSort
3536
+ ? { defaultSort: declaration.list.defaultSort }
3537
+ : visible[0]
3538
+ ? { defaultSort: { field: visible[0].code, order: 'asc' } }
3539
+ : {}),
3540
+ },
3541
+ form: {
3542
+ layout: declaration.form?.layout || 'flat',
3543
+ fieldOrder: declaration.fields.map(field => field.code),
3544
+ },
3545
+ detail: {
3546
+ layout: declaration.detail?.layout || 'flat',
3547
+ fieldOrder: declaration.fields.map(field => field.code),
3548
+ },
3549
+ mobile: declaration.mobile || { enabled: true },
3550
+ };
3551
+ return {
3552
+ schemaVersion: SCHEMA_VERSIONS.dataResource,
3553
+ appCode,
3554
+ code: declaration.code,
3555
+ name: declaration.name,
3556
+ schema: { fields },
3557
+ ...(declaration.invariants ? { invariants: declaration.invariants } : {}),
3558
+ surface,
3559
+ capabilities,
3560
+ ...(declaration.dataPolicyCode
3561
+ ? { dataPolicyCode: declaration.dataPolicyCode }
3562
+ : {}),
3563
+ fieldPolicies: Object.fromEntries(declaration.fields.map(field => [
3564
+ field.code,
3565
+ {
3566
+ read: access(field, 'read'),
3567
+ create: access(field, 'create'),
3568
+ update: access(field, 'update'),
3569
+ ...(field.access?.mask ? { mask: field.access.mask } : {}),
3570
+ },
3571
+ ])),
3572
+ };
3573
+ }
3574
+ export function validateAppDeclaration(value) {
3575
+ const declaration = object(value);
3576
+ const appCode = string(object(declaration.app).code);
3577
+ const data = object(declaration.data);
3578
+ const resources = Array.isArray(data.resources) ? data.resources : [];
3579
+ const diagnostics = [];
3580
+ const resourceKeys = new Set([
3581
+ 'code',
3582
+ 'name',
3583
+ 'mutationOwner',
3584
+ 'generated',
3585
+ 'fields',
3586
+ 'invariants',
3587
+ 'list',
3588
+ 'form',
3589
+ 'detail',
3590
+ 'mobile',
3591
+ 'dataPolicyCode',
3592
+ ]);
3593
+ const fieldKeys = new Set([
3594
+ 'code',
3595
+ 'type',
3596
+ 'label',
3597
+ 'required',
3598
+ 'indexed',
3599
+ 'options',
3600
+ 'source',
3601
+ 'maxLength',
3602
+ 'precision',
3603
+ 'scale',
3604
+ 'min',
3605
+ 'max',
3606
+ 'rangeBoundary',
3607
+ 'timePrecision',
3608
+ 'file',
3609
+ 'serial',
3610
+ 'subtable',
3611
+ 'widget',
3612
+ 'section',
3613
+ 'system',
3614
+ 'list',
3615
+ 'filter',
3616
+ 'searchable',
3617
+ 'sortable',
3618
+ 'access',
3619
+ ]);
3620
+ resources.forEach((rawResource, resourceIndex) => {
3621
+ const resource = object(rawResource);
3622
+ const resourcePath = `data.resources[${resourceIndex}]`;
3623
+ for (const key of Object.keys(resource)) {
3624
+ if (!resourceKeys.has(key)) {
3625
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_PROPERTY_UNSUPPORTED', `${resourcePath}.${key} 不属于单次资源声明合同`, `${resourcePath}.${key}`, '只声明 code、name、fields 及可选列表/布局/数据策略'));
3626
+ }
3627
+ }
3628
+ const fields = Array.isArray(resource.fields) ? resource.fields : [];
3629
+ const mutationOwner = string(resource.mutationOwner) || 'native';
3630
+ const generated = object(resource.generated);
3631
+ if (!['native', 'action', 'readonly', 'workflow'].includes(mutationOwner)) {
3632
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_MUTATION_OWNER_INVALID', 'mutationOwner 必须是 native、action、readonly 或 workflow', `${resourcePath}.mutationOwner`));
3633
+ }
3634
+ for (const key of Object.keys(generated)) {
3635
+ if (!['list', 'detail', 'create', 'update', 'delete'].includes(key)) {
3636
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_GENERATED_SURFACE_INVALID', `generated.${key} 不是受支持的标准页面/操作`, `${resourcePath}.generated.${key}`));
3637
+ }
3638
+ else if (typeof generated[key] !== 'boolean') {
3639
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_GENERATED_SURFACE_INVALID', `generated.${key} 必须是 boolean`, `${resourcePath}.generated.${key}`));
3640
+ }
3641
+ }
3642
+ if (mutationOwner !== 'native') {
3643
+ for (const operation of ['create', 'update', 'delete']) {
3644
+ if (generated[operation] === true) {
3645
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_NATIVE_MUTATION_OWNER_CONFLICT', `${mutationOwner} owns mutations; Native ${operation} cannot be generated`, `${resourcePath}.generated.${operation}`));
3646
+ }
3647
+ }
3648
+ }
3649
+ if (fields.length === 0) {
3650
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_FIELDS_REQUIRED', '资源必须直接声明非空 fields 数组', `${resourcePath}.fields`));
3651
+ return;
3652
+ }
3653
+ const declaredCodes = new Set();
3654
+ let materializable = true;
3655
+ fields.forEach((rawField, fieldIndex) => {
3656
+ const field = object(rawField);
3657
+ const fieldPath = `${resourcePath}.fields[${fieldIndex}]`;
3658
+ for (const key of Object.keys(field)) {
3659
+ if (!fieldKeys.has(key)) {
3660
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_FIELD_PROPERTY_UNSUPPORTED', `${fieldPath}.${key} 不属于单次字段声明合同`, `${fieldPath}.${key}`));
3661
+ }
3662
+ }
3663
+ const code = string(field.code);
3664
+ const fieldType = string(field.type);
3665
+ if (!APP_DATA_FIELD_TYPES.has(fieldType)) {
3666
+ materializable = false;
3667
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_FIELD_TYPE_INVALID', `${fieldPath}.type 必须是 OpenXiangda 2.0 语义字段类型之一: ${DATA_FIELD_TYPES.join('、')}`, `${fieldPath}.type`, '直接声明业务语义类型,不声明 PostgreSQL 物理类型'));
3668
+ }
3669
+ else {
3670
+ const semanticType = fieldType;
3671
+ const widget = string(field.widget);
3672
+ if (widget && !isCompatibleFieldWidget({ type: semanticType, widget })) {
3673
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_FIELD_WIDGET_INCOMPATIBLE', `${fieldPath}.widget 与 ${semanticType} 不兼容;允许 ${compatibleWidgets(semanticType).join('、')}`, `${fieldPath}.widget`));
3674
+ }
3675
+ if (field.filter === true && !supportsFilter(semanticType)) {
3676
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_FIELD_FILTER_UNSUPPORTED', `${semanticType} 不支持作为父资源筛选字段`, `${fieldPath}.filter`));
3677
+ }
3678
+ if (field.searchable === true && !supportsSearch(semanticType)) {
3679
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_FIELD_SEARCH_UNSUPPORTED', `${semanticType} 没有全文/模糊搜索和 trigram 索引计划`, `${fieldPath}.searchable`));
3680
+ }
3681
+ if (field.sortable === true && !supportsSort(semanticType)) {
3682
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_FIELD_SORT_UNSUPPORTED', `${semanticType} 不支持稳定排序`, `${fieldPath}.sortable`));
3683
+ }
3684
+ const rangeType = semanticType === 'date-range' || semanticType === 'datetime-range';
3685
+ if (rangeType &&
3686
+ !['closed', 'half-open'].includes(string(field.rangeBoundary))) {
3687
+ materializable = false;
3688
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_FIELD_RANGE_BOUNDARY_REQUIRED', `${semanticType} 必须显式声明 rangeBoundary: closed 或 half-open`, `${fieldPath}.rangeBoundary`));
3689
+ }
3690
+ else if (!rangeType && field.rangeBoundary !== undefined) {
3691
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_FIELD_RANGE_BOUNDARY_TYPE_INVALID', 'rangeBoundary 只能用于 date-range 或 datetime-range', `${fieldPath}.rangeBoundary`));
3692
+ }
3693
+ if (field.indexed === false &&
3694
+ (field.filter === true ||
3695
+ field.searchable === true ||
3696
+ field.sortable === true)) {
3697
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_FIELD_INDEX_REQUIRED', '启用筛选、搜索或排序时不能显式关闭索引', `${fieldPath}.indexed`));
3698
+ }
3699
+ if (['radio', 'checkbox'].includes(widget) &&
3700
+ object(field.source).loadMode !== 'all' &&
3701
+ field.source !== undefined) {
3702
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_FIELD_SOURCE_LOAD_MODE_INVALID', '动态 Radio/Checkbox 必须声明 source.loadMode = all', `${fieldPath}.source.loadMode`));
3703
+ }
3704
+ }
3705
+ if (!string(field.label)) {
3706
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_FIELD_LABEL_REQUIRED', '字段必须同时声明中文或业务 label', `${fieldPath}.label`));
3707
+ }
3708
+ for (const key of [
3709
+ 'required',
3710
+ 'indexed',
3711
+ 'system',
3712
+ 'list',
3713
+ 'filter',
3714
+ 'searchable',
3715
+ 'sortable',
3716
+ ]) {
3717
+ if (field[key] !== undefined && typeof field[key] !== 'boolean') {
3718
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_FIELD_BOOLEAN_INVALID', `${fieldPath}.${key} 必须为 boolean`, `${fieldPath}.${key}`));
3719
+ }
3720
+ }
3721
+ const accessDeclaration = object(field.access);
3722
+ for (const key of Object.keys(accessDeclaration)) {
3723
+ if (!['read', 'create', 'update', 'mask'].includes(key)) {
3724
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_FIELD_ACCESS_PROPERTY_UNSUPPORTED', `${fieldPath}.access.${key} 不受支持`, `${fieldPath}.access.${key}`));
3725
+ }
3726
+ }
3727
+ for (const operation of ['read', 'create', 'update']) {
3728
+ const grant = accessDeclaration[operation];
3729
+ if (grant !== undefined &&
3730
+ grant !== false &&
3731
+ (!Array.isArray(grant) ||
3732
+ grant.length === 0 ||
3733
+ grant.some(item => typeof item !== 'string' || !item.trim()))) {
3734
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_FIELD_ACCESS_INVALID', `${fieldPath}.access.${operation} 必须为 false 或非空 capability 数组`, `${fieldPath}.access.${operation}`));
3735
+ }
3736
+ }
3737
+ if (code)
3738
+ declaredCodes.add(code);
3739
+ });
3740
+ for (const operation of ['create', 'update']) {
3741
+ const enabled = generated[operation] ?? mutationOwner === 'native';
3742
+ const writable = fields.some(rawField => {
3743
+ const field = object(rawField);
3744
+ return (APP_DATA_FIELD_TYPES.has(string(field.type)) &&
3745
+ supportsGeneratedMutation({
3746
+ type: string(field.type),
3747
+ system: field.system === true,
3748
+ }) &&
3749
+ object(field.access)[operation] !== false);
3750
+ });
3751
+ if (materializable && enabled && !writable) {
3752
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_WRITABLE_FIELDS_REQUIRED', `generated.${operation} 需要至少一个可写业务字段`, `${resourcePath}.generated.${operation}`));
3753
+ }
3754
+ }
3755
+ const defaultSortField = string(object(resource.list).defaultSort && object(object(resource.list).defaultSort).field);
3756
+ if (defaultSortField && !declaredCodes.has(defaultSortField)) {
3757
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_SORT_FIELD_INVALID', '默认排序字段必须在同一资源 fields 中声明', `${resourcePath}.list.defaultSort.field`));
3758
+ }
3759
+ if (appCode && fields.length && materializable) {
3760
+ const canonical = materializeDataResource(appCode, rawResource);
3761
+ diagnostics.push(...validateDataResource(canonical).map(item => ({
3762
+ ...item,
3763
+ path: `${resourcePath}.${item.path}`,
3764
+ source: 'openxiangda-app.config.ts',
3765
+ })));
3766
+ }
3767
+ });
3768
+ const declaredResources = new Map(resources.map((rawResource, resourceIndex) => {
3769
+ const resource = object(rawResource);
3770
+ return [
3771
+ string(resource.code),
3772
+ {
3773
+ resource,
3774
+ resourceIndex,
3775
+ fields: new Map((Array.isArray(resource.fields) ? resource.fields : []).map(rawField => {
3776
+ const field = object(rawField);
3777
+ return [string(field.code), field];
3778
+ })),
3779
+ },
3780
+ ];
3781
+ }));
3782
+ for (const [resourceCode, declaration] of declaredResources) {
3783
+ const fields = Array.isArray(declaration.resource.fields)
3784
+ ? declaration.resource.fields
3785
+ : [];
3786
+ const aggregateMaxRows = fields.reduce((total, rawField) => {
3787
+ const field = object(rawField);
3788
+ if (field.type !== 'subtable')
3789
+ return total;
3790
+ const configured = Number(object(field.subtable).maxRows);
3791
+ return total + (Number.isSafeInteger(configured) ? configured : 20);
3792
+ }, 0);
3793
+ if (aggregateMaxRows > 49) {
3794
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_SUBTABLE_AGGREGATE_MAX_ROWS_EXCEEDED', '同一父资源的所有子表 maxRows 总和不能超过 49', `data.resources[${declaration.resourceIndex}].fields`));
3795
+ }
3796
+ fields.forEach((rawField, fieldIndex) => {
3797
+ const field = object(rawField);
3798
+ if (field.type !== 'subtable')
3799
+ return;
3800
+ const path = `data.resources[${declaration.resourceIndex}].fields[${fieldIndex}].subtable`;
3801
+ const subtable = object(field.subtable);
3802
+ const targetCode = string(subtable.resourceCode);
3803
+ const target = declaredResources.get(targetCode);
3804
+ if (!target || targetCode === resourceCode) {
3805
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_SUBTABLE_TARGET_INVALID', '子表必须引用同一应用内不同的子资源', `${path}.resourceCode`));
3806
+ return;
3807
+ }
3808
+ const foreignKey = target.fields.get(string(subtable.foreignKey));
3809
+ if (!foreignKey ||
3810
+ foreignKey.type !== 'uuid' ||
3811
+ foreignKey.required !== true ||
3812
+ object(foreignKey.access).create === false) {
3813
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_SUBTABLE_FOREIGN_KEY_INVALID', '子表外键必须是子资源中可创建写入的必填 uuid 字段', `${path}.foreignKey`));
3814
+ }
3815
+ const orderField = target.fields.get(string(subtable.orderField));
3816
+ if (!orderField ||
3817
+ orderField.type !== 'number.integer' ||
3818
+ orderField.required !== true ||
3819
+ object(orderField.access).create === false ||
3820
+ object(orderField.access).update === false) {
3821
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_SUBTABLE_ORDER_FIELD_INVALID', '子表排序字段必须是子资源中可创建和更新的必填 number.integer 字段', `${path}.orderField`));
3822
+ }
3823
+ });
3824
+ }
3825
+ return diagnostics;
3826
+ }
3827
+ export function defineOpenXiangdaApp(declaration) {
3828
+ const declarationDiagnostics = validateAppDeclaration(declaration);
3829
+ if (declarationDiagnostics.length > 0) {
3830
+ throw new AppConfigValidationError(declarationDiagnostics);
3831
+ }
3832
+ const { data, ...application } = declaration;
3833
+ const config = {
3834
+ ...application,
3835
+ ...(data
3836
+ ? {
3837
+ data: {
3838
+ resources: data.resources.map(resource => materializeDataResource(declaration.app.code, resource)),
3839
+ },
3840
+ }
3841
+ : {}),
3842
+ };
3843
+ const diagnostics = validateAppConfig(config);
3844
+ if (diagnostics.length > 0)
3845
+ throw new AppConfigValidationError(diagnostics);
3846
+ return Object.freeze(config);
3847
+ }
3848
+ //# sourceMappingURL=config.js.map