openyida 2026.8.19 → 2026.8.25-1

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 (49) hide show
  1. package/README.md +1 -1
  2. package/bin/yida.js +3 -7
  3. package/lib/aggregate-table/aggregate-table.js +37 -14
  4. package/lib/aggregate-table/contract.js +234 -0
  5. package/lib/app-permission/app-permission.js +25 -6
  6. package/lib/auth/org.js +8 -8
  7. package/lib/auth/profile.js +6 -6
  8. package/lib/auth/token-auth.js +39 -28
  9. package/lib/auth/token-store.js +58 -34
  10. package/lib/connector/api.js +144 -36
  11. package/lib/connector/connector-create.js +4 -4
  12. package/lib/connector/connector-delete.js +7 -10
  13. package/lib/connector/connector-test.js +3 -2
  14. package/lib/connector/contract.js +98 -0
  15. package/lib/core/agent-capabilities.js +14 -102
  16. package/lib/core/command-manifest.js +1 -1
  17. package/lib/core/locales/en.js +57 -3
  18. package/lib/core/locales/zh.js +57 -3
  19. package/lib/core/query-data.js +21 -0
  20. package/lib/core/utils.js +15 -11
  21. package/lib/core/yida-client.js +19 -0
  22. package/lib/corp-manager/api.js +15 -3
  23. package/lib/corp-manager/corp-manager.js +34 -4
  24. package/lib/integration/connector-presets.js +2 -3
  25. package/lib/integration/integration-create.js +161 -80
  26. package/lib/integration/integration-process-builder.js +177 -48
  27. package/lib/integration/integration-spec-builder.js +171 -48
  28. package/lib/integration/integration-view-builder.js +26 -25
  29. package/lib/page-config/save-share-config.js +111 -10
  30. package/lib/permission/get-permission.js +15 -0
  31. package/lib/permission/matrix-service.js +56 -0
  32. package/lib/permission/save-permission.js +470 -68
  33. package/lib/process/configure-process.js +114 -71
  34. package/lib/process/create-process.js +4 -1
  35. package/lib/process/services/process-compiler.js +53 -28
  36. package/lib/report/append.js +18 -1
  37. package/lib/report/contract.js +158 -0
  38. package/lib/report/http.js +28 -5
  39. package/lib/report/index.js +23 -1
  40. package/package.json +5 -1
  41. package/scripts/i18n-baseline.json +45 -23
  42. package/scripts/validate-i18n.js +45 -15
  43. package/yida-skills/skills/yida-app-permission/SKILL.md +39 -47
  44. package/yida-skills/skills/yida-connector/SKILL.md +6 -6
  45. package/yida-skills/skills/yida-corp-manager/SKILL.md +45 -34
  46. package/yida-skills/skills/yida-data-management/SKILL.md +1 -0
  47. package/yida-skills/skills/yida-form-permission/SKILL.md +108 -77
  48. package/yida-skills/skills/yida-integration/references/integration-node-schemas.md +2 -2
  49. package/yida-skills/skills/yida-page-config/SKILL.md +54 -79
package/README.md CHANGED
@@ -461,7 +461,7 @@ Run `openyida --help` or `openyida <command> --help` for detailed usage.
461
461
  | `openyida connector list` | List HTTP connectors |
462
462
  | `openyida connector create "name" "domain" ...` | Create a connector |
463
463
  | `openyida connector detail <id>` | View connector details |
464
- | `openyida connector delete <id>` | Delete a connector |
464
+ | `openyida connector delete <id> [--force]` | Show manual deletion guidance (CLI does not delete) |
465
465
  | `openyida connector add-action --operations <file> --connector-id <id>` | Add an action |
466
466
  | `openyida connector list-actions <id>` | List actions |
467
467
  | `openyida connector delete-action <id> <operation-id>` | Delete an action |
