@embeddable.com/sdk-core
Version:
Core Embeddable SDK module responsible for web-components bundling and publishing.
176 lines (152 loc) • 5.24 kB
text/typescript
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, "");
}
}