UNPKG

everything-dev

Version:

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

1 lines • 13.4 kB
{"version":3,"file":"auth-login.cjs","names":["platform"],"sources":["../src/auth-login.ts"],"sourcesContent":["import { platform } from \"node:os\";\nimport { execa } from \"execa\";\n\nconst DEVICE_CODE_GRANT_TYPE = \"urn:ietf:params:oauth:grant-type:device_code\";\nconst DEFAULT_CLIENT_ID = \"bos-cli\";\nconst DEFAULT_POLL_INTERVAL_SECONDS = 5;\nconst SLOW_DOWN_PENALTY_SECONDS = 5;\n\nexport interface DeviceLoginOptions {\n siteUrl: string;\n /** label for the approving device, shown on the approval page */\n device?: string;\n /** the NEAR account the CLI expects to authenticate as (approval-page copy only) */\n account?: string;\n /** delegate mode — the approval page adds this scoped function-call key to the user's account */\n delegate?: { pubKey: string; contract: string; network: string };\n /** seconds the minted API key stays valid (login mode) */\n expiresIn?: number;\n}\n\nexport interface DeviceApproval {\n sessionToken: string;\n /** the NEAR account signed in on the approving browser (login mode) */\n accountId: string | null;\n /** the minted CLI credential (login mode) */\n apiKey: { key: string; id: string } | null;\n}\n\nexport interface DeviceLoginHandle {\n userCode: string;\n verificationUrl: string;\n waitForApproval: (timeoutMs?: number) => Promise<DeviceApproval>;\n abort: (reason?: string) => void;\n}\n\ninterface DeviceCodeResponse {\n device_code: string;\n user_code: string;\n verification_uri?: string;\n verification_uri_complete?: string;\n expires_in?: number;\n interval?: number;\n}\n\ninterface DeviceTokenSuccess {\n access_token: string;\n token_type: \"Bearer\";\n}\n\ninterface DeviceTokenError {\n error:\n | \"authorization_pending\"\n | \"slow_down\"\n | \"expired_token\"\n | \"access_denied\"\n | \"invalid_grant\"\n | \"invalid_request\"\n | \"invalid_client\";\n error_description?: string;\n}\n\ninterface ApiKeyCreated {\n id: string;\n key: string;\n}\n\ninterface NearAccountsResponse {\n accounts: { accountId: string }[];\n activeAccount: { accountId: string } | null;\n}\n\nasync function authFetch(\n siteUrl: string,\n path: string,\n body: unknown,\n cookiePair?: string,\n): Promise<{ status: number; json: unknown; setCookie: string | null }> {\n // Better Auth's origin check demands an Origin on cookie-bearing\n // requests — present the site's own origin, exactly as a browser would.\n const origin = new URL(siteUrl).origin;\n const headers: Record<string, string> = { \"content-type\": \"application/json\", origin };\n if (cookiePair) headers.cookie = cookiePair;\n const response = await fetch(`${siteUrl.replace(/\\/$/, \"\")}/api/auth${path}`, {\n method: \"POST\",\n headers,\n body: JSON.stringify(body),\n });\n return {\n status: response.status,\n json: await response.json().catch(() => ({})),\n setCookie: response.headers.get(\"set-cookie\"),\n };\n}\n\n/**\n * CLI login via the OAuth 2.0 Device Flow (RFC 8628) — the same flow the\n * site's QR pairing uses. The CLI requests a device code, the user approves\n * at `/login/device` in any browser (same machine or not), and the CLI polls\n * the token endpoint until the grant completes.\n */\nexport async function startDeviceLogin(opts: DeviceLoginOptions): Promise<DeviceLoginHandle> {\n const siteUrl = opts.siteUrl.replace(/\\/$/, \"\");\n const clientId = DEFAULT_CLIENT_ID;\n\n const codeResponse = await fetch(`${siteUrl}/api/auth/device/code`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ client_id: clientId }),\n });\n if (!codeResponse.ok) {\n throw new Error(\n `Device login could not start (${codeResponse.status}) — is ${siteUrl} running the platform auth plugin?`,\n );\n }\n const code = (await codeResponse.json()) as DeviceCodeResponse;\n\n const verificationUrl = buildVerificationUrl(siteUrl, code, opts);\n\n let abortReason: string | null = null;\n let rejectPending!: (reason?: Error) => void;\n const pendingReject = new Promise<never>((_, reject) => {\n rejectPending = reject;\n });\n\n const waitForApproval = (timeoutMs = 10 * 60_000): Promise<DeviceApproval> => {\n const deadline = Date.now() + timeoutMs;\n let intervalSeconds = code.interval ?? DEFAULT_POLL_INTERVAL_SECONDS;\n\n const poll = async (): Promise<DeviceApproval> => {\n if (abortReason) throw new Error(abortReason);\n if (Date.now() > deadline) {\n throw new Error(\"Login timed out — the device code expired before approval\");\n }\n\n const { status, json } = await authFetch(siteUrl, \"/device/token\", {\n grant_type: DEVICE_CODE_GRANT_TYPE,\n device_code: code.device_code,\n client_id: clientId,\n });\n\n if (status === 200) {\n const { access_token: sessionToken } = json as DeviceTokenSuccess;\n return await completeLogin(siteUrl, sessionToken, opts);\n }\n\n const error = (json as DeviceTokenError).error;\n if (error === \"authorization_pending\") {\n // keep polling\n } else if (error === \"slow_down\") {\n intervalSeconds += SLOW_DOWN_PENALTY_SECONDS;\n } else if (error === \"expired_token\") {\n throw new Error(\"Login timed out — the device code expired before approval\");\n } else if (error === \"access_denied\") {\n throw new Error(\"Login request was denied on the approval page\");\n } else {\n throw new Error(\n (json as DeviceTokenError).error_description ?? `Login failed (${error ?? status})`,\n );\n }\n\n await new Promise((resolve) => setTimeout(resolve, intervalSeconds * 1000));\n return poll();\n };\n\n return Promise.race([pendingReject, poll()]);\n };\n\n return {\n userCode: code.user_code,\n verificationUrl,\n waitForApproval,\n abort: (reason) => {\n abortReason = reason ?? \"Login aborted\";\n rejectPending(new Error(abortReason));\n },\n };\n}\n\nfunction buildVerificationUrl(\n siteUrl: string,\n code: DeviceCodeResponse,\n opts: DeviceLoginOptions,\n): string {\n const base = code.verification_uri_complete ?? code.verification_uri ?? \"/login/device\";\n const url = new URL(base, siteUrl);\n const params = url.searchParams;\n params.set(\"user_code\", code.user_code);\n if (opts.account) params.set(\"account\", opts.account);\n if (opts.device) params.set(\"device\", opts.device);\n if (opts.delegate) {\n params.set(\"pubKey\", opts.delegate.pubKey);\n params.set(\"contract\", opts.delegate.contract);\n params.set(\"network\", opts.delegate.network);\n }\n return url.toString();\n}\n\nasync function completeLogin(\n siteUrl: string,\n sessionToken: string,\n opts: DeviceLoginOptions,\n): Promise<DeviceApproval> {\n // The token endpoint returns the raw session token; Better Auth session\n // cookies carry an HMAC signature, so exchange the token for a signed\n // session cookie via the device-link claim endpoint (the QR pairing path).\n const claim = await authFetch(siteUrl, \"/device-link/claim\", {\n token: sessionToken,\n client_id: DEFAULT_CLIENT_ID,\n });\n if (claim.status !== 200) {\n const detail = (claim.json as { message?: string }).message;\n throw new Error(\n `Approved session could not be claimed${detail ? `: ${detail}` : ` (${claim.status})`}`,\n );\n }\n const cookiePair = claim.setCookie?.split(\";\")[0];\n if (!cookiePair?.includes(\"=\")) {\n throw new Error(\"Session claim returned no session cookie\");\n }\n\n const apiKey = await mintApiKey(siteUrl, cookiePair, opts);\n const accountId = await resolveNearAccount(siteUrl, cookiePair);\n return { sessionToken, accountId, apiKey };\n}\n\nasync function mintApiKey(\n siteUrl: string,\n cookiePair: string,\n opts: DeviceLoginOptions,\n): Promise<DeviceApproval[\"apiKey\"]> {\n const { status, json } = await authFetch(\n siteUrl,\n \"/api-key/create\",\n {\n configId: \"user-keys\",\n name: `bos login — ${opts.device ?? \"cli\"} — ${new Date().toISOString().slice(0, 16).replace(\"T\", \" \")}`,\n ...(opts.expiresIn ? { expiresIn: opts.expiresIn } : {}),\n },\n cookiePair,\n );\n if (status !== 200) {\n const detail = (json as { message?: string }).message;\n throw new Error(\n `CLI credential could not be created${detail ? `: ${detail}` : ` (${status})`}`,\n );\n }\n const created = json as ApiKeyCreated;\n if (!created.key) throw new Error(\"API key creation returned no key\");\n return { key: created.key, id: created.id };\n}\n\nasync function resolveNearAccount(siteUrl: string, cookiePair: string): Promise<string | null> {\n try {\n const response = await fetch(`${siteUrl.replace(/\\/$/, \"\")}/api/auth/near/list-accounts`, {\n headers: { cookie: cookiePair },\n });\n if (!response.ok) return null;\n const data = (await response.json()) as NearAccountsResponse;\n return data.activeAccount?.accountId ?? null;\n } catch {\n return null;\n }\n}\n\nexport async function openInBrowser(url: string): Promise<void> {\n const os = platform();\n try {\n if (os === \"darwin\") {\n await execa(\"open\", [url], { stdio: \"ignore\" });\n } else if (os === \"win32\") {\n await execa(\"cmd\", [\"/c\", \"start\", \"\", url], {\n stdio: \"ignore\",\n windowsVerbatimArguments: true,\n });\n } else {\n await execa(\"xdg-open\", [url], { stdio: \"ignore\" });\n }\n } catch {\n throw new Error(`Failed to open browser. Open this URL manually:\\n ${url}`);\n }\n}\n"],"mappings":"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"}