package/bin/yida.js CHANGED
@@ -670,15 +670,13 @@ async function main() {
670
670
 
671
671
  case 'login': {
672
672
  const loginArgs = applyLoginEnvironmentFlags(args, { inferTargetUrl: true });
673
- const { getAuthStatus, isEnvAuthMode } = require('../lib/core/utils');
673
+ const { getAuthStatus } = require('../lib/core/utils');
674
674
  if (hasHelpFlag(loginArgs)) {
675
675
  printLoginHelp();
676
676
  } else {
677
677
  assertNoUnsupportedLegacyLoginFlags(rawArgs, loginArgs);
678
678
  if (loginArgs.includes('--check-only')) {
679
679
  console.log(JSON.stringify(getAuthStatus(buildTokenLoginOptions(loginArgs)), null, 2));
680
- } else if (isEnvAuthMode()) {
681
- printLoginResult(getAuthStatus(buildTokenLoginOptions(loginArgs)));
682
680
  } else {
683
681
  const { tokenLogin } = require('../lib/auth/token-auth');
684
682
  const result = await tokenLogin(buildTokenLoginOptions(loginArgs));
@@ -697,7 +695,7 @@ async function main() {
697
695
  case 'auth': {
698
696
  const subCommand = args[0];
699
697
  const authArgs = applyLoginEnvironmentFlags(args.slice(1), { inferTargetUrl: true });
700
- const { getAuthStatus, isEnvAuthMode } = require('../lib/core/utils');
698
+ const { getAuthStatus } = require('../lib/core/utils');
701
699
  const { tokenLogin, tokenLogout, tokenRefresh } = require('../lib/auth/token-auth');
702
700
  if (!subCommand || subCommand === '--help' || subCommand === '-h') {
703
701
  printAuthHelp();
@@ -720,9 +718,7 @@ async function main() {
720
718
  printLoginHelp();
721
719
  } else {
722
720
  assertNoUnsupportedLegacyLoginFlags(rawArgs.slice(1), authArgs);
723
- const result = isEnvAuthMode()
724
- ? getAuthStatus(buildTokenLoginOptions(authArgs))
725
- : await tokenLogin(buildTokenLoginOptions(authArgs));
721
+ const result = await tokenLogin(buildTokenLoginOptions(authArgs));
726
722
  printLoginResult(result);
727
723
  }
728
724
  } else if (subCommand === 'refresh') {
@@ -8,16 +8,13 @@ const { t } = require('../core/i18n');
8
8
  const { buildYidaTitleI18n, normalizeYidaLocale, resolveContentLocale } = require('../core/yida-i18n');
9
9
  const { parseOpenOption, withBrowserHandoff } = require('../core/browser-handoff');
10
10
  const { fetchFormPageList, resolveLocalizedText } = require('../app/form-navigation');
11
+ const {
12
+ DESIGN_KEYS,
13
+ assertAggregateDesignConfig,
14
+ assertAggregateDesignReadback,
15
+ } = require('./contract');
11
16
 
12
17
  const FORM_TYPE_VIRTUAL_VIEW = 'virtualView';
13
- const DESIGN_KEYS = [
14
- 'relationForms',
15
- 'relationships',
16
- 'aggregatedFields',
17
- 'auxFields',
18
- 'formulaFields',
19
- 'validators',
20
- ];
21
18
 
22
19
  function hasHelpFlag(args) {
23
20
  return (args || []).includes('--help') || (args || []).includes('-h');
@@ -133,7 +130,7 @@ async function checkVirtualViewFeature(authRef, appType) {
133
130
  }
134
131
 
135
132
  async function createEmptyVirtualView(authRef, appType, title) {
136
- return createYidaClient({ authRef }).postForm(
133
+ return createYidaClient({ authRef }).postFormOnce(
137
134
  `/dingtalk/web/${appType}/query/formdesign/saveFormSchemaInfo.json`,
138
135
  buildCreateEmptyPostData(title)
139
136
  );
@@ -157,7 +154,11 @@ async function postVirtualViewDesign(authRef, appType, formUuid, designInfo, act
157
154
  throw new Error(`Unknown aggregate table action: ${action}`);
158
155
  }
159
156
 
160
- return createYidaClient({ authRef }).postForm(
157
+ const client = createYidaClient({ authRef });
158
+ const post = action === 'save' || action === 'publish'
159
+ ? client.postFormOnce.bind(client)
160
+ : client.postForm.bind(client);
161
+ return post(
161
162
  `/alibaba/web/${appType}/query/virtualview/${endpoint}`,
162
163
  buildDesignPostData(formUuid, designInfo, gmtModified)
163
164
  );
@@ -318,6 +319,11 @@ async function runCreateEmpty(args) {
318
319
  const createResult = await createEmptyVirtualView(authRef, appType, name);
319
320
  const created = assertSuccess(createResult, t('aggregate_table.create_empty'));
320
321
  const formUuid = (created && created.formUuid) || created;
322
+ if ((typeof formUuid !== 'string' && typeof formUuid !== 'number') || String(formUuid).length === 0) {
323
+ const error = new Error('Aggregate table creation returned no recoverable identity.');
324
+ error.code = 'AGGREGATE_CREATE_IDENTITY_MISSING';
325
+ throw error;
326
+ }
321
327
  const designUrl = buildDesignUrl(authRef.baseUrl, appType, formUuid, { fromNew: true });
322
328
  const workbenchUrl = buildWorkbenchUrl(authRef.baseUrl, appType, formUuid);
323
329
 
@@ -401,6 +407,7 @@ async function runPreview(args) {
401
407
 
402
408
  const authRef = await createAuthRef();
403
409
  const designInfo = normalizeDesignConfig(readJsonInput(input), formUuid);
410
+ assertAggregateDesignConfig(designInfo, { mode: 'draft' });
404
411
  const result = await postVirtualViewDesign(authRef, appType, formUuid, designInfo, 'preview');
405
412
  const rows = assertSuccess(result, t('aggregate_table.preview'));
406
413
  const rowCount = Array.isArray(rows) ? rows.length : 0;
@@ -439,13 +446,28 @@ async function runSaveOrPublish(args, action) {
439
446
  }
440
447
 
441
448
  const authRef = await createAuthRef();
442
- const { designInfo, gmtModified } = await loadDesignForMutation(authRef, appType, formUuid, input, action);
443
- if (isPublish && designInfo.relationForms.length === 0) {
444
- throw new Error(t('aggregate_table.publish_requires_source'));
445
- }
449
+ const { designInfo, currentConfig, gmtModified } = await loadDesignForMutation(authRef, appType, formUuid, input, action);
450
+ assertAggregateDesignConfig(designInfo, { mode: isPublish ? 'publish' : 'draft' });
446
451
 
447
452
  const result = await postVirtualViewDesign(authRef, appType, formUuid, designInfo, action, gmtModified);
448
453
  const content = assertSuccess(result, isPublish ? t('aggregate_table.publish') : t('aggregate_table.save'));
454
+ const readbackResult = await getVirtualViewConfig(authRef, appType, formUuid);
455
+ const readback = assertSuccess(readbackResult, t('aggregate_table.inspect'));
456
+ assertAggregateDesignReadback(designInfo, readback);
457
+ const responseRevision = content && content.gmtModified;
458
+ const readbackRevision = readback && readback.gmtModified;
459
+ if ((responseRevision === undefined || responseRevision === null) &&
460
+ (readbackRevision === undefined || readbackRevision === null)) {
461
+ const error = new Error('AGGREGATE_WRITE_REVISION_MISSING');
462
+ error.code = 'AGGREGATE_WRITE_REVISION_MISSING';
463
+ throw error;
464
+ }
465
+ if ((responseRevision === undefined || responseRevision === null) &&
466
+ currentConfig && String(readbackRevision) === String(currentConfig.gmtModified)) {
467
+ const error = new Error('AGGREGATE_WRITE_REVISION_UNCHANGED');
468
+ error.code = 'AGGREGATE_WRITE_REVISION_UNCHANGED';
469
+ throw error;
470
+ }
449
471
  const designUrl = buildDesignUrl(authRef.baseUrl, appType, formUuid);
450
472
  const workbenchUrl = buildWorkbenchUrl(authRef.baseUrl, appType, formUuid);
451
473
 
@@ -465,6 +487,7 @@ async function runSaveOrPublish(args, action) {
465
487
  formUuid,
466
488
  gmtModified: content && content.gmtModified,
467
489
  response: content,
490
+ readbackVerified: true,
468
491
  designUrl,
469
492
  workbenchUrl,
470
493
  }, designUrl, { stage: `aggregate_table_${action}_success`, title: formUuid }, flags.openMode), null, 2));
@@ -0,0 +1,234 @@
1
+ 'use strict';
2
+
3
+ const { isDeepStrictEqual } = require('util');
4
+
5
+ const DESIGN_KEYS = [
6
+ 'relationForms',
7
+ 'relationships',
8
+ 'aggregatedFields',
9
+ 'auxFields',
10
+ 'formulaFields',
11
+ 'validators',
12
+ ];
13
+
14
+ // Mirrors the designer defaults read from configLimit. Only limits that map to
15
+ // persisted design arrays are enforced here.
16
+ const DEFAULT_LIMITS = Object.freeze({
17
+ relationForms: 10,
18
+ relationships: 10,
19
+ formulaFields: 10,
20
+ auxFields: 10,
21
+ });
22
+
23
+ function addError(errors, code, path) {
24
+ errors.push({ code, path });
25
+ }
26
+
27
+ function isNonEmptyValue(value) {
28
+ return value !== null && value !== undefined && String(value).length > 0;
29
+ }
30
+
31
+ function validateArrayShapes(config, errors) {
32
+ for (const key of DESIGN_KEYS) {
33
+ if (!Array.isArray(config && config[key])) {
34
+ addError(errors, 'AGGREGATE_DESIGN_ARRAY_REQUIRED', key);
35
+ }
36
+ }
37
+ }
38
+
39
+ function validateLimits(config, limits, errors) {
40
+ const limitCodes = {
41
+ relationForms: 'AGGREGATE_RELATION_FORMS_LIMIT',
42
+ relationships: 'AGGREGATE_RELATIONSHIPS_LIMIT',
43
+ formulaFields: 'AGGREGATE_FORMULA_FIELDS_LIMIT',
44
+ auxFields: 'AGGREGATE_AUX_FIELDS_LIMIT',
45
+ };
46
+
47
+ for (const [key, limit] of Object.entries(limits)) {
48
+ if (Array.isArray(config[key]) && config[key].length > limit) {
49
+ addError(errors, limitCodes[key] || 'AGGREGATE_DESIGN_LIMIT', key);
50
+ }
51
+ }
52
+ }
53
+
54
+ function validateEntries(config, errors) {
55
+ if (Array.isArray(config.relationForms)) {
56
+ config.relationForms.forEach((item, index) => {
57
+ if (!item || !isNonEmptyValue(item.formUuid)) {
58
+ addError(errors, 'AGGREGATE_RELATION_FORM_INVALID', `relationForms[${index}]`);
59
+ }
60
+ });
61
+ }
62
+
63
+ if (Array.isArray(config.relationships)) {
64
+ config.relationships.forEach((item, index) => {
65
+ const relationshipInfos = item && item.relationshipInfos;
66
+ if (!Array.isArray(relationshipInfos) || relationshipInfos.length === 0 ||
67
+ relationshipInfos.some((entry) => entry === null || entry === undefined)) {
68
+ addError(errors, 'AGGREGATE_RELATIONSHIP_INVALID', `relationships[${index}]`);
69
+ }
70
+ });
71
+ }
72
+
73
+ if (Array.isArray(config.aggregatedFields)) {
74
+ config.aggregatedFields.forEach((item, index) => {
75
+ if (!item || !isNonEmptyValue(item.name)) {
76
+ addError(errors, 'AGGREGATE_COLUMN_INVALID', `aggregatedFields[${index}]`);
77
+ }
78
+ });
79
+ }
80
+
81
+ if (Array.isArray(config.formulaFields)) {
82
+ config.formulaFields.forEach((item, index) => {
83
+ if (!item || !isNonEmptyValue(item.formula)) {
84
+ addError(errors, 'AGGREGATE_FORMULA_FIELD_INVALID', `formulaFields[${index}]`);
85
+ }
86
+ });
87
+ }
88
+
89
+ if (Array.isArray(config.validators)) {
90
+ config.validators.forEach((item, index) => {
91
+ if (!item || !isNonEmptyValue(item.formula)) {
92
+ addError(errors, 'AGGREGATE_VALIDATOR_INVALID', `validators[${index}]`);
93
+ }
94
+ });
95
+ }
96
+ }
97
+
98
+ function validatePublishCompleteness(config, errors) {
99
+ const required = [
100
+ ['relationForms', 'AGGREGATE_RELATION_FORMS_REQUIRED'],
101
+ ['relationships', 'AGGREGATE_RELATIONSHIPS_REQUIRED'],
102
+ ['aggregatedFields', 'AGGREGATE_COLUMNS_REQUIRED'],
103
+ ['formulaFields', 'AGGREGATE_FORMULA_FIELDS_REQUIRED'],
104
+ ];
105
+
106
+ for (const [key, code] of required) {
107
+ if (!Array.isArray(config[key]) || config[key].length === 0) {
108
+ addError(errors, code, key);
109
+ }
110
+ }
111
+ }
112
+
113
+ function validateAggregateDesignConfig(config, options = {}) {
114
+ const mode = options.mode || 'draft';
115
+ if (mode !== 'draft' && mode !== 'publish') {
116
+ throw new Error(`Unsupported aggregate contract mode: ${mode}`);
117
+ }
118
+
119
+ const safeConfig = config && typeof config === 'object' && !Array.isArray(config)
120
+ ? config
121
+ : {};
122
+ const limits = { ...DEFAULT_LIMITS, ...(options.limits || {}) };
123
+ const errors = [];
124
+
125
+ validateArrayShapes(safeConfig, errors);
126
+ validateLimits(safeConfig, limits, errors);
127
+ validateEntries(safeConfig, errors);
128
+ if (mode === 'publish') {
129
+ validatePublishCompleteness(safeConfig, errors);
130
+ }
131
+
132
+ return { valid: errors.length === 0, errors };
133
+ }
134
+
135
+ function assertAggregateDesignConfig(config, options = {}) {
136
+ const result = validateAggregateDesignConfig(config, options);
137
+ if (result.valid) {
138
+ return config;
139
+ }
140
+
141
+ const error = new Error(result.errors.map((item) => item.code).join(', '));
142
+ error.name = 'AggregateDesignContractError';
143
+ error.code = 'AGGREGATE_DESIGN_CONTRACT_INVALID';
144
+ error.details = result.errors;
145
+ throw error;
146
+ }
147
+
148
+ function projectAggregateDesignConfig(config) {
149
+ const safeConfig = config && typeof config === 'object' ? config : {};
150
+ const projected = {};
151
+ for (const key of DESIGN_KEYS) {
152
+ projected[key] = Array.isArray(safeConfig[key]) ? safeConfig[key] : [];
153
+ }
154
+ return projected;
155
+ }
156
+
157
+ function isI18nValue(value) {
158
+ return value && typeof value === 'object' && !Array.isArray(value) && (
159
+ value.type === 'i18n' ||
160
+ Object.prototype.hasOwnProperty.call(value, 'zh_CN') ||
161
+ Object.prototype.hasOwnProperty.call(value, 'en_US') ||
162
+ Object.prototype.hasOwnProperty.call(value, 'pureEn_US')
163
+ );
164
+ }
165
+
166
+ function canonicalizeI18nValue(value) {
167
+ const zhCN = value.zh_CN || value.value || '';
168
+ const enUS = value.en_US || value.pureEn_US || zhCN;
169
+ return {
170
+ type: value.type || 'i18n',
171
+ zh_CN: String(zhCN),
172
+ en_US: String(enUS),
173
+ };
174
+ }
175
+
176
+ function isEmptyFilter(value) {
177
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
178
+ return false;
179
+ }
180
+ const rules = value.rules;
181
+ return (rules === undefined || rules === null || (Array.isArray(rules) && rules.length === 0)) &&
182
+ !value.logicOperator;
183
+ }
184
+
185
+ function canonicalizeAggregateValue(value, key = '') {
186
+ if (isI18nValue(value)) {
187
+ return canonicalizeI18nValue(value);
188
+ }
189
+ if (key === 'filter' && isEmptyFilter(value)) {
190
+ return {};
191
+ }
192
+ if (Array.isArray(value)) {
193
+ return value.map((item) => canonicalizeAggregateValue(item));
194
+ }
195
+ if (!value || typeof value !== 'object') {
196
+ if (key === 'precision' && value !== undefined && value !== null && value !== '') {
197
+ return String(value);
198
+ }
199
+ return value;
200
+ }
201
+
202
+ const result = {};
203
+ for (const childKey of Object.keys(value)) {
204
+ const childValue = value[childKey];
205
+ if ((childKey === 'filterState' && (childValue === null || childValue === undefined)) ||
206
+ (childKey === 'parentId' && (childValue === '' || childValue === null || childValue === undefined))) {
207
+ continue;
208
+ }
209
+ result[childKey] = canonicalizeAggregateValue(childValue, childKey);
210
+ }
211
+ return result;
212
+ }
213
+
214
+ function assertAggregateDesignReadback(expected, actual) {
215
+ const expectedProjection = canonicalizeAggregateValue(projectAggregateDesignConfig(expected));
216
+ const actualProjection = canonicalizeAggregateValue(projectAggregateDesignConfig(actual));
217
+ if (isDeepStrictEqual(expectedProjection, actualProjection)) {
218
+ return actualProjection;
219
+ }
220
+
221
+ const error = new Error('AGGREGATE_DESIGN_READBACK_MISMATCH');
222
+ error.name = 'AggregateDesignReadbackError';
223
+ error.code = 'AGGREGATE_DESIGN_READBACK_MISMATCH';
224
+ throw error;
225
+ }
226
+
227
+ module.exports = {
228
+ DEFAULT_LIMITS,
229
+ DESIGN_KEYS,
230
+ assertAggregateDesignConfig,
231
+ assertAggregateDesignReadback,
232
+ projectAggregateDesignConfig,
233
+ validateAggregateDesignConfig,
234
+ };
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { createAuthRef, createYidaClient, isAuthRefReady } = require('../core/yida-client');
4
4
  const { CliError } = require('../core/cli-error');
5
+ const { t } = require('../core/i18n');
5
6
  const { searchUsers } = require('../corp-manager/api');
6
7
 
7
8
  const ROLE_CONFIGS = {
@@ -140,6 +141,12 @@ function unique(values) {
140
141
  return [...new Set((values || []).map(value => String(value).trim()).filter(Boolean))];
141
142
  }
142
143
 
144
+ function sameUserIds(left, right) {
145
+ const leftIds = unique(left).sort();
146
+ const rightIds = unique(right).sort();
147
+ return JSON.stringify(leftIds) === JSON.stringify(rightIds);
148
+ }
149
+
143
150
  function toPositiveInt(value, defaultValue) {
144
151
  const parsed = Number.parseInt(value || `${defaultValue}`, 10);
145
152
  if (!Number.isFinite(parsed) || parsed <= 0) {
@@ -310,11 +317,7 @@ async function updateRoleManagers(options = {}, authRef = getAuthRef()) {
310
317
  const action = options.action || 'set';
311
318
  const inputUserIds = unique(options.userIds || options.users || []);
312
319
 
313
- if (action === 'set') {
314
- return saveRoleManagers({ appType, roleType, userIds: inputUserIds }, authRef);
315
- }
316
-
317
- if (inputUserIds.length === 0) {
320
+ if (action !== 'set' && inputUserIds.length === 0) {
318
321
  throw new Error(`${action} 操作必须提供 --users`);
319
322
  }
320
323
 
@@ -323,7 +326,9 @@ async function updateRoleManagers(options = {}, authRef = getAuthRef()) {
323
326
  const previousUserIds = current.roles[roleKey].userIds;
324
327
  let nextUserIds;
325
328
 
326
- if (action === 'add') {
329
+ if (action === 'set') {
330
+ nextUserIds = inputUserIds;
331
+ } else if (action === 'add') {
327
332
  nextUserIds = unique(previousUserIds.concat(inputUserIds));
328
333
  } else if (action === 'remove') {
329
334
  const removeSet = new Set(inputUserIds);
@@ -387,9 +392,22 @@ async function runUpdate(action, positionals, options) {
387
392
  });
388
393
  const current = await getAppPermission(appType);
389
394
  const roleKey = ROLE_CONFIGS[saved.roleType].key;
395
+ if (!sameUserIds(saved.userIds, current.roles[roleKey].userIds)) {
396
+ throw new CliError(t('app_permission.verify_failed'), {
397
+ code: 'APP_PERMISSION_VERIFY_FAILED',
398
+ details: {
399
+ expected: saved.userIds,
400
+ actual: current.roles[roleKey].userIds,
401
+ },
402
+ });
403
+ }
390
404
 
391
405
  printJson({
392
406
  ...saved,
407
+ before: {
408
+ userIds: saved.previousUserIds,
409
+ },
410
+ after: current.roles[roleKey],
393
411
  currentRole: current.roles[roleKey],
394
412
  });
395
413
  }
@@ -420,6 +438,7 @@ module.exports = {
420
438
  USAGE,
421
439
  parseCliOptions,
422
440
  splitList,
441
+ sameUserIds,
423
442
  normalizeRole,
424
443
  normalizeText,
425
444
  normalizeMember,
package/lib/auth/org.js CHANGED
@@ -13,7 +13,7 @@ const {
13
13
  loadTokenSession,
14
14
  normalizeCorpName,
15
15
  resolveTokenSession,
16
- isHostInjectedTokenMode,
16
+ isEnvTokenAuthMode,
17
17
  saveAuthProfilePointer,
18
18
  saveProjectLegacyTokenSession,
19
19
  saveTokenSession,
@@ -70,15 +70,15 @@ function createProfileRequiredError(targetCorpId, resolution = {}) {
70
70
  return error;
71
71
  }
72
72
 
73
- function createHostInjectedSwitchError(targetCorpId, currentSession, resolution = {}) {
73
+ function createEnvTokenSwitchError(targetCorpId, currentSession, resolution = {}) {
74
74
  const actualCorpId = currentSession && currentSession.corp_id;
75
- const error = new Error(`当前为宿主注入登录态,不能通过 OAuth 切换组织:target=${targetCorpId}, actual=${actualCorpId || 'unknown'}`);
76
- error.code = 'ORG_SWITCH_HOST_INJECTED_MISMATCH';
77
- error.status = 'host_injected_token_mismatch';
75
+ const error = new Error(`当前为 env token 登录态,不能通过 OAuth 切换组织:target=${targetCorpId}, actual=${actualCorpId || 'unknown'}`);
76
+ error.code = 'ORG_SWITCH_ENV_TOKEN_MISMATCH';
77
+ error.status = 'env_token_mismatch';
78
78
  error.targetCorpId = targetCorpId;
79
79
  error.actualCorpId = actualCorpId;
80
80
  error.auth_source = resolution.auth_source || (currentSession && currentSession.auth_source) || 'env';
81
- error.auth_store = resolution.auth_store || (currentSession && currentSession.auth_store) || 'host_injected';
81
+ error.auth_store = resolution.auth_store || (currentSession && currentSession.auth_store) || 'env';
82
82
  error.can_auto_use = false;
83
83
  return error;
84
84
  }
@@ -220,8 +220,8 @@ async function switchOrganization(targetCorpId, options = {}) {
220
220
  if (targetResolution.status === 'profile_required') {
221
221
  throw createProfileRequiredError(targetCorpId, targetResolution);
222
222
  }
223
- if (isHostInjectedTokenMode(projectOptions.env)) {
224
- throw createHostInjectedSwitchError(targetCorpId, currentSession, targetResolution);
223
+ if (isEnvTokenAuthMode(projectOptions.env)) {
224
+ throw createEnvTokenSwitchError(targetCorpId, currentSession, targetResolution);
225
225
  }
226
226
 
227
227
  const loginOptions = {
@@ -2,7 +2,7 @@
2
2
 
3
3
  const { CliError } = require('../core/cli-error');
4
4
  const {
5
- isHostInjectedTokenMode,
5
+ isEnvTokenAuthMode,
6
6
  listUserAuthProfiles,
7
7
  loadAuthProfilePointer,
8
8
  loadTokenSession,
@@ -129,14 +129,14 @@ function resolveSwitchTarget(target, options = {}) {
129
129
  }
130
130
 
131
131
  function switchAuthProfile(target, options = {}) {
132
- if (isHostInjectedTokenMode(options.env || process.env)) {
132
+ if (isEnvTokenAuthMode(options.env || process.env)) {
133
133
  throw new CliError(
134
- 'host-injected token mode cannot switch local auth profiles',
134
+ 'env token mode cannot switch local auth profiles',
135
135
  {
136
- code: 'AUTH_PROFILE_SWITCH_HOST_INJECTED',
136
+ code: 'AUTH_PROFILE_SWITCH_ENV_TOKEN',
137
137
  details: {
138
- nextStep: 'Ask the host runtime to inject the target organization token instead of switching local profiles.',
139
- next_step: 'Ask the host runtime to inject the target organization token instead of switching local profiles.',
138
+ nextStep: 'Ask the runtime to inject the target organization token instead of switching local profiles.',
139
+ next_step: 'Ask the runtime to inject the target organization token instead of switching local profiles.',
140
140
  },
141
141
  }
142
142
  );
@@ -15,14 +15,13 @@ const {
15
15
  clearAllUserAuthProfiles,
16
16
  clearTokenSession,
17
17
  deleteUserAuthProfile,
18
- isHostInjectedTokenMode,
18
+ isEnvTokenAuthMode,
19
19
  listUserAuthProfiles,
20
20
  loadLocalTokenSession,
21
21
  loadTokenSession,
22
22
  loadUserProfileFile,
23
23
  maskToken,
24
24
  normalizeCorpName,
25
- normalizeTokenSession,
26
25
  resolveTokenSession,
27
26
  saveBusinessContext,
28
27
  saveTokenSession,
@@ -274,6 +273,32 @@ function isRefreshAuthRequired(value) {
274
273
  }
275
274
 
276
275
  async function tokenLogin(options = {}) {
276
+ const env = options.env || process.env;
277
+ if (isEnvTokenAuthMode(env)) {
278
+ const status = tokenStatus(options);
279
+ if (status && status.can_auto_use) {
280
+ return {
281
+ ...status,
282
+ ok: true,
283
+ status: 'ok',
284
+ can_auto_use: true,
285
+ message: 'env token bootstrap credentials are already available; OAuth login was skipped',
286
+ };
287
+ }
288
+ return {
289
+ ...status,
290
+ ok: false,
291
+ auth_mode: 'token',
292
+ auth_source: status && status.auth_source ? status.auth_source : 'env',
293
+ auth_store: status && status.auth_store ? status.auth_store : 'env',
294
+ status: status && status.status ? status.status : 'not_logged_in',
295
+ can_auto_use: false,
296
+ message: status && status.message
297
+ ? status.message
298
+ : 'env token bootstrap credential is missing. Ask the runtime to inject OPENYIDA_ACCESS_TOKEN or OPENYIDA_REFRESH_TOKEN.',
299
+ };
300
+ }
301
+
277
302
  const baseUrl = resolveTokenBaseUrl(options);
278
303
  const authBaseUrl = appendPath(baseUrl, DEFAULT_AUTH_PATH_PREFIX);
279
304
  const clientId = options.clientId || process.env.OPENYIDA_DINGTALK_CLIENT_ID || DINGTALK_OAUTH_CLIENT_ID;
@@ -315,21 +340,21 @@ function tokenStatus(options = {}) {
315
340
  const session = resolution.session;
316
341
  if (!session || (!session.access_token && !session.refresh_token)) {
317
342
  const env = options.env || process.env;
318
- const hostInjected = isHostInjectedTokenMode(env);
343
+ const envTokenMode = isEnvTokenAuthMode(env);
319
344
  const status = {
320
345
  ok: false,
321
346
  auth_mode: 'token',
322
- auth_source: resolution.auth_source || (hostInjected ? 'env' : undefined),
347
+ auth_source: resolution.auth_source || (envTokenMode ? 'env' : undefined),
323
348
  auth_store: resolution.auth_store,
324
349
  auth_profile: resolution.auth_profile,
325
350
  status: resolution.status === 'profile_required' ? 'profile_required' : 'not_logged_in',
326
351
  can_auto_use: false,
327
- failure_reason: hostInjected ? 'env_token_missing' : resolution.status,
328
- message: resolution.message || (hostInjected
329
- ? 'host-injected token is missing. Ask the host to inject OPENYIDA_ACCESS_TOKEN or OPENYIDA_REFRESH_TOKEN.'
352
+ failure_reason: envTokenMode ? 'env_token_missing' : resolution.status,
353
+ message: resolution.message || (envTokenMode
354
+ ? 'env token bootstrap credential is missing. Ask the runtime to inject OPENYIDA_ACCESS_TOKEN or OPENYIDA_REFRESH_TOKEN.'
330
355
  : undefined),
331
- next_step: resolution.next_step || (hostInjected
332
- ? 'Ask the host runtime to inject OPENYIDA_ACCESS_TOKEN or OPENYIDA_REFRESH_TOKEN; do not run OAuth login in host-injected token mode.'
356
+ next_step: resolution.next_step || (envTokenMode
357
+ ? 'Ask the runtime to inject OPENYIDA_ACCESS_TOKEN or OPENYIDA_REFRESH_TOKEN; stop until credentials are available.'
333
358
  : 'Run openyida login to add an auth profile.'),
334
359
  next_step_commands: resolution.next_step_commands,
335
360
  candidate_count: resolution.candidate_count,
@@ -338,7 +363,7 @@ function tokenStatus(options = {}) {
338
363
  persistence_scope: resolution.persistence_scope,
339
364
  warning: resolution.warning,
340
365
  };
341
- if (!hostInjected) {
366
+ if (!envTokenMode) {
342
367
  status.token_file = require('./token-store').getTokenFilePath(options);
343
368
  }
344
369
  return status;
@@ -353,7 +378,7 @@ function tokenStatus(options = {}) {
353
378
  persistence_scope: session.persistence_scope,
354
379
  user_auth_store_writable: session.user_auth_store_writable,
355
380
  warning: session.warning,
356
- status: 'refresh_required',
381
+ status: 'ok',
357
382
  can_auto_use: true,
358
383
  refresh_token: maskToken(session.refresh_token),
359
384
  base_url: session.base_url,
@@ -446,7 +471,7 @@ async function tokenRefresh(options = {}) {
446
471
  }
447
472
  }
448
473
  const env = options.env || process.env;
449
- if (isHostInjectedTokenMode(env) && session.auth_source === 'env') {
474
+ if (isEnvTokenAuthMode(env) && ['env', 'project_legacy'].includes(session.auth_source)) {
450
475
  env.OPENYIDA_ACCESS_TOKEN = normalized.access_token;
451
476
  if (normalized.refresh_token) {
452
477
  env.OPENYIDA_REFRESH_TOKEN = normalized.refresh_token;
@@ -454,10 +479,10 @@ async function tokenRefresh(options = {}) {
454
479
  if (normalized.expires_at) {
455
480
  env.OPENYIDA_ACCESS_TOKEN_EXPIRES_AT = String(normalized.expires_at);
456
481
  }
457
- return normalizeTokenSession({
482
+ return saveTokenSession({
458
483
  ...normalized,
459
484
  auth_source: 'env',
460
- });
485
+ }, options);
461
486
  }
462
487
  return saveTokenSession(normalized, options);
463
488
  }
@@ -496,20 +521,6 @@ function toLogoutProfileSummary(session = {}) {
496
521
  }
497
522
 
498
523
  async function tokenLogout(options = {}) {
499
- const env = options.env || process.env;
500
- if (isHostInjectedTokenMode(env)) {
501
- return {
502
- ok: true,
503
- auth_mode: 'token',
504
- status: 'host_injected_noop',
505
- can_auto_use: false,
506
- auth_source: 'env',
507
- auth_store: 'host_injected',
508
- persistence_scope: 'host',
509
- message: 'host-injected token mode does not write or delete local auth profiles',
510
- };
511
- }
512
-
513
524
  const authProfile = options.authProfile || options.profile;
514
525
  const deleteAllProfiles = options.allProfiles === true || options.all === true;
515
526
  if (deleteAllProfiles) {