openxiangda 1.0.84 → 1.0.85

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.
package/README.md CHANGED
@@ -178,6 +178,8 @@ const affiliatedDepartmentExternalId = user?.affiliatedDepartment?.externalId
178
178
 
179
179
  后端业务逻辑优先声明为 App Function:源码放在 `src/functions/<functionCode>/index.ts`,资源 manifest 放在 `src/resources/functions/<functionCode>.json`。函数运行在 trusted_node 中,通过 `ctx.form`、`ctx.dataView`、`ctx.connector`、`ctx.notification`、`ctx.platform.api` 等受控 API 访问平台能力;自动化、流程和运行时接口都可以调用同一个 function。JS_CODE V2 仍兼容,但新逻辑建议写成 function,再由 `function_call` 节点、`sdk.function.invoke(code, { input })` 或 `/:appType/v1/functions/:code/invoke.json` 调用。当前 MVP 中直接运行时接口要求调用者具备应用自动化管理权限,自动化/流程内部调用走服务端受控上下文。
180
180
 
181
+ 应用登录能力通过 auth resource 和 runtime SDK 提供:登录配置放在 `src/resources/auth/<code>.json`,默认 React SPA 模板已包含 `/view/:appType/login`,自定义页面可使用 `createAuthClient({ appType, servicePrefix })` 或 `LoginPage` / `useAuth` from `openxiangda/runtime/react`。手机号验证码、CAS/SSO 或其他外部登录可以由 App Function provider 校验外部凭证,但 provider 只能返回 `phone` / `email` / `externalId` / `unionId` 等身份声明;平台后端按 auth resource 策略执行账号匹配、绑定、创建或拒绝,并由平台统一签发 token/cookie。默认注册策略是拒绝,开启自动注册或白名单注册前必须确认身份匹配键、默认角色、验证码 TTL/频率/失败次数、审计字段和错误文案策略。
182
+
181
183
  常用数据视图命令:
182
184
 
