UNPKG

@controlplane/cli

Version:

Control Plane Corporation CLI

141 lines 6.96 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.deriveAuthBaseUrl = deriveAuthBaseUrl; exports.doHeadlessLogin = doHeadlessLogin; const axios_1 = require("axios"); const jwt_decode_1 = require("jwt-decode"); const logger_1 = require("../util/logger"); const time_1 = require("../util/time"); // ANCHOR - Constants // Default polling interval in seconds, used when the server does not suggest one const DEFAULT_POLL_INTERVAL_SECONDS = 3; // Number of consecutive poll failures tolerated before giving up const MAX_CONSECUTIVE_POLL_FAILURES = 3; // SECTION - Functions /** * Resolves the auth-service base URL for the headless login flow. * * @param {Discovery} discovery - The discovery document fetched from the API endpoint. * @param {string} endpoint - The API endpoint configured in the profile (e.g., https://api.cpln.io). * @returns {string} The auth-service base URL without a trailing slash. */ function deriveAuthBaseUrl(discovery, endpoint) { let res = discovery.endpoints.auth; if (!res) { return endpoint.replace('api.', 'auth.'); } return res; } /** * Creates a new CLI login session on the auth service. * * @param {string} authBaseUrl - The auth-service base URL. * @returns {Promise<CreateCliSessionResponse>} A promise that resolves to the created session details. */ async function createCliSession(authBaseUrl) { // Ask the auth service for a new login session const { data: session } = await axios_1.default.post(`${authBaseUrl}/api/cli/session`); return session; } /** * Polls the auth service until the login session completes, then returns the custom token. * * @param {string} authBaseUrl - The auth-service base URL. * @param {CreateCliSessionResponse} session - The CLI login session being polled. * @returns {Promise<string>} A promise that resolves to the Firebase custom token. */ async function pollForCustomToken(authBaseUrl, session) { var _a; const deadline = new Date(session.expires * 1000); // Convert the server-suggested interval (seconds) into milliseconds const intervalMillis = (session.interval || DEFAULT_POLL_INTERVAL_SECONDS) * 1000; // Track consecutive transient failures so a single network blip does not kill the flow let consecutiveFailures = 0; while (new Date() < deadline) { // Wait before each poll to respect the server-suggested interval await (0, time_1.sleep)(intervalMillis); let poll; try { // Poll the session using the CLI-only secret session id const response = await axios_1.default.get(`${authBaseUrl}/api/cli/session/${session.sessionId}`); poll = response.data; // A successful poll resets the transient failure counter consecutiveFailures = 0; } catch (e) { // The server signals an expired or unknown session with HTTP 410 if (axios_1.default.isAxiosError(e) && ((_a = e.response) === null || _a === void 0 ? void 0 : _a.status) === 410) { throw new Error('Login session expired. Run cpln login again.'); } // Tolerate transient errors (network blips) up to a limit consecutiveFailures += 1; logger_1.logger.debug(`Login poll failed (${consecutiveFailures}/${MAX_CONSECUTIVE_POLL_FAILURES})`, e); if (consecutiveFailures >= MAX_CONSECUTIVE_POLL_FAILURES) { const reason = e instanceof Error ? e.message : String(e); throw new Error(`Failed to poll the login session after ${MAX_CONSECUTIVE_POLL_FAILURES} attempts: ${reason}`); } continue; } // The session completed: the server returns the custom token exactly once if (poll.status === 'complete' && poll.customToken) { return poll.customToken; } } throw new Error('Timed out waiting for login.'); } /** * Exchanges a Firebase custom token for an ID token and refresh token. * * @param {Discovery} discovery - The discovery document containing the Firebase API key. * @param {string} customToken - The Firebase custom token minted by the auth service. * @returns {Promise<SignInWithCustomTokenResponse>} A promise that resolves to the Firebase tokens. */ async function exchangeCustomToken(discovery, customToken) { // Exchange the custom token for ID and refresh tokens via the Firebase identity toolkit const { data: tokens } = await axios_1.default.post(`https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=${discovery.firebase.apiKey}`, { token: customToken, returnSecureToken: true, }); return tokens; } /** * Performs the headless login flow: creates a login session, prints the login URL and * confirmation code, polls until the user completes sign-in in a browser, exchanges the * resulting custom token for Firebase tokens, and stores them on the profile. * * The profile's auth info is only written on success: a failed or abandoned login * (timeout, network outage, expired session) leaves any existing credentials intact. * * @param {Profile} profile - The profile to log in; its authInfo is updated in place. * @returns {Promise<void>} A promise that resolves when the profile holds fresh tokens. */ async function doHeadlessLogin(profile) { var _a; // Fetch the discovery document from the configured API endpoint const { data: discovery } = await axios_1.default.get(`${profile.request.endpoint}/discovery`); // Resolve the auth-service base URL from discovery or the configured endpoint const authBaseUrl = deriveAuthBaseUrl(discovery, profile.request.endpoint); // Create a new CLI login session on the auth service const session = await createCliSession(authBaseUrl); // Show the user how to complete sign-in from any browser console.log(`Open this URL in your browser to log in:\n\n ${session.loginUrl}\n`); console.log(`Your confirmation code: ${session.userCode}\n`); console.log('Waiting for login to complete... (code expires in ~10 minutes)'); // Poll until the user completes sign-in and the server releases the custom token const customToken = await pollForCustomToken(authBaseUrl, session); // Exchange the custom token for Firebase ID and refresh tokens const tokens = await exchangeCustomToken(discovery, customToken); // Decode the email claim from the ID token const claims = (0, jwt_decode_1.jwtDecode)(tokens.idToken); if (!claims.email) { logger_1.logger.debug('The ID token has no email claim; storing an empty email'); } // Store the fresh tokens and email on the profile profile.authInfo = { accessToken: tokens.idToken, refreshToken: tokens.refreshToken, email: (_a = claims.email) !== null && _a !== void 0 ? _a : '', }; } // !SECTION //# sourceMappingURL=headlessLogin.js.map