UNPKG

@lark-project/cli

Version:

飞书项目插件开发工具

214 lines (213 loc) 9.01 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.loginWithToken = exports.login = void 0; const os_1 = __importDefault(require("os")); const qrcode_1 = __importDefault(require("qrcode")); const request_1 = require("../../../api/request"); const auth_config_1 = require("../../utils/auth-config"); const logger_1 = require("../../../utils/logger"); const open_browser_1 = require("../../../utils/open-browser"); const GRANT_TYPE_DEVICE_CODE = 'urn:ietf:params:oauth:grant-type:device_code'; /** Generate a dynamic client name like the Go CLI: "user@hostname (os/arch)" */ function getClientName() { const username = os_1.default.userInfo().username || ''; const hostname = os_1.default.hostname() || ''; const platform = process.platform; const arch = process.arch; const identity = username && hostname ? `${username}@${hostname}` : username || hostname || 'lpm-cli'; return `${identity} (${platform}/${arch})`; } /** * Rewrite verification_uri_complete: rename user_code → usercode in query string. * Aligned with Go CLI behavior. */ function rewriteVerificationURI(uri) { try { const u = new URL(uri); const uc = u.searchParams.get('user_code'); if (uc) { u.searchParams.delete('user_code'); u.searchParams.set('usercode', uc); } return u.toString(); } catch (_a) { return uri; } } /** Step 1: Fetch authorization server metadata (developer endpoints) */ async function getAuthServerMetadata(host) { try { const data = await (0, request_1.request)(`${host}/.well-known/oauth-authorization-server/b/auth/developer`, { method: 'GET', timeout: 5000 }); if (data.registration_endpoint && data.device_authorization_endpoint && data.token_endpoint) { return data; } } catch (err) { logger_1.logger.error(`Failed to fetch auth server metadata: ${err.message}`); } // Fallback: use default developer endpoint paths return { registration_endpoint: `${host}/goapi/v5/app/developer/oauth/register`, device_authorization_endpoint: `${host}/goapi/v5/app/developer/oauth/device_code`, token_endpoint: `${host}/goapi/v5/app/developer/oauth/token`, }; } /** Step 2: Dynamic Client Registration */ async function registerClient(registrationEndpoint) { try { const data = await (0, request_1.request)(registrationEndpoint, { method: 'POST', data: { client_name: getClientName(), grant_types: [GRANT_TYPE_DEVICE_CODE, 'refresh_token'], token_endpoint_auth_method: 'none', }, headers: { 'Content-Type': 'application/json' }, }); if (!data.client_id) { throw new Error('Client registration failed: missing client_id in response'); } return data.client_id; } catch (err) { logger_1.logger.error(`Client registration failed: ${err.message}`); throw err; } } /** Step 3: Request device code */ async function requestDeviceCode(deviceAuthEndpoint, clientId) { try { const params = new URLSearchParams(); params.append('client_id', clientId); const data = await (0, request_1.request)(deviceAuthEndpoint, { method: 'POST', data: params, headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, }); if (!data.device_code) { throw new Error('Failed to obtain device code'); } return data; } catch (err) { logger_1.logger.error(`Device code request failed: ${err.message}`); throw err; } } /** Step 4: Poll token endpoint until approved, denied, or expired */ async function pollForToken(tokenEndpoint, clientId, deviceCode, intervalSeconds, expiresIn) { const deadline = Date.now() + expiresIn * 1000; let intervalMs = Math.max(intervalSeconds, 5) * 1000; while (Date.now() < deadline) { await new Promise(resolve => setTimeout(resolve, intervalMs)); const params = new URLSearchParams(); params.append('grant_type', GRANT_TYPE_DEVICE_CODE); params.append('client_id', clientId); params.append('device_code', deviceCode); try { const data = await (0, request_1.request)(tokenEndpoint, { method: 'POST', data: params, headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, validateStatus: () => true, }); if (data.access_token) { return data; } const error = data.error; if (error === 'authorization_pending') { process.stdout.write('.'); continue; } if (error === 'slow_down') { intervalMs += 5 * 1000; continue; } if (error === 'access_denied') { logger_1.logger.error('Authorization denied by user.'); throw new Error('Authorization denied by user.'); } if (error === 'expired_token') { logger_1.logger.error('Authorization code expired.'); throw new Error('Authorization code expired. Please run `lpm login` again.'); } logger_1.logger.error(`Unexpected error from token endpoint: ${error}`); throw new Error(`Unexpected error from token endpoint: ${error}`); } catch (err) { if (!err.response) throw err; logger_1.logger.error(`Token poll request failed: ${err.message}`); } } logger_1.logger.error('Timed out waiting for authorization.'); throw new Error('Timed out waiting for authorization. Please run `lpm login` again.'); } /** Display QR code in terminal */ async function displayQRCode(url) { try { const qr = await qrcode_1.default.toString(url, { type: 'terminal', small: true }); console.log(qr); } catch (_a) { logger_1.logger.debug('Failed to render QR code'); } } /** Full login flow (Developer OAuth Device Authorization Grant) */ async function login(host) { const normalizedHost = host.replace(/\/$/, ''); logger_1.logger.info('Fetching authorization server metadata...'); const metadata = await getAuthServerMetadata(normalizedHost); logger_1.logger.info('Registering CLI client...'); const clientId = await registerClient(metadata.registration_endpoint); logger_1.logger.info('Requesting device authorization...'); const deviceCodeResp = await requestDeviceCode(metadata.device_authorization_endpoint, clientId); const { user_code, verification_uri_complete, interval, expires_in } = deviceCodeResp; const displayURI = verification_uri_complete ? rewriteVerificationURI(verification_uri_complete) : ''; console.log(''); console.log('='.repeat(60)); console.log(` Authorization code: ${user_code}`); console.log(` Visit: ${displayURI || deviceCodeResp.verification_uri}`); console.log('='.repeat(60)); console.log(''); const qrURI = displayURI || deviceCodeResp.verification_uri; if (qrURI) { await displayQRCode(qrURI); } logger_1.logger.info('Opening browser... (if it does not open, visit the URL above manually)'); (0, open_browser_1.openBrowser)(displayURI || deviceCodeResp.verification_uri); logger_1.logger.info('Waiting for authorization'); const tokenResp = await pollForToken(metadata.token_endpoint, clientId, deviceCodeResp.device_code, interval, expires_in); console.log(''); const now = Math.floor(Date.now() / 1000); (0, auth_config_1.saveProfile)(normalizedHost, { accessToken: tokenResp.access_token, accessTokenExpiresAt: tokenResp.expires_in ? now + tokenResp.expires_in : undefined, refreshToken: tokenResp.refresh_token, refreshTokenExpiresAt: tokenResp.refresh_token_expires_in ? now + tokenResp.refresh_token_expires_in : undefined, clientId, tokenId: tokenResp.token_id, }); logger_1.logger.success(`Login successful! Credentials saved to ${(0, auth_config_1.resolveConfigPath)()}`); } exports.login = login; /** * Login by directly setting a permanent developer token, skipping the OAuth flow. */ async function loginWithToken(host, token) { const normalizedHost = host.replace(/\/+$/, ''); (0, auth_config_1.saveProfile)(normalizedHost, { developerToken: token, }); logger_1.logger.success(`Developer token saved to ${(0, auth_config_1.resolveConfigPath)()}`); logger_1.logger.info(`Server: ${normalizedHost}`); } exports.loginWithToken = loginWithToken;