openyida 2026.9.23 → 2026.9.24-beta.0

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
@@ -74,10 +74,17 @@ openyida login
74
74
 
75
75
  OpenYida login defaults to OAuth token mode. It opens the DingTalk OAuth authorization page, receives the local loopback callback, exchanges `code` / `authCode` with the Yida server, and stores `access_token` / `refresh_token` in the user auth store when available. If the user auth store is not writable, OpenYida explicitly falls back to the current project cache and reports that narrower persistence scope.
76
76
 
77
+ In a sandbox, headless shell, or CI dashboard where the loopback callback cannot complete, use the device authorization flow. Open the displayed verification URL on any device and enter the user code. `--env-hint` optionally pins the intended environment profile at authorization time:
78
+
79
+ ```bash
80
+ openyida login --device
81
+ openyida login --device --env-hint pre
82
+ ```
83
+
77
84
  When the user names a target Yida entry URL, pass it to the login command so OpenYida can select the matching environment and auth profile. For example:
78
85
 
79
86
  ```bash
80
- openyida login https://yida-group.alibaba-inc.com/
87
+ openyida login https://<your-yida-entry-url>/
81
88
  openyida login --alibaba
82
89
  ```
83
90
 
@@ -383,7 +390,7 @@ Run `openyida --help` or `openyida <command> --help` for detailed usage.
383
390
 
384
391
  | Command | Description |
385
392
  |---------|-------------|
386
- | `openyida login [target-url] [--env <name>\|--intl\|--overseas\|--global\|--yidaapps\|--alibaba] [--client-id <clientId>] [--endpoint <url>] [--no-browser]` | Login with OAuth token mode |
393
+ | `openyida login [target-url] [--env <name>\|--intl\|--overseas\|--global\|--yidaapps\|--alibaba] [--client-id <clientId>] [--endpoint <url>] [--no-browser] [--device] [--env-hint <name>]` | Login with OAuth token mode |
387
394
  | `openyida logout` | Logout / unbind current project auth |
388
395
  | `openyida auth <status\|login\|refresh\|logout\|profiles\|profile switch>` | Token login state and profile management |
389
396
  | `openyida org <list\|switch> [--json] [--corp-id <corpId>]` | Organization management (list / switch existing profiles first) |
