openxiangda-devkit-core 2.0.0-alpha.78 → 2.0.0-alpha.80

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.
@@ -344,7 +344,20 @@ export const openXiangdaAppConfigSchema = {
344
344
  type: 'object',
345
345
  additionalProperties: false,
346
346
  required: ['root'],
347
- properties: { root: { type: 'string', minLength: 1 } },
347
+ properties: {
348
+ root: { type: 'string', minLength: 1 },
349
+ requiredCapabilities: {
350
+ type: 'array',
351
+ maxItems: 64,
352
+ uniqueItems: true,
353
+ items: {
354
+ type: 'string',
355
+ minLength: 1,
356
+ maxLength: 128,
357
+ pattern: '^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$',
358
+ },
359
+ },
360
+ },
348
361
  },
349
362
  perspectives: {
350
363
  type: 'array',
@@ -724,6 +737,16 @@ export function validateAppConfig(value) {
724
737
  if (!string(app.name)) {
725
738
  diagnostics.push(diagnostic('APP_CONFIG_NAME_REQUIRED', 'app.name 必须是非空字符串', 'app.name'));
726
739
  }
740
+ const requiredCapabilities = object(config.platform).requiredCapabilities;
741
+ if (requiredCapabilities !== undefined &&
742
+ (!Array.isArray(requiredCapabilities) ||
743
+ requiredCapabilities.length > 64 ||
744
+ new Set(requiredCapabilities).size !== requiredCapabilities.length ||
745
+ requiredCapabilities.some(capability => typeof capability !== 'string' ||
746
+ capability.length > 128 ||
747
+ !/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/.test(capability)))) {
748
+ diagnostics.push(diagnostic('APP_CONFIG_PLATFORM_CAPABILITIES_INVALID', 'platform.requiredCapabilities 必须是不重复的规范平台能力代码数组(最多 64 项)', 'platform.requiredCapabilities'));
749
+ }
727
750
  const frontend = object(config.frontend);
728
751
  const backend = object(config.backend);
729
752
  const platform = object(config.platform);
@@ -768,6 +791,7 @@ export function validateAppConfig(value) {
768
791
  !['light', 'standard'].includes(String(backend.resourceProfile))) {
769
792
  diagnostics.push(diagnostic('APP_CONFIG_BACKEND_RESOURCE_PROFILE_UNSUPPORTED', 'backend.resourceProfile 必须是 light 或 standard', 'backend.resourceProfile'));
770
793
  }
794
+ validateApplicationAuthentication(config, diagnostics);
771
795
  validateFrontendRoutes(config, diagnostics);
772
796
  validateAdminNavigation(config, diagnostics);
773
797
  validatePerspectives(config.perspectives, object(config.authz).roles, diagnostics);
@@ -2199,6 +2223,18 @@ function validateFrontendRoutes(config, diagnostics) {
2199
2223
  for (const claim of generatedPlatformRouteClaims(config)) {
2200
2224
  pathClaims.set(canonicalFrontendRouteShape(claim.path), claim);
2201
2225
  }
2226
+ const authentication = object(frontend.authentication);
2227
+ const authenticationSurfaces = object(authentication.surfaces);
2228
+ for (const device of ['desktop', 'mobile']) {
2229
+ const surface = object(authenticationSurfaces[device]);
2230
+ const surfacePath = string(surface.path);
2231
+ if (surfacePath) {
2232
+ pathClaims.set(canonicalFrontendRouteShape(surfacePath), {
2233
+ owner: `authentication:${device}`,
2234
+ path: surfacePath,
2235
+ });
2236
+ }
2237
+ }
2202
2238
  routes.forEach((raw, index) => {
2203
2239
  const route = object(raw);
2204
2240
  const code = string(route.code);
@@ -2297,6 +2333,124 @@ function validateFrontendRoutes(config, diagnostics) {
2297
2333
  }
2298
2334
  }
2299
2335
  }
2336
+ function validateApplicationAuthentication(config, diagnostics) {
2337
+ const frontend = object(config.frontend);
2338
+ if (frontend.authentication === undefined)
2339
+ return;
2340
+ const authentication = object(frontend.authentication);
2341
+ const allowedRootKeys = new Set([
2342
+ 'accountMode',
2343
+ 'registration',
2344
+ 'methods',
2345
+ 'surfaces',
2346
+ ]);
2347
+ const registration = object(authentication.registration);
2348
+ const surfaces = object(authentication.surfaces);
2349
+ const methods = Array.isArray(authentication.methods)
2350
+ ? authentication.methods
2351
+ : [];
2352
+ if (!frontend.authentication ||
2353
+ typeof frontend.authentication !== 'object' ||
2354
+ Array.isArray(frontend.authentication) ||
2355
+ Object.keys(authentication).some(key => !allowedRootKeys.has(key)) ||
2356
+ authentication.accountMode !== 'existing-platform-users-only' ||
2357
+ Object.keys(registration).some(key => key !== 'mode') ||
2358
+ registration.mode !== 'reject' ||
2359
+ !Array.isArray(authentication.methods) ||
2360
+ methods.length < 1 ||
2361
+ methods.length > 8 ||
2362
+ !authentication.surfaces ||
2363
+ typeof authentication.surfaces !== 'object' ||
2364
+ Array.isArray(authentication.surfaces) ||
2365
+ Object.keys(surfaces).some(key => !['desktop', 'mobile'].includes(key)) ||
2366
+ !surfaces.desktop ||
2367
+ !surfaces.mobile) {
2368
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHENTICATION_INVALID', 'frontend.authentication 只支持现有平台用户、拒绝注册、1-8 个方法和独立 desktop/mobile surface', 'frontend.authentication'));
2369
+ }
2370
+ const methodCodes = new Set();
2371
+ const methodTypes = new Set();
2372
+ let primaryCount = 0;
2373
+ methods.forEach((raw, index) => {
2374
+ const method = object(raw);
2375
+ const type = string(method.type);
2376
+ const code = string(method.code);
2377
+ const commonKeys = ['code', 'type', 'label', 'presentation', 'required'];
2378
+ const allowedKeys = new Set(type === 'sso'
2379
+ ? [...commonKeys, 'provider']
2380
+ : type === 'dingtalk'
2381
+ ? [...commonKeys, 'flow']
2382
+ : commonKeys);
2383
+ if (method.presentation === 'primary')
2384
+ primaryCount += 1;
2385
+ const invalid = Object.keys(method).some(key => !allowedKeys.has(key)) ||
2386
+ !OPERATION_CODE_PATTERN.test(code) ||
2387
+ methodCodes.has(code) ||
2388
+ methodTypes.has(type) ||
2389
+ !['sso', 'password', 'dingtalk'].includes(type) ||
2390
+ !string(method.label) ||
2391
+ !['primary', 'secondary'].includes(string(method.presentation)) ||
2392
+ typeof method.required !== 'boolean' ||
2393
+ (type === 'sso' && method.provider !== 'tenant-default') ||
2394
+ (type === 'dingtalk' &&
2395
+ !['auto', 'jsapi', 'oauth'].includes(string(method.flow)));
2396
+ if (invalid) {
2397
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHENTICATION_METHOD_INVALID', '登录方法必须使用唯一 code、受支持类型和不含 Secret 的有界公开描述', `frontend.authentication.methods[${index}]`));
2398
+ }
2399
+ methodCodes.add(code);
2400
+ methodTypes.add(type);
2401
+ });
2402
+ if (methods.length > 0 && primaryCount !== 1) {
2403
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHENTICATION_PRIMARY_METHOD_INVALID', '登录方法必须且只能有一个 primary', 'frontend.authentication.methods'));
2404
+ }
2405
+ const routes = Array.isArray(frontend.routes) ? frontend.routes : [];
2406
+ const routeByCode = new Map(routes.map(raw => {
2407
+ const route = object(raw);
2408
+ return [string(route.code), route];
2409
+ }));
2410
+ const routeCodes = new Set(routeByCode.keys());
2411
+ const claimedShapes = new Map(generatedPlatformRouteClaims(config).map(claim => [
2412
+ canonicalFrontendRouteShape(claim.path),
2413
+ claim,
2414
+ ]));
2415
+ const surfaceCodes = new Set();
2416
+ const surfaceShapes = new Set();
2417
+ for (const device of ['desktop', 'mobile']) {
2418
+ const surface = object(surfaces[device]);
2419
+ const pointer = `frontend.authentication.surfaces.${device}`;
2420
+ const routeCode = string(surface.routeCode);
2421
+ const routePath = string(surface.path);
2422
+ const defaultRouteCode = string(surface.defaultRouteCode);
2423
+ const defaultRoute = routeByCode.get(defaultRouteCode);
2424
+ const shape = canonicalFrontendRouteShape(routePath);
2425
+ const expectedPath = device === 'mobile' ? '/m/login' : '/login';
2426
+ const invalid = Object.keys(surface).some(key => !['routeCode', 'path', 'defaultRouteCode'].includes(key)) ||
2427
+ !OPERATION_CODE_PATTERN.test(routeCode) ||
2428
+ surfaceCodes.has(routeCode) ||
2429
+ routeCodes.has(routeCode) ||
2430
+ !validAppPath(routePath) ||
2431
+ /[:*?#\\\\]/.test(routePath) ||
2432
+ routePath !== expectedPath ||
2433
+ surfaceShapes.has(shape) ||
2434
+ /(?:^|\/)(?:auth|oauth)[^/]*callback(?:\/|$)/i.test(routePath);
2435
+ if (invalid) {
2436
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHENTICATION_SURFACE_INVALID', '登录 surface 必须使用独立静态 routeCode,desktop path=/login、mobile path=/m/login', pointer));
2437
+ }
2438
+ const generatedClaim = claimedShapes.get(shape);
2439
+ if (routePath && generatedClaim) {
2440
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHENTICATION_ROUTE_CONFLICT', `登录 surface path 与 ${generatedClaim.owner} 冲突: ${routePath}`, `${pointer}.path`));
2441
+ }
2442
+ if (!defaultRoute ||
2443
+ defaultRoute.surface !== 'user' ||
2444
+ (device === 'mobile'
2445
+ ? !string(defaultRoute.path).startsWith('/m/')
2446
+ : string(defaultRoute.path).startsWith('/m/')) ||
2447
+ canonicalFrontendRouteShape(string(defaultRoute.path)) === shape) {
2448
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHENTICATION_DEFAULT_ROUTE_INVALID', 'defaultRouteCode 必须引用同设备的受保护 user route,且不能指回登录 surface', `${pointer}.defaultRouteCode`));
2449
+ }
2450
+ surfaceCodes.add(routeCode);
2451
+ surfaceShapes.add(shape);
2452
+ }
2453
+ }
2300
2454
  const ADMIN_NAVIGATION_ICONS = new Set([
2301
2455
  'overview',
2302
2456
  'database',