183
185
  ```bash
package/lib/cli.js CHANGED
@@ -3374,6 +3374,7 @@ function ensureResourceBuckets(bound) {
3374
3374
  bound.resources.roles = bound.resources.roles || {};
3375
3375
  bound.resources.connectors = bound.resources.connectors || {};
3376
3376
  bound.resources.dataViews = bound.resources.dataViews || {};
3377
+ bound.resources.authConfigs = bound.resources.authConfigs || {};
3377
3378
  bound.resources.notifications = bound.resources.notifications || {};
3378
3379
  bound.resources.notifications.templates = bound.resources.notifications.templates || {};
3379
3380
  bound.resources.notifications.typeConfigs = bound.resources.notifications.typeConfigs || {};
@@ -3584,6 +3585,14 @@ function saveDataViewResource(target, dataViewCode, dataViewId, extra = {}) {
3584
3585
  }, keys);
3585
3586
  }
3586
3587
 
3588
+ function saveAuthConfigResource(target, authConfigCode, configId, extra = {}) {
3589
+ const keys = ['configId', 'status'];
3590
+ saveStateResource(target, 'authConfigs', authConfigCode, {
3591
+ ...pickStateFields(extra, keys),
3592
+ configId,
3593
+ }, keys);
3594
+ }
3595
+
3587
3596
  function saveRoleResource(target, roleCode, roleId) {
3588
3597
  saveStateResource(target, 'roles', roleCode, { roleId }, ['roleId']);
3589
3598
  }
@@ -3691,6 +3700,7 @@ const RESOURCE_SPECS = [
3691
3700
  { key: 'automations', dir: 'automations', topFiles: ['automations.json'], pluralKeys: ['automations'] },
3692
3701
  { key: 'functions', dir: 'functions', topFiles: ['functions.json'], pluralKeys: ['functions'] },
3693
3702
  { key: 'dataViews', dir: 'data-views', topFiles: ['data-views.json'], pluralKeys: ['dataViews', 'data-views'] },
3703
+ { key: 'authConfigs', dir: 'auth', topFiles: ['auth.json'], pluralKeys: ['authConfigs', 'auth'] },
3694
3704
  {
3695
3705
  key: 'pagePermissionGroups',
3696
3706
  dir: path.join('permissions', 'page-groups'),
@@ -3890,6 +3900,7 @@ function generateResourceTypes(manifest, outputFile) {
3890
3900
  );
3891
3901
  const dataViewCodes = unique((manifest.dataViews || []).map(item => item.code).filter(Boolean));
3892
3902
  const functionCodes = unique((manifest.functions || []).map(item => item.code).filter(Boolean));
3903
+ const authConfigCodes = unique((manifest.authConfigs || []).map(item => item.code).filter(Boolean));
3893
3904
  const content = [
3894
3905
  '/* eslint-disable */',
3895
3906
  '// Generated by openxiangda resource typegen. Do not edit manually.',
@@ -3909,6 +3920,9 @@ function generateResourceTypes(manifest, outputFile) {
3909
3920
  `export const functionCodes = ${JSON.stringify(functionCodes, null, 2)} as const`,
3910
3921
  'export type FunctionCode = typeof functionCodes[number]',
3911
3922
  '',
3923
+ `export const authConfigCodes = ${JSON.stringify(authConfigCodes, null, 2)} as const`,
3924
+ 'export type AuthConfigCode = typeof authConfigCodes[number]',
3925
+ '',
3912
3926
  `export const runtimeMenus = ${JSON.stringify(menus, null, 2)} as const`,
3913
3927
  '',
3914
3928
  `export const pagePermissionGroups = ${JSON.stringify(pagePermissionGroups, null, 2)} as const`,
@@ -3923,6 +3937,7 @@ function generateResourceTypes(manifest, outputFile) {
3923
3937
  pagePermissionGroups: pagePermissionGroupCodes.length,
3924
3938
  dataViews: dataViewCodes.length,
3925
3939
  functions: functionCodes.length,
3940
+ authConfigs: authConfigCodes.length,
3926
3941
  };
3927
3942
  }
3928
3943
 
@@ -4001,6 +4016,32 @@ function validateResourceItem(kind, item, errors, warnings) {
4001
4016
  }
4002
4017
  validateDataViewPerformance(label, definition, errors, warnings, storageMode);
4003
4018
  }
4019
+ if (kind === 'authConfigs') {
4020
+ const config = item.configJson || item.config || item;
4021
+ if (!Array.isArray(config.methods)) {
4022
+ errors.push(`${label}: 缺少 methods`);
4023
+ } else {
4024
+ config.methods.forEach((method, index) => {
4025
+ if (!method?.type) errors.push(`${label}.methods[${index}]: 缺少 type`);
4026
+ if (
4027
+ method?.type === 'phone_code' &&
4028
+ method.enabled !== false &&
4029
+ !(
4030
+ method.provider?.functionCode ||
4031
+ method.providerFunctionCode ||
4032
+ config.providers?.phone_code?.functionCode ||
4033
+ config.providers?.phoneCode?.functionCode
4034
+ )
4035
+ ) {
4036
+ errors.push(`${label}.methods[${index}]: phone_code 缺少 provider.functionCode`);
4037
+ }
4038
+ });
4039
+ }
4040
+ const registrationMode = String(config.registration?.mode || 'reject');
4041
+ if (registrationMode !== 'reject') {
4042
+ warnings.push(`${label}: 已开启非默认注册策略 ${registrationMode},请确认身份匹配键、默认角色和审计字段`);
4043
+ }
4044
+ }
4004
4045
  if (kind === 'pagePermissionGroups' && !item.name) errors.push(`${label}: 缺少 name`);
4005
4046
  if (kind === 'formPermissionGroups') {
4006
4047
  if (!item.name) errors.push(`${label}: 缺少 name`);
@@ -4402,6 +4443,7 @@ async function buildResourcePlan(config, target, manifest) {
4402
4443
  addNotificationPlanActions(target, actions, manifest.notifications || [], existing);
4403
4444
  await addWorkflowPlanActions(config, target, actions, manifest.workflows, existing.workflows);
4404
4445
  addPlanActions(actions, 'function', manifest.functions, existing.functions, (item, current) => functionEquals(target, item, current));
4446
+ addPlanActions(actions, 'authConfig', manifest.authConfigs, existing.authConfigs, (item, current) => authConfigEquals(item, current));
4405
4447
  await addAutomationPlanActions(config, target, actions, manifest.automations, existing.automations);
4406
4448
  addPlanActions(actions, 'dataView', manifest.dataViews, existing.dataViews, (item, current) => dataViewEquals(target.bound, item, current));
4407
4449
  addPlanActions(actions, 'pagePermissionGroup', manifest.pagePermissionGroups, existing.pagePermissionGroups, (item, current) => pagePermissionGroupEquals(target.bound, item, current));
@@ -4437,6 +4479,7 @@ async function publishResourceManifest(config, target, manifest, options = {}) {
4437
4479
  await publishNotificationResources(config, target, manifest.notifications || [], result);
4438
4480
  await publishWorkflowResources(config, target, manifest.workflows || [], result);
4439
4481
  await publishFunctionResources(config, target, manifest.functions || [], result);
4482
+ await publishAuthConfigResources(config, target, manifest.authConfigs || [], result);
4440
4483
  await publishAutomationResources(config, target, manifest.automations || [], result);
4441
4484
  await publishDataViewResources(config, target, manifest.dataViews || [], result);
4442
4485
  await publishPagePermissionGroupResources(config, target, manifest.pagePermissionGroups || [], result);
@@ -4460,6 +4503,7 @@ async function fetchExistingResourceMaps(config, target, manifest) {
4460
4503
  notificationTypeConfigs: new Map(),
4461
4504
  workflows: new Map(),
4462
4505
  functions: new Map(),
4506
+ authConfigs: new Map(),
4463
4507
  automations: new Map(),
4464
4508
  dataViews: new Map(),
4465
4509
  pagePermissionGroups: new Map(),
@@ -4565,6 +4609,31 @@ async function fetchExistingResourceMaps(config, target, manifest) {
4565
4609
  });
4566
4610
  }
4567
4611
  }
4612
+ if ((manifest.authConfigs || []).length > 0) {
4613
+ const data = await requestWithAuth(
4614
+ config,
4615
+ target.profileName,
4616
+ apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/auth/configs`, {
4617
+ page: 1,
4618
+ pageSize: 1000,
4619
+ })
4620
+ );
4621
+ indexByCode(maps.authConfigs, normalizeItems(data), item => item.code || item.resourceCode);
4622
+ for (const item of manifest.authConfigs || []) {
4623
+ const code = item.code || item.resourceCode;
4624
+ const existing = maps.authConfigs.get(code);
4625
+ if (!existing) continue;
4626
+ const detail = await requestOptionalWithAuth(
4627
+ config,
4628
+ target.profileName,
4629
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/auth/configs/${encodeURIComponent(code)}`
4630
+ );
4631
+ maps.authConfigs.set(code, {
4632
+ ...existing,
4633
+ ...(detail || {}),
4634
+ });
4635
+ }
4636
+ }
4568
4637
  if ((manifest.dataViews || []).length > 0) {
4569
4638
  const data = await requestWithAuth(
4570
4639
  config,
@@ -5227,6 +5296,38 @@ async function publishFunctionResources(config, target, functions, result) {
5227
5296
  }
5228
5297
  }
5229
5298
 
5299
+ async function publishAuthConfigResources(config, target, authConfigs, result) {
5300
+ for (const authItem of authConfigs) {
5301
+ const code = authItem.code || authItem.resourceCode || 'default';
5302
+ const existing = await findExistingAuthConfig(config, target, code);
5303
+ const body = normalizeAuthConfigManifest(authItem);
5304
+ const data = existing
5305
+ ? await requestWithAuth(
5306
+ config,
5307
+ target.profileName,
5308
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/auth/configs/${encodeURIComponent(code)}`,
5309
+ { method: 'PUT', body }
5310
+ )
5311
+ : await requestWithAuth(
5312
+ config,
5313
+ target.profileName,
5314
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/auth/configs`,
5315
+ { method: 'POST', body }
5316
+ );
5317
+ if (data?.id) {
5318
+ saveAuthConfigResource(target, data.code || code, data.id, {
5319
+ status: data.status,
5320
+ });
5321
+ }
5322
+ result.published.push({
5323
+ kind: 'authConfig',
5324
+ code,
5325
+ action: existing ? 'update' : 'create',
5326
+ id: data?.id,
5327
+ });
5328
+ }
5329
+ }
5330
+
5230
5331
  async function publishDataViewResources(config, target, dataViews, result) {
5231
5332
  for (const dataViewItem of dataViews) {
5232
5333
  const existing = await findExistingDataView(config, target, dataViewItem.code);
@@ -5272,6 +5373,18 @@ async function findExistingFunction(config, target, code) {
5272
5373
  return null;
5273
5374
  }
5274
5375
 
5376
+ async function findExistingAuthConfig(config, target, code) {
5377
+ const stateId = target.bound.resources?.authConfigs?.[code]?.configId;
5378
+ const byCode = await requestOptionalWithAuth(
5379
+ config,
5380
+ target.profileName,
5381
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/auth/configs/${encodeURIComponent(code)}`
5382
+ );
5383
+ if (byCode?.id) return byCode;
5384
+ if (stateId) return { id: stateId, code };
5385
+ return null;
5386
+ }
5387
+
5275
5388
  async function findExistingDataView(config, target, code) {
5276
5389
  const stateId = target.bound.resources?.dataViews?.[code]?.dataViewId;
5277
5390
  const byCode = await requestOptionalWithAuth(
@@ -5422,6 +5535,7 @@ async function pruneResourceManifest(config, target, manifest, result) {
5422
5535
  await pruneWorkflows(config, target, desiredCodes(manifest.workflows), result);
5423
5536
  await pruneAutomations(config, target, desiredCodes(manifest.automations), result);
5424
5537
  await pruneFunctions(config, target, desiredCodes(manifest.functions), result);
5538
+ await pruneAuthConfigs(config, target, desiredCodes(manifest.authConfigs), result);
5425
5539
  await pruneDataViews(config, target, desiredCodes(manifest.dataViews), result);
5426
5540
  await prunePagePermissionGroups(
5427
5541
  config,
@@ -5617,6 +5731,29 @@ async function pruneFunctions(config, target, desired, result) {
5617
5731
  }
5618
5732
  }
5619
5733
 
5734
+ async function pruneAuthConfigs(config, target, desired, result) {
5735
+ const data = await requestWithAuth(
5736
+ config,
5737
+ target.profileName,
5738
+ apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/auth/configs`, {
5739
+ page: 1,
5740
+ pageSize: 1000,
5741
+ })
5742
+ );
5743
+ for (const item of normalizeItems(data)) {
5744
+ const code = item.code || item.resourceCode;
5745
+ if (!code || desired.has(code)) continue;
5746
+ await pruneOne(config, target, result, 'authConfig', code, item.id, async () => {
5747
+ await requestWithAuth(
5748
+ config,
5749
+ target.profileName,
5750
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/auth/configs/${encodeURIComponent(code)}`,
5751
+ { method: 'DELETE' }
5752
+ );
5753
+ });
5754
+ }
5755
+ }
5756
+
5620
5757
  async function prunePagePermissionGroups(config, target, desired, result) {
5621
5758
  const data = await requestWithAuth(
5622
5759
  config,
@@ -5692,6 +5829,7 @@ function removeStateResource(target, kind, code) {
5692
5829
  workflow: 'workflows',
5693
5830
  automation: 'automations',
5694
5831
  function: 'functions',
5832
+ authConfig: 'authConfigs',
5695
5833
  dataView: 'dataViews',
5696
5834
  pagePermissionGroup: 'pagePermissionGroups',
5697
5835
  formPermissionGroup: 'formPermissionGroups',
@@ -5890,6 +6028,26 @@ async function pullResources(config, target) {
5890
6028
  written.push(path.relative(process.cwd(), filePath));
5891
6029
  }
5892
6030
 
6031
+ const authConfigs = await requestWithAuth(
6032
+ config,
6033
+ target.profileName,
6034
+ apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/auth/configs`, {
6035
+ page: 1,
6036
+ pageSize: 1000,
6037
+ })
6038
+ );
6039
+ for (const item of normalizeItems(authConfigs)) {
6040
+ const code = item.code || item.resourceCode || item.id;
6041
+ const detail = await requestWithAuth(
6042
+ config,
6043
+ target.profileName,
6044
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/auth/configs/${encodeURIComponent(code)}`
6045
+ );
6046
+ const filePath = path.join(baseDir, 'auth', `${code}.json`);
6047
+ writeResourceJsonFile(filePath, toPulledAuthConfig(detail));
6048
+ written.push(path.relative(process.cwd(), filePath));
6049
+ }
6050
+
5893
6051
  const dataViews = await requestWithAuth(
5894
6052
  config,
5895
6053
  target.profileName,
@@ -6105,6 +6263,16 @@ function toPulledDataView(dataView, permissionGroups, lookups) {
6105
6263
  });
6106
6264
  }
6107
6265
 
6266
+ function toPulledAuthConfig(authConfig) {
6267
+ return stripUndefinedValues({
6268
+ code: authConfig.code || authConfig.resourceCode,
6269
+ name: authConfig.name,
6270
+ description: authConfig.description || '',
6271
+ status: authConfig.status || 'active',
6272
+ configJson: authConfig.configJson || authConfig.config || {},
6273
+ });
6274
+ }
6275
+
6108
6276
  function rewriteDataViewDefinitionForManifest(definition, lookups) {
6109
6277
  rewriteDataViewSourceForManifest(definition.base, lookups);
6110
6278
  for (const join of definition.joins || []) {
@@ -6285,6 +6453,40 @@ function normalizeDataViewManifest(bound, dataView) {
6285
6453
  });
6286
6454
  }
6287
6455
 
6456
+ function normalizeAuthConfigManifest(authConfig) {
6457
+ const code = authConfig.code || authConfig.resourceCode || 'default';
6458
+ const configJson = clonePlainJson(
6459
+ authConfig.configJson || authConfig.config || withoutAuthConfigManifestMeta(authConfig)
6460
+ );
6461
+ return stripUndefinedValues({
6462
+ code,
6463
+ name: authConfig.name || configJson.name || code,
6464
+ description:
6465
+ authConfig.description !== undefined
6466
+ ? authConfig.description
6467
+ : configJson.description || '',
6468
+ status: authConfig.status || configJson.status || 'active',
6469
+ configJson: {
6470
+ ...configJson,
6471
+ methods: Array.isArray(configJson.methods) ? configJson.methods : [],
6472
+ registration: configJson.registration || { mode: 'reject' },
6473
+ binding: configJson.binding || { mode: 'auto' },
6474
+ },
6475
+ });
6476
+ }
6477
+
6478
+ function withoutAuthConfigManifestMeta(authConfig) {
6479
+ const next = withoutResourceMeta(authConfig);
6480
+ delete next.code;
6481
+ delete next.resourceCode;
6482
+ delete next.name;
6483
+ delete next.description;
6484
+ delete next.status;
6485
+ delete next.config;
6486
+ delete next.configJson;
6487
+ return next;
6488
+ }
6489
+
6288
6490
  function withoutDataViewManifestMeta(dataView) {
6289
6491
  const next = withoutResourceMeta(dataView);
6290
6492
  delete next.permissionGroups;
@@ -6754,6 +6956,18 @@ function functionEquals(target, desired, existing) {
6754
6956
  );
6755
6957
  }
6756
6958
 
6959
+ function authConfigEquals(desired, existing) {
6960
+ if (!existing) return false;
6961
+ const expected = normalizeAuthConfigManifest(desired);
6962
+ return (
6963
+ String(existing.code || existing.resourceCode || '') === String(expected.code || '') &&
6964
+ String(existing.name || '') === String(expected.name || '') &&
6965
+ String(existing.description || '') === String(expected.description || '') &&
6966
+ String(existing.status || 'active') === String(expected.status || 'active') &&
6967
+ jsonEqualsForPlan(existing.configJson || {}, expected.configJson || {}, desired.__dir)
6968
+ );
6969
+ }
6970
+
6757
6971
  function dataViewEquals(bound, desired, existing) {
6758
6972
  if (!existing) return false;
6759
6973
  const expected = normalizeDataViewManifest(bound, desired);
@@ -28,6 +28,7 @@ OpenXiangda supports two workspace modes. Classic `sy-lowcode-app-workspace` pub
28
28
  | 审批流程 / workflow / 流程节点 / JS_CODE | `openxiangda-workflow-automation` | `openxiangda workflow validate / create / publish` |
29
29
  | 自动化 / 定时任务 / 提交触发 / cron | `openxiangda-workflow-automation` | `openxiangda automation validate / create / publish / enable` |
30
30
  | App Function / function_call / 可复用后端逻辑 | `openxiangda-workflow-automation` | declare `src/resources/functions/<code>.json` + `src/functions/<code>/index.ts` |
31
+ | 应用登录 / Auth SDK / 手机号验证码 / CAS / 钉钉免登 | `openxiangda-architecture-design` + `openxiangda-page` | design security gate first, then declare `src/resources/auth/<code>.json` and use `createAuthClient` / `LoginPage` |
31
32
  | 角色 / 权限组 / 字段权限 / 数据范围 / 公开访问 | `openxiangda-permission-settings` | `openxiangda permission ...` / `openxiangda settings ...` |
32
33
  | 看应用结构 / 快照 / 对比 / 诊断 / 排查 / 报错 | `openxiangda-inspect` | `openxiangda app snapshot <APP_XXX> --profile <name> --json` |
33
34
  | 登录 / 切换平台 / profile / token / whoami | `openxiangda-core` | `openxiangda env --profile <name>` / `openxiangda auth status` |
@@ -42,6 +43,7 @@ OpenXiangda supports two workspace modes. Classic `sy-lowcode-app-workspace` pub
42
43
  - ✅ Each profile (dev / prod / ...) has its own `appType` and resource IDs; never copy a `formUuid` / `pageId` / `workflowId` / `automationId` across profiles.
43
44
  - ✅ Run `openxiangda update check --json` at the start of substantial work; if `updateAvailable`, run `openxiangda update install` and `openxiangda skill install --force`.
44
45
  - ✅ For non-trivial app requirements, run the architecture design gate first: forms, fields, pages, permissions, data views, status/workflow, automation, notifications, and development tasks must be decided before coding.
46
+ - ✅ For app login/auth requirements, run the auth security gate before coding: enabled methods, registration policy, identity matching keys, provider boundary, default roles, rate limits, audit fields, and third-party ownership must be confirmed.
45
47
  - ✅ For form-entry UX, pick components in this order: OpenXiangda platform form components → `antd` / `antd-mobile` wrappers → custom component only when neither exists.
46
48
  - ✅ List pages, linked options, remote selectors, and report drill-downs must use paginated server-side queries with explicit searchable fields. Do not fetch a large page and filter in local state.
47
49
  - ✅ Treat workflow forms as an exception. Ordinary ticket/order/task/asset lifecycles use status fields, state machines, action logs, permissions, and automations; workflows are for real approval tasks.
@@ -142,6 +144,7 @@ When the user provides a root domain such as `https://yida.wisejob.cn/`, use it
142
144
  - For repeated read-only multi-form queries or predefined dashboard metrics, create a data view manifest in `src/resources/data-views/`. Use `storageMode: "materialized"` for refreshed lists/reports that tolerate delay; use `storageMode: "live"` for bounded real-time joins where source changes must appear immediately. Use row views plus `sdk.dataView.query` for joined lists/lookups; use `viewType: "aggregate"` plus `sdk.dataView.stats` for count/sum/avg/min/max statistics. Add `indexes` only for materialized views. Do not use data views for single-form CRUD, simple linkedForm selects, writes, write-back, unbounded realtime joins, or ad-hoc BI. Read `references/data-views.md` before designing one.
143
145
  - For external APIs, create a connector manifest in `src/resources/connectors/` and call it from pages through `sdk.connector`; never put third-party API keys in page source.
144
146
  - For reusable backend logic shared by pages, automations, and workflows, create an App Function in `src/resources/functions/<functionCode>.json` plus `src/functions/<functionCode>/index.ts`. Call it from pages with `sdk.function.invoke`, from automation/workflow graphs with `function_call`, or from the runtime endpoint when the caller has app automation management permission. Current App Function MVP exposes controlled helpers such as `ctx.resources`, `ctx.form`, `ctx.dataView`, `ctx.connector`, `ctx.notification`, and `ctx.platform.api`; it does not expose raw SQL or Redis.
147
+ - For application login, declare auth resources in `src/resources/auth/<code>.json` and publish them with `openxiangda resource publish`. App Function auth providers may validate external credentials and return only an identity assertion; they must never issue tokens, set cookies, or write platform user/binding tables directly. The platform auth service decides create/bind/reject and issues tokens.
145
148
  - Before scaffolding a real app, complex page, data management page, portal shell, status lifecycle, role governance, or automation, read `references/best-practices.md` and pick the architecture first. Prefer copying the relevant pattern from `examples/best-practices/` into `src/` instead of generating a single large page file.
146
149
  - Before publishing to another platform, verify the workspace is bound for that profile. Resource IDs from one profile must not be reused for another profile.
147
150
  - Use `openxiangda app snapshot <APP_XXX> --profile <name> --json` for diagnosis before changing an existing app.
@@ -181,7 +184,7 @@ Core CLI / state:
181
184
  - `references/architecture-design.md` — OpenXiangda architecture design gate, anti-pattern rejection, question gates, final design template, and black-box demo scenario.
182
185
  - `references/workspace-state.md` — `.openxiangda/state.json` shape and profile isolation rules.
183
186
  - `references/connector-resources.md` — `src/resources` manifests, connector schema, App Function boundary, and platform runtime calls.
184
- - `references/resource-manifest-cheatsheet.md` — 复制即用的 connector / data-view / notification / App Function / workflow / automation / JS_CODE / role / permission group / settings / menu manifest 骨架与运行时调用示例。在创建任何 `src/resources/` 资源前先看这份。
187
+ - `references/resource-manifest-cheatsheet.md` — 复制即用的 auth / connector / data-view / notification / App Function / workflow / automation / JS_CODE / role / permission group / settings / menu manifest 骨架与运行时调用示例。在创建任何 `src/resources/` 资源前先看这份。
185
188
  - `references/data-views.md` — `src/resources/data-views` materialized/live data view resources, DSL, permissions, refresh, CLI commands, and runtime SDK usage.
186
189
  - `references/notifications.md` — `src/resources/notifications`, notification templates/type bindings, and `sdk.notification` / `ctx.notification`.
187
190
  - `references/best-practices.md` — initialized examples for modular pages, state lifecycles, role governance, permission isolation, high-performance queries, portal shells, workflow boundaries, and automation patterns.
@@ -24,6 +24,7 @@ This design flow is for applications built on OpenXiangda. The output is not a g
24
24
  - status machines or workflows
25
25
  - automations, JS_CODE V2 nodes, and App Functions
26
26
  - notifications and connectors
27
+ - application login/auth methods, registration policy, identity matching, and provider boundaries
27
28
  - publish and acceptance steps
28
29
 
29
30
  The design is complete only when another agent can implement it without deciding major architecture tradeoffs.
@@ -72,6 +73,8 @@ Do not insult the person. Criticize the proposal and explain the technical conse
72
73
  | Materialized view for hard realtime data | Users see stale data | `storageMode: "live"` only for bounded realtime joins, or direct source queries/App Function |
73
74
  | Live view for unbounded heavy joins | Slow runtime queries | Materialized view with scheduled refresh and indexes |
74
75
  | JS_CODE for reusable business service | Logic gets duplicated in graph nodes | App Function for shared backend logic; JS_CODE only for node-local trigger logic |
76
+ | Auth provider issues tokens or writes users directly | Breaks platform account, binding, permission, and audit boundaries | Provider returns identity assertion only; platform creates/binds/rejects and issues token |
77
+ | Phone-code login auto-registers by default | Allows uncontrolled account creation from weak identity proof | Default registration is reject; enable auto-create/whitelist only after explicit confirmation |
75
78
  | Raw native form controls | Inconsistent with platform runtime and validation | OpenXiangda platform components first, Ant Design wrappers second |
76
79
  | Multiple form permission groups without stable local codes | CLI state and platform `resourceCode` cannot reliably distinguish groups on the same form | Give every group a unique `code`, for example `ticket_reporter_view` and `ticket_repairer_view` |
77
80
  | Assuming resource publish means workflow is active | Workflow resources may exist as drafts until explicitly published | Verify `workflow list` shows `isPublished: true`; run `workflow publish <workflowCode>` when needed |
@@ -153,6 +156,20 @@ Default: reusable calculations and validations become App Functions; one-off bac
153
156
  - Are templates reusable resources?
154
157
  - Are external credentials needed? If yes, use connectors/resources, never page source secrets.
155
158
 
159
+ ### Login And Auth
160
+
161
+ Ask these before generating any login/auth design:
162
+
163
+ - Which methods are enabled: password, DingTalk in-app free login, CAS/SSO, phone code, guest?
164
+ - What is the phone registration policy: reject, auto-create, bind existing only, whitelist, or manual approval?
165
+ - What are the identity match keys and priorities: `phone`, `email`, `externalId`, `unionId`, `jobNumber`, `username`?
166
+ - Which App Function provider validates each external credential, and what identity assertion fields must it return?
167
+ - Which default app roles/page groups/data scopes should new users receive, if registration is enabled?
168
+ - What are the security parameters: code TTL, send frequency, failed attempts, IP/device limits, audit fields, and failure message disclosure?
169
+ - Is CAS/DingTalk configured by the platform tenant, or delegated to an app-level provider function?
170
+
171
+ Default: registration is rejected; provider functions return identity assertions only; platform auth service owns account creation, binding, permission assignment, cookies, and tokens.
172
+
156
173
  ### UI Design
157
174
 
158
175
  Ask whether to use Product Design or another design skill for:
@@ -213,6 +230,14 @@ For simple CRUD/admin lists, design can proceed with the appropriate OpenXiangda
213
230
  - After publishing workflow resources, verify `workflow list` and record whether `isPublished` is true.
214
231
  - In React SPA and classic workspaces, JS_CODE/App Function TypeScript lives under `src/js-code-nodes`, `src/automations`, or `src/functions`; build with `pnpm build-js-code`.
215
232
 
233
+ ### Auth
234
+
235
+ - Auth resources use stable local codes under `src/resources/auth/`.
236
+ - Use `/view/:appType/login` or `LoginPage` from `openxiangda/runtime/react` for default React login.
237
+ - Use `createAuthClient({ appType, servicePrefix })` for custom React login pages.
238
+ - Phone-code providers are App Functions. They validate send/verify events and return identity assertions; they do not issue tokens or mutate platform users.
239
+ - New-user registration must be explicit, with default roles and identity conflict behavior documented.
240
+
216
241
  ## Final Document Template
217
242
 
218
243
  ```markdown
@@ -262,17 +287,26 @@ For simple CRUD/admin lists, design can proceed with the appropriate OpenXiangda
262
287
  - JS_CODE V2 nodes:
263
288
  - App Functions:
264
289
 
265
- ## 8. Notifications And Connectors
290
+ ## 8. Login And Auth
291
+
292
+ - Enabled methods:
293
+ - Registration policy:
294
+ - Identity matching keys:
295
+ - Provider functions:
296
+ - Default roles/permissions:
297
+ - Security parameters:
298
+
299
+ ## 9. Notifications And Connectors
266
300
 
267
301
  | Scenario | Trigger | Channel | Recipients | Template/resource | Notes |
268
302
  |---|---|---|---|---|---|
269
303
 
270
- ## 9. Development Task Table
304
+ ## 10. Development Task Table
271
305
 
272
306
  | Phase | Task | Resources/files | Validation | Publish step |
273
307
  |---|---|---|---|---|
274
308
 
275
- ## 10. Acceptance Criteria
309
+ ## 11. Acceptance Criteria
276
310
 
277
311
  - Functional checks:
278
312
  - Permission checks:
@@ -281,7 +315,7 @@ For simple CRUD/admin lists, design can proceed with the appropriate OpenXiangda
281
315
  - Notification/automation checks:
282
316
  - Workflow checks: real approval flows show `isPublished: true`.
283
317
 
284
- ## 11. Open Questions And Confirmed Assumptions
318
+ ## 12. Open Questions And Confirmed Assumptions
285
319
 
286
320
  - Confirmed:
287
321
  - Still open:
@@ -12,6 +12,7 @@ Common folders:
12
12
  - `automations`
13
13
  - `data-views`
14
14
  - `functions`
15
+ - `auth`
15
16
  - `permissions/page-groups`
16
17
  - `permissions/form-groups`
17
18
  - `settings/forms`
@@ -33,6 +34,8 @@ Data view manifests live under `src/resources/data-views/` and define read-only
33
34
 
34
35
  App Function manifests live under `src/resources/functions/`, with source in `src/functions/<functionCode>/index.ts`. Use them for reusable server-side logic that pages, automations, and workflows can share through `sdk.function.invoke` or `function_call` nodes. App Functions expose controlled runtime helpers such as `ctx.resources`, `ctx.form`, `ctx.dataView`, `ctx.connector`, `ctx.notification`, and `ctx.platform.api`; they do not expose raw SQL or Redis in the current MVP. Use JS_CODE V2 only for node-local workflow/automation scripts.
35
36
 
37
+ Auth manifests live under `src/resources/auth/`. Use them to enable app-level login methods and bind phone-code/CAS/custom providers to App Functions. Auth provider functions are called only by the platform auth flow. They validate external credentials and return identity assertions such as `phone`, `email`, `externalId`, or `unionId`; they must not issue tokens, set cookies, or mutate platform user/binding tables.
38
+
36
39
  ```json
37
40
  {
38
41
  "code": "crm",
@@ -14,6 +14,7 @@ Guidelines:
14
14
  - Use `sdk.function.invoke(code, { input })` for reusable backend business logic declared under `src/resources/functions/` and `src/functions/`. Do not implement multi-form orchestration, connector fan-out, notification orchestration, or permission-sensitive backend rules directly in a page component.
15
15
  - Use `sdk.connector.invoke`, `sdk.connector.call("connector.api")`, or `sdk.connector.download` for external services. The SDK calls the platform runtime connector endpoint; it must not call third-party domains directly.
16
16
  - Use `sdk.notification.sendByType` and `batchSendByType` for reusable business messages. Custom notification types must be declared in `src/resources/notifications/` and published with `openxiangda resource publish`.
17
+ - Use `createAuthClient` from `openxiangda/runtime` or `LoginPage` / `useAuth` from `openxiangda/runtime/react` for application login pages. Auth provider App Functions return identity assertions only; platform auth owns create/bind/reject/token decisions.
17
18
  - For the current user's department hierarchy, use `sdk.department.getCurrentUserParentDepartments()`; do not hardcode `GET /department/:id/parentDepartments` in page code.
18
19
  - Use `sdk.auth.logoutAndRedirect({ loginUrl })` for user logout when the page should return after login. It calls the platform logout endpoint, appends the current page URL as `callback`, and redirects to the login URL. Use `sdk.auth.logout()` only when the page wants to handle redirect itself.
19
20
  - Use `sdk.role.getMyRoles()`, `sdk.role.getCurrentRole()`, and `sdk.role.switchAppRole()` for current-user app role switching. Pass `roleId: ""` to switch back to all app roles.
@@ -102,6 +103,44 @@ const result = await sdk.function.invoke("reservation_reminder_summary", {
102
103
 
103
104
  Use App Functions when the logic must run server-side and be reusable by pages, automations, or workflows. Current runtime invocation requires the caller to have app automation management permission; ordinary page users should normally consume function-backed data through a permission-aware page or a narrower backend resource.
104
105
 
106
+ Application auth:
107
+
108
+ ```tsx
109
+ import { LoginPage, useAuth, useLoginMethods } from "openxiangda/runtime/react";
110
+
111
+ export function DefaultLogin() {
112
+ return <LoginPage />;
113
+ }
114
+
115
+ export function CustomPhoneLogin() {
116
+ const auth = useAuth();
117
+ const methods = useLoginMethods();
118
+
119
+ async function submit(phone: string, code: string, challengeId: string) {
120
+ await auth.phoneCodeLogin({ phone, code, challengeId });
121
+ }
122
+
123
+ return null;
124
+ }
125
+ ```
126
+
127
+ Standalone client:
128
+
129
+ ```ts
130
+ import { createAuthClient } from "openxiangda/runtime";
131
+
132
+ const auth = createAuthClient({ appType, servicePrefix: "/service" });
133
+ const methods = await auth.getMethods();
134
+ const sent = await auth.sendPhoneCode({ phone, purpose: "login" });
135
+ const result = await auth.phoneCodeLogin({
136
+ phone,
137
+ code,
138
+ challengeId: sent.challengeId,
139
+ });
140
+ ```
141
+
142
+ For phone-code auth, the App Function provider receives `event`, `appType`, `method`, `credential`, `requestId`, `request`, and `challenge`. It may return `{ ok, providerState }` for send and `{ ok, identity }` for verify. It must not return `token`, `accessToken`, `refreshToken`, cookies, or mutate platform account tables.
143
+
105
144
  Logout and current-user role switching:
106
145
 
107
146
  ```ts
@@ -9,6 +9,7 @@
9
9
  - **平台 ID 由 CLI 解析**:`openxiangda resource publish --profile <name>` 把 code 解析成当前 profile 的真实 ID 并写回 `.openxiangda/state.json`。
10
10
  - **永远不写密钥**:第三方 API key、token、secret、password、authorization、headers 等绝不出现在 `src/resources/`,平台管理员在后台配置。
11
11
  - **多 profile 不共享 ID**:`dev` / `prod` 各自维护一份资源 ID 映射,复制 manifest 即可复用,不要复制平台 ID。
12
+ - **Auth provider 只返回身份声明**:登录 provider App Function 不能发 token、写 cookie、直接改用户表或绑定表;平台按策略创建/绑定/拒绝。
12
13
 
13
14
  ## 命令
14
15
 
@@ -20,6 +21,86 @@ openxiangda resource publish --profile <name> --prune # 删除 manifest 未声
20
21
  openxiangda resource pull --profile <name> # 拉取平台资源回写到本地(用于初次同步)
21
22
  ```
22
23
 
24
+ ## 0. Auth — `src/resources/auth/<code>.json`
25
+
26
+ ```json
27
+ {
28
+ "code": "default",
29
+ "name": "Default App Login",
30
+ "status": "active",
31
+ "configJson": {
32
+ "methods": [
33
+ { "type": "password", "enabled": true, "label": "账号密码" },
34
+ { "type": "dingtalk", "enabled": true, "label": "钉钉免登" },
35
+ { "type": "sso", "enabled": true, "label": "CAS", "protocol": "cas" },
36
+ {
37
+ "type": "phone_code",
38
+ "enabled": true,
39
+ "label": "手机号验证码",
40
+ "ttlSeconds": 300,
41
+ "sendFrequencySeconds": 60,
42
+ "maxAttempts": 5,
43
+ "provider": { "functionCode": "auth_phone_code" }
44
+ }
45
+ ],
46
+ "registration": { "mode": "reject" },
47
+ "binding": { "mode": "auto" },
48
+ "matching": {
49
+ "keys": ["phone", "email", "externalId", "unionId", "jobNumber", "username"]
50
+ },
51
+ "defaultRoleCodes": []
52
+ }
53
+ }
54
+ ```
55
+
56
+ Provider function:
57
+
58
+ ```ts
59
+ export default async function authPhoneCodeProvider(ctx, input) {
60
+ if (input.event === "phone_code.send") {
61
+ return { ok: true, providerState: { nonce: "vendor-message-id" } };
62
+ }
63
+
64
+ if (input.event === "phone_code.verify") {
65
+ if (input.credential.code !== "123456") {
66
+ return { ok: false, message: "验证码错误" };
67
+ }
68
+ return {
69
+ ok: true,
70
+ identity: {
71
+ phone: input.credential.phone,
72
+ externalId: `phone:${input.credential.phone}`
73
+ }
74
+ };
75
+ }
76
+
77
+ return { ok: false, message: "不支持的认证事件" };
78
+ }
79
+ ```
80
+
81
+ React 默认页:
82
+
83
+ ```tsx
84
+ import { LoginPage } from "openxiangda/runtime/react";
85
+
86
+ export default function AppLogin() {
87
+ return <LoginPage />;
88
+ }
89
+ ```
90
+
91
+ 自定义登录页:
92
+
93
+ ```ts
94
+ import { createAuthClient } from "openxiangda/runtime";
95
+
96
+ const auth = createAuthClient({ appType, servicePrefix: "/service" });
97
+ const methods = await auth.getMethods();
98
+ const sent = await auth.sendPhoneCode({ phone, purpose: "login" });
99
+ await auth.phoneCodeLogin({ phone, code, challengeId: sent.challengeId });
100
+ ```
101
+
102
+ 设计前必须确认启用方式、注册策略、身份匹配键、provider 边界、默认权限、安全参数、第三方配置归属。
103
+
23
104
  ## 1. Connector — `src/resources/connectors/<code>.json`
24
105
 
25
106
  ```json
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openxiangda",
3
- "version": "1.0.84",
3
+ "version": "1.0.85",
4
4
  "description": "OpenXiangda CLI, workspace build tools, runtime SDK, and form components.",
5
5
  "private": false,
6
6
  "bin": {