openxiangda-devkit-core 2.0.0-alpha.86 → 2.0.0-alpha.88

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.
@@ -335,6 +335,9 @@ export const openXiangdaAppConfigSchema = {
335
335
  properties: {
336
336
  root: { type: 'string', minLength: 1 },
337
337
  routes: { type: 'array', maxItems: 500, items: { type: 'object' } },
338
+ admin: { type: 'object' },
339
+ authentication: { type: 'object' },
340
+ publicAccess: { type: 'object' },
338
341
  },
339
342
  },
340
343
  backend: {
@@ -815,6 +818,7 @@ export function validateAppConfig(value) {
815
818
  }
816
819
  validateApplicationAuthentication(config, diagnostics);
817
820
  validateFrontendRoutes(config, diagnostics);
821
+ validateAnonymousPublicAccess(config, diagnostics);
818
822
  validateAdminNavigation(config, diagnostics);
819
823
  validatePerspectives(config.perspectives, object(config.authz).roles, diagnostics);
820
824
  const declaredResources = new Map((Array.isArray(object(config.data).resources)
@@ -2493,6 +2497,168 @@ function validateFrontendRoutes(config, diagnostics) {
2493
2497
  }
2494
2498
  }
2495
2499
  }
2500
+ const ANONYMOUS_PUBLIC_OPERATIONS = new Set([
2501
+ 'draft.read',
2502
+ 'draft.update',
2503
+ 'validate',
2504
+ 'create',
2505
+ 'own.list',
2506
+ 'own.read',
2507
+ ]);
2508
+ function validateAnonymousPublicAccess(config, diagnostics) {
2509
+ const frontend = object(config.frontend);
2510
+ if (frontend.publicAccess === undefined)
2511
+ return;
2512
+ const publicAccess = object(frontend.publicAccess);
2513
+ const policies = Array.isArray(publicAccess.policies)
2514
+ ? publicAccess.policies
2515
+ : [];
2516
+ if (Object.keys(publicAccess).some(key => key !== 'policies') ||
2517
+ !Array.isArray(publicAccess.policies) ||
2518
+ policies.length < 1 ||
2519
+ policies.length > 16) {
2520
+ diagnostics.push(diagnostic('APP_CONFIG_ANONYMOUS_PUBLIC_ACCESS_INVALID', 'frontend.publicAccess 只接受 1 到 16 个 policies', 'frontend.publicAccess'));
2521
+ return;
2522
+ }
2523
+ const routes = new Map((Array.isArray(frontend.routes) ? frontend.routes : []).map(raw => {
2524
+ const route = object(raw);
2525
+ return [string(route.code), route];
2526
+ }));
2527
+ const resourceMap = new Map((Array.isArray(object(config.data).resources)
2528
+ ? object(config.data).resources
2529
+ : []).map(raw => {
2530
+ const resource = object(raw);
2531
+ const fields = Array.isArray(object(resource.schema).fields)
2532
+ ? object(resource.schema).fields
2533
+ : [];
2534
+ const fieldMap = new Map(fields.map(field => {
2535
+ const declaration = object(field);
2536
+ return [string(declaration.code), string(declaration.type)];
2537
+ }));
2538
+ const requiredCreateFields = new Set(fields
2539
+ .map(object)
2540
+ .filter(field => {
2541
+ const type = string(field.type);
2542
+ return (field.nullable === false &&
2543
+ APP_DATA_FIELD_TYPES.has(type) &&
2544
+ supportsGeneratedMutation({ type }));
2545
+ })
2546
+ .map(field => string(field.code)));
2547
+ const surface = object(resource.surface);
2548
+ const generated = object(surface.generated);
2549
+ const nativeCreate = (string(surface.mutationOwner) || 'native') === 'native' &&
2550
+ generated.create !== false;
2551
+ return [
2552
+ string(resource.code),
2553
+ { fields: fieldMap, requiredCreateFields, nativeCreate },
2554
+ ];
2555
+ }));
2556
+ const policyCodes = new Set();
2557
+ const routeCodes = new Set();
2558
+ policies.forEach((raw, index) => {
2559
+ const policy = object(raw);
2560
+ const path = `frontend.publicAccess.policies[${index}]`;
2561
+ const code = string(policy.code);
2562
+ const routeCode = string(policy.routeCode);
2563
+ const resourceCode = string(policy.resourceCode);
2564
+ const route = routes.get(routeCode);
2565
+ const resource = resourceMap.get(resourceCode);
2566
+ const resourceFields = resource?.fields;
2567
+ const operations = Array.isArray(policy.operations)
2568
+ ? policy.operations.map(string)
2569
+ : [];
2570
+ const fields = Array.isArray(policy.fields) ? policy.fields.map(string) : [];
2571
+ const requiredFields = Array.isArray(policy.requiredFields)
2572
+ ? policy.requiredFields.map(string)
2573
+ : [];
2574
+ const ownRecordFields = Array.isArray(policy.ownRecordFields)
2575
+ ? policy.ownRecordFields.map(string)
2576
+ : fields;
2577
+ const draft = object(policy.draft);
2578
+ const validations = Array.isArray(policy.validations)
2579
+ ? policy.validations
2580
+ : [];
2581
+ const exactRootKeys = [
2582
+ 'code',
2583
+ 'routeCode',
2584
+ 'mode',
2585
+ 'resourceCode',
2586
+ 'operations',
2587
+ 'fields',
2588
+ 'requiredFields',
2589
+ 'ownRecordFields',
2590
+ 'draft',
2591
+ 'validations',
2592
+ ];
2593
+ const invalid = Object.keys(policy).some(key => !exactRootKeys.includes(key)) ||
2594
+ !OPERATION_CODE_PATTERN.test(code) ||
2595
+ policyCodes.has(code) ||
2596
+ routeCodes.has(routeCode) ||
2597
+ policy.mode !== 'anonymous' ||
2598
+ !route ||
2599
+ route.surface !== 'user' ||
2600
+ route.capability !== undefined ||
2601
+ route.access !== undefined ||
2602
+ /[:*]/.test(string(route.path)) ||
2603
+ !resourceFields ||
2604
+ operations.length < 1 ||
2605
+ operations.length > ANONYMOUS_PUBLIC_OPERATIONS.size ||
2606
+ new Set(operations).size !== operations.length ||
2607
+ operations.some(operation => !ANONYMOUS_PUBLIC_OPERATIONS.has(operation)) ||
2608
+ fields.length < 1 ||
2609
+ fields.length > 64 ||
2610
+ new Set(fields).size !== fields.length ||
2611
+ fields.some(field => !resourceFields?.has(field)) ||
2612
+ new Set(requiredFields).size !== requiredFields.length ||
2613
+ requiredFields.some(field => !fields.includes(field)) ||
2614
+ (operations.includes('create') &&
2615
+ (!resource?.nativeCreate ||
2616
+ [...(resource?.requiredCreateFields || [])].some(field => !fields.includes(field) || !requiredFields.includes(field)))) ||
2617
+ new Set(ownRecordFields).size !== ownRecordFields.length ||
2618
+ ownRecordFields.some(field => !fields.includes(field)) ||
2619
+ (operations.includes('draft.read') !== operations.includes('draft.update')) ||
2620
+ (operations.some(operation => operation.startsWith('draft.')) &&
2621
+ (policy.draft === undefined || draft.enabled !== true)) ||
2622
+ (policy.draft !== undefined &&
2623
+ (Object.keys(draft).some(key => !['enabled', 'inactivityTtlSeconds', 'maxBytes'].includes(key)) ||
2624
+ draft.enabled !== true ||
2625
+ (draft.inactivityTtlSeconds !== undefined &&
2626
+ (!Number.isSafeInteger(draft.inactivityTtlSeconds) ||
2627
+ Number(draft.inactivityTtlSeconds) < 3600 ||
2628
+ Number(draft.inactivityTtlSeconds) > 7_776_000)) ||
2629
+ (draft.maxBytes !== undefined &&
2630
+ (!Number.isSafeInteger(draft.maxBytes) ||
2631
+ Number(draft.maxBytes) < 4096 ||
2632
+ Number(draft.maxBytes) > 262_144)))) ||
2633
+ validations.length > 16;
2634
+ if (invalid) {
2635
+ diagnostics.push(diagnostic('APP_CONFIG_ANONYMOUS_PUBLIC_POLICY_INVALID', '匿名公开策略必须绑定唯一静态 user route、已声明资源/字段和有界操作', path));
2636
+ }
2637
+ const validationCodes = new Set();
2638
+ validations.forEach((rawValidation, validationIndex) => {
2639
+ const validation = object(rawValidation);
2640
+ const validationCode = string(validation.code);
2641
+ const validationFields = Array.isArray(validation.fields)
2642
+ ? validation.fields.map(string)
2643
+ : [];
2644
+ if (Object.keys(validation).some(key => !['code', 'kind', 'fields', 'result'].includes(key)) ||
2645
+ !OPERATION_CODE_PATTERN.test(validationCode) ||
2646
+ validationCodes.has(validationCode) ||
2647
+ validation.kind !== 'duplicate' ||
2648
+ validation.result !== 'availability' ||
2649
+ validationFields.length < 1 ||
2650
+ validationFields.length > 8 ||
2651
+ new Set(validationFields).size !== validationFields.length ||
2652
+ validationFields.some(field => !fields.includes(field)) ||
2653
+ validationFields.some(field => ['file', 'subtable'].includes(resourceFields?.get(field) || ''))) {
2654
+ diagnostics.push(diagnostic('APP_CONFIG_ANONYMOUS_PUBLIC_VALIDATION_INVALID', '匿名公开校验必须是只使用已授权字段的有界 duplicate availability 校验', `${path}.validations[${validationIndex}]`));
2655
+ }
2656
+ validationCodes.add(validationCode);
2657
+ });
2658
+ policyCodes.add(code);
2659
+ routeCodes.add(routeCode);
2660
+ });
2661
+ }
2496
2662
  function validateApplicationAuthentication(config, diagnostics) {
2497
2663
  const frontend = object(config.frontend);
2498
2664
  if (frontend.authentication === undefined)