UNPKG

@embeddable.com/sdk-core

Version:

Core Embeddable SDK module responsible for web-components bundling and publishing.

176 lines (152 loc) 5.24 kB
import * as fs from "node:fs/promises"; import axios from "axios"; import provideConfig from "./provideConfig"; import { initLogger, logError } from "./logger"; // @ts-ignore import reportErrorToRollbar from "./rollbar.mjs"; import { CREDENTIALS_DIR, CREDENTIALS_FILE } from "./credentials"; const oraP = import("ora"); const openP = import("open"); export default async () => { await initLogger("login"); const breadcrumbs: string[] = []; const ora = (await oraP).default; const authenticationSpinner = ora("Waiting for code verification...").start(); try { const open = (await openP).default; const config = await provideConfig(); breadcrumbs.push("provideConfig"); await resolveFiles(); breadcrumbs.push("resolveFiles"); const deviceCodePayload = { client_id: config.authClientId, audience: config.audienceUrl, scope: "openid offline_access", }; const deviceCodeResponse = await axios.post( `https://${config.authDomain}/oauth/device/code`, deviceCodePayload, ); ora( "Confirm this code on your browser: " + deviceCodeResponse.data["user_code"], ).info(); const tokenPayload = { grant_type: "urn:ietf:params:oauth:grant-type:device_code", device_code: deviceCodeResponse.data["device_code"], client_id: config.authClientId, }; await open(deviceCodeResponse.data["verification_uri_complete"]); let isTerminated = false; const signalHandler = () => { isTerminated = true; authenticationSpinner.fail("Authentication process interrupted"); process.exit(0); }; process.on("SIGTERM", signalHandler); process.on("SIGINT", signalHandler); /** * This is a recommended way to poll, since it take some time for a user to enter a `user_code` in a browser. * deviceCodeResponse.data['interval'] is a recommended/calculated polling interval specified in seconds. */ while (!isTerminated) { try { const tokenResponse = await axios.post( `https://${config.authDomain}/oauth/token`, tokenPayload, ); await fs.writeFile( CREDENTIALS_FILE, JSON.stringify(tokenResponse.data), ); authenticationSpinner.succeed( "You are successfully authenticated now!", ); break; } catch (e: any) { if (e.response.data?.error !== "authorization_pending") { authenticationSpinner.fail( "Authentication failed. Please try again.", ); process.exit(1); } await sleep(deviceCodeResponse.data["interval"] * 1000); } } // Clean up signal handlers process.off("SIGTERM", signalHandler); process.off("SIGINT", signalHandler); } catch (error: unknown) { authenticationSpinner.fail("Authentication failed. Please try again."); await logError({ command: "login", breadcrumbs, error }); await reportErrorToRollbar(error); console.log(error); process.exit(1); } }; const REFRESH_MARGIN_SECONDS = 30; const isAccessTokenExpired = (accessToken: string): boolean => { try { const [, payloadBase64] = accessToken.split("."); if (!payloadBase64) return true; const payload = JSON.parse( Buffer.from(payloadBase64, "base64url").toString(), ); const exp = payload.exp; if (typeof exp !== "number") return true; return Date.now() / 1000 + REFRESH_MARGIN_SECONDS >= exp; } catch { return true; } }; export async function getToken() { try { const rawCredentials = await fs.readFile(CREDENTIALS_FILE, "utf-8"); const credentials = JSON.parse(rawCredentials.toString()); const accessToken: string = credentials?.access_token ?? ""; if (!accessToken) return ""; if (!isAccessTokenExpired(accessToken)) return accessToken; if (!credentials?.refresh_token) return accessToken; try { const config = await provideConfig(); const refreshResponse = await axios.post( `https://${config.authDomain}/oauth/token`, { grant_type: "refresh_token", client_id: config.authClientId, refresh_token: credentials.refresh_token, }, ); const newCredentials = { ...credentials, ...refreshResponse.data }; await fs.writeFile(CREDENTIALS_FILE, JSON.stringify(newCredentials)); return newCredentials.access_token ?? ""; } catch { try { const latestRawCredentials = await fs.readFile(CREDENTIALS_FILE, "utf-8"); const latestCredentials = JSON.parse(latestRawCredentials); const latestAccessToken = latestCredentials?.access_token ?? ""; if (latestAccessToken && !isAccessTokenExpired(latestAccessToken)) { return latestAccessToken; } } catch {} return accessToken; } } catch (_e) { return ""; } } function sleep(ms: number) { return new Promise((res) => setTimeout(res, ms)); } export async function resolveFiles() { try { await fs.access(CREDENTIALS_DIR); } catch (_e) { await fs.mkdir(CREDENTIALS_DIR); } try { await fs.access(CREDENTIALS_FILE); } catch (e) { await fs.writeFile(CREDENTIALS_FILE, ""); } }