@withkeystone/cli
Version:
Keystone CLI - Test automation for modern web apps
113 lines • 4.63 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DeviceFlowAuthenticator = void 0;
const chalk_1 = __importDefault(require("chalk"));
const ora_1 = __importDefault(require("ora"));
async function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
class DeviceFlowAuthenticator {
config;
constructor(config) {
this.config = config;
}
async authenticate() {
// Request device code
const deviceCodeData = await this.requestDeviceCode();
console.log('\nTo authenticate, visit:');
console.log(chalk_1.default.cyan(deviceCodeData.verification_uri));
console.log('\nAnd enter code: ' + chalk_1.default.yellow(deviceCodeData.user_code));
if (deviceCodeData.verification_uri_complete) {
console.log('\nOr visit this URL directly:');
console.log(chalk_1.default.cyan(deviceCodeData.verification_uri_complete));
}
// Poll for completion
const spinner = (0, ora_1.default)('Waiting for authentication...').start();
try {
const tokens = await this.pollForTokens(deviceCodeData.device_code, deviceCodeData.interval, deviceCodeData.expires_in);
spinner.succeed('Authentication successful!');
return tokens;
}
catch (error) {
spinner.fail('Authentication failed');
throw error;
}
}
async requestDeviceCode() {
const response = await fetch(`${this.config.apiUrl}/api/v1/cli/device/code`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
client_id: 'keystone-cli',
scope: 'tests:read tests:write runs:read runs:write'
})
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Failed to request device code: ${error}`);
}
return response.json();
}
async pollForTokens(deviceCode, interval, expiresIn) {
const maxAttempts = Math.floor(expiresIn / interval);
const startTime = Date.now();
for (let i = 0; i < maxAttempts; i++) {
// Wait for the specified interval
if (i > 0) {
await sleep(interval * 1000);
}
// Check if we've exceeded the expiry time
const elapsed = (Date.now() - startTime) / 1000;
if (elapsed > expiresIn) {
throw new Error('Device code expired');
}
try {
const response = await fetch(`${this.config.apiUrl}/api/v1/cli/device/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
device_code: deviceCode,
client_id: 'keystone-cli'
})
});
if (response.ok) {
const data = await response.json();
return {
access_token: data.access_token,
refresh_token: data.refresh_token,
expires_in: data.expires_in
};
}
// Check if it's an expected error
const errorData = await response.json();
if (errorData.error === 'authorization_pending') {
// Continue polling
continue;
}
else if (errorData.error === 'slow_down') {
// Increase polling interval
interval = interval * 1.5;
continue;
}
else {
// Unexpected error
throw new Error(errorData.error_description || errorData.error);
}
}
catch (error) {
// Network error or parsing error
if (error instanceof Error && error.message.includes('authorization_pending')) {
continue;
}
throw error;
}
}
throw new Error('Authentication timeout');
}
}
exports.DeviceFlowAuthenticator = DeviceFlowAuthenticator;
//# sourceMappingURL=DeviceFlowAuthenticator.js.map