everything-dev
Version:
A consolidated product package for building Module Federation apps with oRPC APIs.
163 lines (161 loc) • 6.09 kB
JavaScript
let execa = require("execa");
let node_os = require("node:os");
//#region src/auth-login.ts
const DEVICE_CODE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
const DEFAULT_CLIENT_ID = "bos-cli";
const DEFAULT_POLL_INTERVAL_SECONDS = 5;
const SLOW_DOWN_PENALTY_SECONDS = 5;
async function authFetch(siteUrl, path, body, cookiePair) {
const headers = {
"content-type": "application/json",
origin: new URL(siteUrl).origin
};
if (cookiePair) headers.cookie = cookiePair;
const response = await fetch(`${siteUrl.replace(/\/$/, "")}/api/auth${path}`, {
method: "POST",
headers,
body: JSON.stringify(body)
});
return {
status: response.status,
json: await response.json().catch(() => ({})),
setCookie: response.headers.get("set-cookie")
};
}
/**
* CLI login via the OAuth 2.0 Device Flow (RFC 8628) — the same flow the
* site's QR pairing uses. The CLI requests a device code, the user approves
* at `/login/device` in any browser (same machine or not), and the CLI polls
* the token endpoint until the grant completes.
*/
async function startDeviceLogin(opts) {
const siteUrl = opts.siteUrl.replace(/\/$/, "");
const clientId = DEFAULT_CLIENT_ID;
const codeResponse = await fetch(`${siteUrl}/api/auth/device/code`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ client_id: clientId })
});
if (!codeResponse.ok) throw new Error(`Device login could not start (${codeResponse.status}) — is ${siteUrl} running the platform auth plugin?`);
const code = await codeResponse.json();
const verificationUrl = buildVerificationUrl(siteUrl, code, opts);
let abortReason = null;
let rejectPending;
const pendingReject = new Promise((_, reject) => {
rejectPending = reject;
});
const waitForApproval = (timeoutMs = 6e5) => {
const deadline = Date.now() + timeoutMs;
let intervalSeconds = code.interval ?? DEFAULT_POLL_INTERVAL_SECONDS;
const poll = async () => {
if (abortReason) throw new Error(abortReason);
if (Date.now() > deadline) throw new Error("Login timed out — the device code expired before approval");
const { status, json } = await authFetch(siteUrl, "/device/token", {
grant_type: DEVICE_CODE_GRANT_TYPE,
device_code: code.device_code,
client_id: clientId
});
if (status === 200) {
const { access_token: sessionToken } = json;
return await completeLogin(siteUrl, sessionToken, opts);
}
const error = json.error;
if (error === "authorization_pending") {} else if (error === "slow_down") intervalSeconds += SLOW_DOWN_PENALTY_SECONDS;
else if (error === "expired_token") throw new Error("Login timed out — the device code expired before approval");
else if (error === "access_denied") throw new Error("Login request was denied on the approval page");
else throw new Error(json.error_description ?? `Login failed (${error ?? status})`);
await new Promise((resolve) => setTimeout(resolve, intervalSeconds * 1e3));
return poll();
};
return Promise.race([pendingReject, poll()]);
};
return {
userCode: code.user_code,
verificationUrl,
waitForApproval,
abort: (reason) => {
abortReason = reason ?? "Login aborted";
rejectPending(new Error(abortReason));
}
};
}
function buildVerificationUrl(siteUrl, code, opts) {
const base = code.verification_uri_complete ?? code.verification_uri ?? "/login/device";
const url = new URL(base, siteUrl);
const params = url.searchParams;
params.set("user_code", code.user_code);
if (opts.account) params.set("account", opts.account);
if (opts.device) params.set("device", opts.device);
if (opts.delegate) {
params.set("pubKey", opts.delegate.pubKey);
params.set("contract", opts.delegate.contract);
params.set("network", opts.delegate.network);
}
return url.toString();
}
async function completeLogin(siteUrl, sessionToken, opts) {
const claim = await authFetch(siteUrl, "/device-link/claim", {
token: sessionToken,
client_id: DEFAULT_CLIENT_ID
});
if (claim.status !== 200) {
const detail = claim.json.message;
throw new Error(`Approved session could not be claimed${detail ? `: ${detail}` : ` (${claim.status})`}`);
}
const cookiePair = claim.setCookie?.split(";")[0];
if (!cookiePair?.includes("=")) throw new Error("Session claim returned no session cookie");
const apiKey = await mintApiKey(siteUrl, cookiePair, opts);
return {
sessionToken,
accountId: await resolveNearAccount(siteUrl, cookiePair),
apiKey
};
}
async function mintApiKey(siteUrl, cookiePair, opts) {
const { status, json } = await authFetch(siteUrl, "/api-key/create", {
configId: "user-keys",
name: `bos login — ${opts.device ?? "cli"} — ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace("T", " ")}`,
...opts.expiresIn ? { expiresIn: opts.expiresIn } : {}
}, cookiePair);
if (status !== 200) {
const detail = json.message;
throw new Error(`CLI credential could not be created${detail ? `: ${detail}` : ` (${status})`}`);
}
const created = json;
if (!created.key) throw new Error("API key creation returned no key");
return {
key: created.key,
id: created.id
};
}
async function resolveNearAccount(siteUrl, cookiePair) {
try {
const response = await fetch(`${siteUrl.replace(/\/$/, "")}/api/auth/near/list-accounts`, { headers: { cookie: cookiePair } });
if (!response.ok) return null;
return (await response.json()).activeAccount?.accountId ?? null;
} catch {
return null;
}
}
async function openInBrowser(url) {
const os = (0, node_os.platform)();
try {
if (os === "darwin") await (0, execa.execa)("open", [url], { stdio: "ignore" });
else if (os === "win32") await (0, execa.execa)("cmd", [
"/c",
"start",
"",
url
], {
stdio: "ignore",
windowsVerbatimArguments: true
});
else await (0, execa.execa)("xdg-open", [url], { stdio: "ignore" });
} catch {
throw new Error(`Failed to open browser. Open this URL manually:\n ${url}`);
}
}
//#endregion
exports.openInBrowser = openInBrowser;
exports.startDeviceLogin = startDeviceLogin;
//# sourceMappingURL=auth-login.cjs.map