UNPKG

everything-dev

Version:

A consolidated product package for building Module Federation apps with oRPC APIs.

162 lines (160 loc) • 6 kB
import { execa } from "execa"; import { platform } from "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 = platform(); try { if (os === "darwin") await execa("open", [url], { stdio: "ignore" }); else if (os === "win32") await execa("cmd", [ "/c", "start", "", url ], { stdio: "ignore", windowsVerbatimArguments: true }); else await execa("xdg-open", [url], { stdio: "ignore" }); } catch { throw new Error(`Failed to open browser. Open this URL manually:\n ${url}`); } } //#endregion export { openInBrowser, startDeviceLogin }; //# sourceMappingURL=auth-login.mjs.map