@lark-project/cli
Version:
飞书项目插件开发工具
214 lines (213 loc) • 9.01 kB
JavaScript
;
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;