package/bin/yida.js CHANGED
@@ -193,6 +193,7 @@ function printLoginResult(result) {
193
193
 
194
194
  if (!result) {
195
195
  console.log(JSON.stringify({
196
+ type: 'login_result',
196
197
  ok: false,
197
198
  status: 'login_failed',
198
199
  can_auto_use: false,
@@ -205,6 +206,7 @@ function printLoginResult(result) {
205
206
  ? !!result.ok
206
207
  : (result.can_auto_use !== false && !!result.access_token);
207
208
  const summary = {
209
+ type: 'login_result',
208
210
  ok: tokenUsable,
209
211
  status: result.status || (tokenUsable ? 'ok' : 'token_not_issued'),
210
212
  auth_mode: 'token',
@@ -279,6 +281,7 @@ function getFirstPositionalArg(cliArgs, startIndex = 0) {
279
281
  '--login-url',
280
282
  '--profile',
281
283
  '--user-id',
284
+ '--env-hint',
282
285
  ]);
283
286
  for (let index = startIndex; index < cliArgs.length; index++) {
284
287
  const arg = cliArgs[index];
@@ -386,6 +389,7 @@ function applyLoginEnvironmentFlags(cliArgs, options = {}) {
386
389
  '--client-id',
387
390
  '--profile',
388
391
  '--user-id',
392
+ '--env-hint',
389
393
  ]);
390
394
  const targetUrlFlags = new Set([
391
395
  '--endpoint',
@@ -537,13 +541,23 @@ function printLoginHelp() {
537
541
  }
538
542
 
539
543
  function buildTokenLoginOptions(loginArgs) {
544
+ const device = loginArgs.includes('--device');
540
545
  return {
541
546
  clientId: getArgValue(loginArgs, '--client-id'),
542
547
  corpId: getArgValue(loginArgs, '--corp-id'),
543
548
  userId: getArgValue(loginArgs, '--user-id'),
544
549
  authProfile: getArgValue(loginArgs, '--profile'),
545
550
  quiet: process.env.YIDA_QUIET === '1' || loginArgs.includes('--quiet'),
546
- noBrowser: loginArgs.includes('--no-browser'),
551
+ noBrowser: device || loginArgs.includes('--no-browser'),
552
+ device,
553
+ envHint: getArgValue(loginArgs, '--env-hint'),
554
+ // Machine-readable progress events for agent runtimes (sandbox browsers,
555
+ // CI dashboards). Emits one JSON line per state transition.
556
+ onDeviceState: (state) => {
557
+ if (device) {
558
+ process.stdout.write(JSON.stringify({ type: 'device_login_state', ...state }) + '\n');
559
+ }
560
+ },
547
561
  };
548
562
  }
549
563
 
@@ -0,0 +1,311 @@
1
+ /**
2
+ * oauth-device.js - Device Authorization Grant (RFC 8628) flow.
3
+ *
4
+ * Enables headless / sandbox login: the CLI requests a device code from the
5
+ * auth service, the user completes authorization on ANY device (phone,
6
+ * another browser, CI dashboard), and the CLI polls until tokens are issued.
7
+ *
8
+ * The /device/* endpoints use a unified camelCase contract (deviceCode,
9
+ * userCode, verificationUri, grantType, accessToken, ...) matching the
10
+ * tianshu CliAuthRpc device endpoints. Legacy CLI token endpoints
11
+ * (/dingtalk/token, /refresh, /status) keep their snake_case contract and are
12
+ * not handled here.
13
+ *
14
+ * Machine-facing state events (onState):
15
+ * every callback receives a plain object with a camelCase `state` field plus
16
+ * state-specific camelCase fields, so agent runtimes can parse one schema:
17
+ * { state: 'awaiting_verification', verificationUri, verificationUriComplete,
18
+ * userCode, expiresIn, interval }
19
+ * { state: 'pending', nextPollMs } (heartbeat, every poll cycle)
20
+ * { state: 'slow_down', intervalMs }
21
+ * { state: 'poll_retry', statusCode, body } (transient unrecognized error)
22
+ *
23
+ * Reuses the same token normalization / profile persistence pipeline as the
24
+ * loopback flow in oauth-loopback.js.
25
+ */
26
+
27
+ const { requestJson, getTokenPayload } = require('./token-auth');
28
+ const { redactString } = require('../core/redact');
29
+ const { t } = require('../core/i18n');
30
+
31
+ const DEVICE_CODE_PATH = '/device/code';
32
+ const DEVICE_TOKEN_PATH = '/device/token';
33
+ const DEFAULT_DEVICE_TIMEOUT_MS = 10 * 60 * 1000; // device codes are usually valid 10 minutes
34
+ const DEFAULT_POLL_INTERVAL_MS = 5 * 1000;
35
+ const SLOW_DOWN_EXTRA_DELAY_MS = 5 * 1000;
36
+
37
+ const DEVICE_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:device_code';
38
+
39
+ function createDeviceTimeoutError() {
40
+ const timeout = new Error(t('login.device_timeout'));
41
+ timeout.code = 'device_timeout';
42
+ return timeout;
43
+ }
44
+
45
+ function assertBeforeDeadline(deadline) {
46
+ if (Date.now() >= deadline) {
47
+ throw createDeviceTimeoutError();
48
+ }
49
+ }
50
+
51
+ function positiveNumber(value, fallback) {
52
+ const parsed = Number(value);
53
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
54
+ }
55
+
56
+ function errorDescription(payload) {
57
+ return payload.errorDescription || payload.error_description || payload.errorMsg
58
+ || payload.error_msg || payload.message || payload.raw || '';
59
+ }
60
+
61
+ /**
62
+ * Request a device code from the auth service.
63
+ *
64
+ * @param {object} options
65
+ * @param {string} options.authBaseUrl - e.g. https://<entry-host>/openapi/cli/v1/auth
66
+ * @param {string} options.clientId
67
+ * @param {string} [options.scope] - defaults handled by caller
68
+ * @param {string} [options.envHint] - environment name to pin the token audience
69
+ * @param {string} [options.timeoutMs] - overall flow timeout
70
+ * @returns {Promise<{deviceCode, userCode, verificationUri,
71
+ * verificationUriComplete, expiresIn, interval}>}
72
+ */
73
+ async function requestDeviceCode(options = {}) {
74
+ const { authBaseUrl, clientId } = options;
75
+ if (!authBaseUrl || !clientId) {
76
+ throw new Error(t('login.device_missing_params'));
77
+ }
78
+ const body = { clientId };
79
+ if (options.scope) { body.scope = options.scope; }
80
+ if (options.envHint) { body.envHint = options.envHint; }
81
+
82
+ const deadline = positiveNumber(
83
+ options.deadline,
84
+ Date.now() + positiveNumber(options.timeoutMs, DEFAULT_DEVICE_TIMEOUT_MS)
85
+ );
86
+ assertBeforeDeadline(deadline);
87
+ const remainingMs = Math.max(1, deadline - Date.now());
88
+ let response;
89
+ try {
90
+ response = await requestJson(
91
+ 'POST',
92
+ `${authBaseUrl}${DEVICE_CODE_PATH}`,
93
+ body,
94
+ {},
95
+ { timeoutMs: remainingMs }
96
+ );
97
+ } catch (error) {
98
+ if (error.code === 'request_timeout') {
99
+ throw createDeviceTimeoutError();
100
+ }
101
+ throw error;
102
+ }
103
+ const payload = getTokenPayload(response);
104
+ if (!payload.deviceCode || !payload.userCode || !payload.verificationUri) {
105
+ const message = payload.message || payload.errorDescription || payload.errorMsg
106
+ || t('login.device_invalid_code_response');
107
+ const error = new Error(message);
108
+ error.payload = payload;
109
+ throw error;
110
+ }
111
+ return payload;
112
+ }
113
+
114
+ /**
115
+ * Poll the token endpoint until the device flow completes.
116
+ *
117
+ * @param {object} options
118
+ * @param {string} options.authBaseUrl
119
+ * @param {string} options.clientId
120
+ * @param {string} options.deviceCode
121
+ * @param {number} [options.intervalMs] - from /device/code response
122
+ * @param {number} [options.timeoutMs] - overall wall clock budget
123
+ * @param {number} [options.deadline] - absolute wall clock deadline
124
+ * @param {(state: object) => void} [options.onState] - progress callback for agents
125
+ * @returns {Promise<object>} token payload (accessToken, refreshToken, ...)
126
+ */
127
+ async function pollDeviceToken(options = {}) {
128
+ const { authBaseUrl, clientId, deviceCode } = options;
129
+ if (!authBaseUrl || !clientId || !deviceCode) {
130
+ throw new Error(t('login.device_poll_missing_params'));
131
+ }
132
+ const intervalMs = Math.max(1, positiveNumber(options.intervalMs, DEFAULT_POLL_INTERVAL_MS));
133
+ const deadline = positiveNumber(
134
+ options.deadline,
135
+ Date.now() + positiveNumber(options.timeoutMs, DEFAULT_DEVICE_TIMEOUT_MS)
136
+ );
137
+ let currentInterval = intervalMs;
138
+
139
+ // First poll immediately, then honor the interval.
140
+ for (;;) {
141
+ assertBeforeDeadline(deadline);
142
+ const remainingMs = Math.max(1, deadline - Date.now());
143
+ let response;
144
+ try {
145
+ response = await requestJson('POST', `${authBaseUrl}${DEVICE_TOKEN_PATH}`, {
146
+ grantType: DEVICE_GRANT_TYPE,
147
+ deviceCode: deviceCode,
148
+ clientId: clientId,
149
+ }, {}, { timeoutMs: remainingMs });
150
+ } catch (error) {
151
+ if (error.code === 'request_timeout') {
152
+ throw createDeviceTimeoutError();
153
+ }
154
+ const payload = error.payload || {};
155
+ const errorCode = payload.error || payload.errorCode || payload.error_msg || payload.errorMsg;
156
+ if (errorCode === 'authorization_pending') {
157
+ await waitFor(deadline, currentInterval, options);
158
+ continue;
159
+ }
160
+ if (errorCode === 'slow_down') {
161
+ currentInterval += SLOW_DOWN_EXTRA_DELAY_MS;
162
+ if (options.onState) {
163
+ options.onState({ state: 'slow_down', intervalMs: currentInterval });
164
+ }
165
+ await waitFor(deadline, currentInterval, options);
166
+ continue;
167
+ }
168
+ if (errorCode === 'expired_token') {
169
+ const expired = new Error(t('login.device_code_expired'));
170
+ expired.code = 'device_code_expired';
171
+ throw expired;
172
+ }
173
+ if (errorCode === 'access_denied') {
174
+ const denied = new Error(t('login.device_access_denied'));
175
+ denied.code = 'device_access_denied';
176
+ throw denied;
177
+ }
178
+ if (errorCode) {
179
+ // RFC 8628 only permits polling to continue for
180
+ // authorization_pending and slow_down. Preserve the server's
181
+ // permanent OAuth error so callers can diagnose it immediately.
182
+ error.code = String(errorCode);
183
+ const description = errorDescription(payload);
184
+ if (description) { error.message = String(description); }
185
+ throw error;
186
+ }
187
+ if (error.statusCode === 400 || error.statusCode === 429) {
188
+ // Unstructured 400/429 bodies can be produced by gateways. Retry
189
+ // those transient shapes, while the OAuth errors above remain
190
+ // terminal unless the RFC explicitly permits continued polling.
191
+ if (options.onState) {
192
+ options.onState({
193
+ state: 'poll_retry',
194
+ statusCode: error.statusCode,
195
+ body: redactString(String(errorDescription(payload)).slice(0, 200)),
196
+ });
197
+ }
198
+ await waitFor(deadline, currentInterval, options);
199
+ continue;
200
+ }
201
+ throw error;
202
+ }
203
+
204
+ const payload = getTokenPayload(response);
205
+ if (!payload.accessToken && !payload.access_token) {
206
+ // 2xx without a token is unexpected; surface raw payload for diagnosis.
207
+ const invalid = new Error(t('login.device_2xx_no_token'));
208
+ invalid.payload = payload;
209
+ throw invalid;
210
+ }
211
+ return payload;
212
+ }
213
+ }
214
+
215
+ function waitFor(deadline, intervalMs, options) {
216
+ const remaining = deadline - Date.now();
217
+ if (remaining <= 0) {
218
+ throw createDeviceTimeoutError();
219
+ }
220
+ if (options && options.onState) {
221
+ options.onState({ state: 'pending', nextPollMs: Math.min(intervalMs, remaining) });
222
+ }
223
+ return new Promise((resolve) => setTimeout(resolve, Math.min(intervalMs, remaining)));
224
+ }
225
+
226
+ /**
227
+ * Run the full device code flow and return a token payload for
228
+ * normalizeTokenResponse(). Designed to be called from tokenLogin().
229
+ *
230
+ * @param {object} options - same option bag as tokenLogin plus:
231
+ * options.authBaseUrl resolved auth base (with /openapi/cli/v1/auth prefix)
232
+ * options.envHint environment name to pin audience
233
+ * options.quiet suppress human-oriented stderr
234
+ * options.onState optional machine-readable progress callback
235
+ */
236
+ async function runDeviceCodeFlow(options = {}) {
237
+ const flowDeadline = Date.now()
238
+ + positiveNumber(options.timeoutMs, DEFAULT_DEVICE_TIMEOUT_MS);
239
+ const code = await requestDeviceCode({
240
+ ...options,
241
+ deadline: flowDeadline,
242
+ });
243
+
244
+ // The server may return verification URIs as absolute paths (e.g.
245
+ // /openapi/cli/v1/auth/device/verify); resolve them against the business
246
+ // base URL so users and agent hosts get clickable links.
247
+ const resolveUrl = (value) => {
248
+ if (!value || /^https?:\/\//i.test(value)) {
249
+ return value;
250
+ }
251
+ try {
252
+ const origin = new URL(options.baseUrl || options.authBaseUrl).origin;
253
+ return new URL(value, origin).toString();
254
+ } catch {
255
+ return value;
256
+ }
257
+ };
258
+ const verificationUri = resolveUrl(code.verificationUri);
259
+ const verificationUriComplete = resolveUrl(code.verificationUriComplete);
260
+
261
+ const intro = [
262
+ '',
263
+ t('login.device_title'),
264
+ ` 1. ${t('login.device_open')} ${verificationUri}`,
265
+ ` 2. ${t('login.device_enter_code')} ${code.userCode}`,
266
+ '',
267
+ ];
268
+ if (verificationUriComplete) {
269
+ intro.push(` ${t('login.device_or_direct')} ${verificationUriComplete}`, '');
270
+ }
271
+ if (!options.quiet) {
272
+ process.stderr.write(intro.join('\n') + '\n');
273
+ }
274
+
275
+ if (options.onState) {
276
+ options.onState({
277
+ state: 'awaiting_verification',
278
+ verificationUri: verificationUri,
279
+ verificationUriComplete: verificationUriComplete,
280
+ userCode: code.userCode,
281
+ expiresIn: code.expiresIn,
282
+ interval: code.interval,
283
+ });
284
+ }
285
+
286
+ const tokenPayload = await pollDeviceToken({
287
+ authBaseUrl: options.authBaseUrl,
288
+ clientId: options.clientId,
289
+ deviceCode: code.deviceCode,
290
+ intervalMs: (code.interval || 5) * 1000,
291
+ deadline: Math.min(
292
+ flowDeadline,
293
+ Date.now() + positiveNumber(code.expiresIn, 600) * 1000
294
+ ),
295
+ onState: options.onState,
296
+ });
297
+
298
+ if (!options.quiet) {
299
+ process.stderr.write(t('login.device_completed') + '\n');
300
+ }
301
+ return tokenPayload;
302
+ }
303
+
304
+ module.exports = {
305
+ runDeviceCodeFlow,
306
+ requestDeviceCode,
307
+ pollDeviceToken,
308
+ DEVICE_CODE_PATH,
309
+ DEVICE_TOKEN_PATH,
310
+ DEVICE_GRANT_TYPE,
311
+ };
@@ -83,11 +83,27 @@ function resolveDingtalkLoginOrigin(options = {}) {
83
83
  return DINGTALK_LOGIN_ORIGIN;
84
84
  }
85
85
 
86
- function requestJson(method, url, body, headers = {}) {
86
+ function requestJson(method, url, body, headers = {}, options = {}) {
87
87
  return new Promise((resolve, reject) => {
88
88
  const parsedUrl = new URL(url);
89
89
  const data = body === undefined || body === null ? null : Buffer.from(JSON.stringify(body));
90
90
  const transport = parsedUrl.protocol === 'https:' ? https : http;
91
+ const socketTimeoutMs = positiveNumber(options.timeoutMs, 0);
92
+ let timeoutId = null;
93
+ let settled = false;
94
+ const finish = (callback, value) => {
95
+ if (settled) {
96
+ return;
97
+ }
98
+ settled = true;
99
+ if (timeoutId) {
100
+ clearTimeout(timeoutId);
101
+ }
102
+ callback(value);
103
+ };
104
+ const resolveOnce = value => finish(resolve, value);
105
+ const rejectOnce = error => finish(reject, error);
106
+
91
107
  const request = transport.request({
92
108
  method,
93
109
  protocol: parsedUrl.protocol,
@@ -105,6 +121,7 @@ function requestJson(method, url, body, headers = {}) {
105
121
  }, (response) => {
106
122
  const chunks = [];
107
123
  response.on('data', (chunk) => chunks.push(chunk));
124
+ response.on('error', rejectOnce);
108
125
  response.on('end', () => {
109
126
  const text = Buffer.concat(chunks).toString('utf8');
110
127
  let payload = null;
@@ -125,14 +142,26 @@ function requestJson(method, url, body, headers = {}) {
125
142
  error.payload = payload;
126
143
  error.headers = response.headers;
127
144
  error.location = location;
128
- reject(error);
145
+ rejectOnce(error);
129
146
  return;
130
147
  }
131
- resolve(payload || {});
148
+ resolveOnce(payload || {});
132
149
  });
133
150
  });
134
151
 
135
- request.on('error', reject);
152
+ if (socketTimeoutMs > 0) {
153
+ // Node's socket timeout is reset by network activity, so it cannot enforce
154
+ // the caller's wall-clock budget. Use an independent timer that also
155
+ // covers response bodies which trickle data without ever completing.
156
+ timeoutId = setTimeout(() => {
157
+ const error = new Error('request timed out');
158
+ error.code = 'request_timeout';
159
+ rejectOnce(error);
160
+ request.destroy(error);
161
+ }, socketTimeoutMs);
162
+ }
163
+
164
+ request.on('error', rejectOnce);
136
165
  if (data) {
137
166
  request.write(data);
138
167
  }
@@ -140,6 +169,11 @@ function requestJson(method, url, body, headers = {}) {
140
169
  });
141
170
  }
142
171
 
172
+ function positiveNumber(value, fallback) {
173
+ const parsed = Number(value);
174
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
175
+ }
176
+
143
177
  function sanitizeRawTokenResponse(response) {
144
178
  if (!response || typeof response !== 'object') {
145
179
  return response;
@@ -302,25 +336,43 @@ async function tokenLogin(options = {}) {
302
336
  const baseUrl = resolveTokenBaseUrl(options);
303
337
  const authBaseUrl = appendPath(baseUrl, DEFAULT_AUTH_PATH_PREFIX);
304
338
  const clientId = options.clientId || process.env.OPENYIDA_DINGTALK_CLIENT_ID || DINGTALK_OAUTH_CLIENT_ID;
305
- const callback = await runDingtalkLoopback({
306
- clientId,
307
- loginOrigin: resolveDingtalkLoginOrigin(options),
308
- scope: options.scope || process.env.OPENYIDA_DINGTALK_SCOPE || 'openid corpid',
309
- prompt: options.prompt,
310
- port: options.port,
311
- quiet: options.quiet,
312
- noBrowser: options.noBrowser,
313
- timeoutMs: options.timeoutMs,
314
- });
339
+ const scope = options.scope || process.env.OPENYIDA_DINGTALK_SCOPE || 'openid corpid';
340
+
341
+ let response;
342
+ if (options.device) {
343
+ // Device Authorization Grant (RFC 8628): sandbox / headless login.
344
+ const { runDeviceCodeFlow } = require('./oauth-device');
345
+ response = await runDeviceCodeFlow({
346
+ authBaseUrl,
347
+ clientId,
348
+ scope,
349
+ envHint: options.envHint,
350
+ quiet: options.quiet,
351
+ onState: options.onDeviceState,
352
+ timeoutMs: options.timeoutMs || env.OPENYIDA_OAUTH_TIMEOUT_MS,
353
+ });
354
+ requireOkResponse(response);
355
+ } else {
356
+ const callback = await runDingtalkLoopback({
357
+ clientId,
358
+ loginOrigin: resolveDingtalkLoginOrigin(options),
359
+ scope,
360
+ prompt: options.prompt,
361
+ port: options.port,
362
+ quiet: options.quiet,
363
+ noBrowser: options.noBrowser,
364
+ timeoutMs: options.timeoutMs,
365
+ });
315
366
 
316
- const response = await requestJson('POST', appendPath(authBaseUrl, '/dingtalk/token'), {
317
- code: callback.code,
318
- authCode: callback.authCode,
319
- redirectUri: callback.redirectUri,
320
- state: callback.state,
321
- clientId,
322
- });
323
- requireOkResponse(response);
367
+ response = await requestJson('POST', appendPath(authBaseUrl, '/dingtalk/token'), {
368
+ code: callback.code,
369
+ authCode: callback.authCode,
370
+ redirectUri: callback.redirectUri,
371
+ state: callback.state,
372
+ clientId,
373
+ });
374
+ requireOkResponse(response);
375
+ }
324
376
 
325
377
  const normalized = normalizeTokenResponse(response, baseUrl, clientId);
326
378
  if (!normalized.access_token) {
@@ -624,4 +676,5 @@ module.exports = {
624
676
  resolveTokenBaseUrl,
625
677
  isRefreshAuthRequired,
626
678
  createTokenRefreshFailedError,
679
+ getTokenPayload,
627
680
  };
@@ -1035,7 +1035,7 @@ const COMMAND_GROUPS = [
1035
1035
  id: 'auth',
1036
1036
  titleKey: 'help.group_auth',
1037
1037
  commands: [
1038
- command('login', ['login'], 'login [target-url] [--env <name>|--intl|--overseas|--global|--yidaapps|--alibaba] [--client-id <clientId>] [--endpoint <url>] [--no-browser]', 'help.cmd_login', {
1038
+ command('login', ['login'], 'login [target-url] [--env <name>|--intl|--overseas|--global|--yidaapps|--alibaba] [--client-id <clientId>] [--endpoint <url>] [--no-browser] [--device] [--env-hint <name>]', 'help.cmd_login', {
1039
1039
  requiresLogin: false,
1040
1040
  output: 'json',
1041
1041
  }),
@@ -382,8 +382,8 @@ Examples:
382
382
  save_permission_usage: 'Usage: openyida save-permission <appType> <formUuid> [--package-uuid <packageUuid>] [--data-permission <json>] [--action-permission <json>]',
383
383
  save_permission_example: "Example: openyida save-permission APP_XXX FORM-XXX --data-permission '{\"role\":\"DEFAULT\",\"dataRange\":\"SELF\"}'",
384
384
  exec_failed: '\n❌ Execution failed: {0}',
385
- login_usage: 'Usage: openyida login [entryUrl|--public|--alibaba|--intl] [--no-browser] [--check-only] [--json] [--client-id <clientId>]',
386
- login_example: 'Examples:\n openyida login # Automatically open the browser via OAuth loopback login\n openyida login --no-browser # Let the caller handle the authorization URL\n openyida login --check-only --json # Check token auth status only\n openyida login --intl # Login against the international environment\n OPENYIDA_NO_BROWSER=1 openyida login # Suppress auto-opening the browser with an environment variable\n openyida auth login # Login alias',
385
+ login_usage: 'Usage: openyida login [entryUrl|--public|--alibaba|--intl] [--no-browser] [--device] [--env-hint <name>] [--check-only] [--json] [--client-id <clientId>]',
386
+ login_example: 'Examples:\n openyida login # Automatically open the browser via OAuth loopback login\n openyida login --device # Use device authorization in a headless environment\n openyida login --device --env-hint pre # Pin device authorization to an environment profile\n openyida login --no-browser # Let the caller handle the authorization URL\n openyida login --check-only --json # Check token auth status only\n openyida login --intl # Login against the international environment\n OPENYIDA_NO_BROWSER=1 openyida login # Suppress auto-opening the browser with an environment variable\n openyida auth login # Login alias',
387
387
  login_unsupported_option: 'Removed legacy login option is no longer supported: {0}. Use token/OAuth login; env token mode reads only OPENYIDA_* tokens.',
388
388
  auth_usage: 'Usage: openyida auth <status|login|refresh|logout|profiles|profile switch>',
389
389
  auth_example: 'Examples:\n openyida auth status # View login status\n openyida auth profiles # List existing login profiles\n openyida auth profile switch <auth_profile> # Switch current project to an existing profile\n openyida auth login # Add a profile when the target does not exist\n openyida auth refresh # Refresh login session\n openyida auth logout # Unbind current project auth\n openyida auth logout --profile <auth_profile> # Delete a shared profile explicitly',
@@ -655,6 +655,18 @@ Examples:
655
655
  oauth_success_message: 'Sign-in is complete. Return to the previous window to continue.',
656
656
  oauth_closing: 'Closing this window automatically…',
657
657
  oauth_manual_close: 'This window could not close automatically. Close it manually, then return to the previous window to continue.',
658
+ device_title: 'Device code login:',
659
+ device_open: 'Open',
660
+ device_enter_code: 'Enter code:',
661
+ device_or_direct: 'Or open directly:',
662
+ device_completed: 'Device authorization completed.',
663
+ device_timeout: 'device authorization timed out; device code may still be pending',
664
+ device_missing_params: 'device code flow requires authBaseUrl and clientId',
665
+ device_poll_missing_params: 'device token polling requires authBaseUrl, clientId and deviceCode',
666
+ device_invalid_code_response: 'auth service did not return a usable device code',
667
+ device_code_expired: 'device code expired; run login --device again',
668
+ device_access_denied: 'authorization was denied on the verification page',
669
+ device_2xx_no_token: 'auth service returned 2xx without accessToken',
658
670
  oauth_failure_title: 'Sign-in not completed',
659
671
  oauth_invalid_state: 'The sign-in request could not be verified. Return to the terminal and try again.',
660
672
  oauth_missing_code: 'No authorization information was received. Return to the terminal and try again.',
@@ -382,8 +382,8 @@ openyida - 宜搭命令行工具
382
382
  save_permission_usage: '用法: openyida save-permission <appType> <formUuid> [--package-uuid <packageUuid>] [--data-permission <json>] [--action-permission <json>]',
383
383
  save_permission_example: "示例: openyida save-permission APP_XXX FORM-XXX --data-permission '{\"role\":\"DEFAULT\",\"dataRange\":\"SELF\"}'",
384
384
  exec_failed: '\n❌ 执行失败: {0}',
385
- login_usage: '用法: openyida login [entryUrl|--public|--alibaba|--intl] [--no-browser] [--check-only] [--json] [--client-id <clientId>]',
386
- login_example: '示例:\n openyida login # 通过 OAuth loopback 自动打开浏览器登录\n openyida login --no-browser # 不自动打开浏览器,由调用方接管授权链接\n openyida login --check-only --json # 只检查 token 登录态\n openyida login --intl # 使用国际站环境登录\n OPENYIDA_NO_BROWSER=1 openyida login # 通过环境变量抑制自动打开浏览器\n openyida auth login # 登录入口别名',
385
+ login_usage: '用法: openyida login [entryUrl|--public|--alibaba|--intl] [--no-browser] [--device] [--env-hint <name>] [--check-only] [--json] [--client-id <clientId>]',
386
+ login_example: '示例:\n openyida login # 通过 OAuth loopback 自动打开浏览器登录\n openyida login --device # 在无浏览器或沙箱环境使用设备授权登录\n openyida login --device --env-hint pre # 将设备授权限定到指定环境 profile\n openyida login --no-browser # 不自动打开浏览器,由调用方接管授权链接\n openyida login --check-only --json # 只检查 token 登录态\n openyida login --intl # 使用国际站环境登录\n OPENYIDA_NO_BROWSER=1 openyida login # 通过环境变量抑制自动打开浏览器\n openyida auth login # 登录入口别名',
387
387
  login_unsupported_option: '已删除的旧登录参数不再支持: {0}。请使用 token/OAuth 登录;env token 模式下只读取 OPENYIDA_* token。',
388
388
  first_run_title: ' 🤖 OpenYida - AI 问答模式已开启! ',
389
389
  first_run_welcome: ' {0}欢迎首次使用 OpenYida!{1} 以下是快速上手指南:',
@@ -626,6 +626,18 @@ openyida - 宜搭命令行工具
626
626
  oauth_success_message: '登录已完成,请返回刚才的窗口继续操作。',
627
627
  oauth_closing: '正在自动关闭此窗口…',
628
628
  oauth_manual_close: '此窗口未能自动关闭,请手动关闭后返回刚才的窗口继续操作。',
629
+ device_title: '设备码登录:',
630
+ device_open: '打开',
631
+ device_enter_code: '输入验证码:',
632
+ device_or_direct: '或直接打开:',
633
+ device_completed: '设备授权完成。',
634
+ device_timeout: '设备授权超时;设备码可能仍在等待验证',
635
+ device_missing_params: '设备码流程需要 authBaseUrl 和 clientId',
636
+ device_poll_missing_params: '设备令牌轮询需要 authBaseUrl、clientId 和 deviceCode',
637
+ device_invalid_code_response: '认证服务未返回可用的设备码',
638
+ device_code_expired: '设备码已过期;请重新执行 login --device',
639
+ device_access_denied: '授权已在验证页面被拒绝',
640
+ device_2xx_no_token: '认证服务返回 2xx 但未包含 accessToken',
629
641
  oauth_failure_title: '登录未完成',
630
642
  oauth_invalid_state: '登录验证信息无效,请返回终端重新发起登录。',
631
643
  oauth_missing_code: '未收到登录授权信息,请返回终端重新发起登录。',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openyida",
3
- "version": "2026.9.23",
3
+ "version": "2026.9.24-beta.0",
4
4
  "description": "OpenYida CLI - 宜搭低代码 AI 开发工具(安装即用,零配置)",
5
5
  "bin": {
6
6
  "openyida": "bin/yida.js",