This repository has been archived on 2026-08-07. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
openclaw/tools/node/npm/lib/node_modules/clawhub/dist/deviceAuth.js
T

153 lines
5.4 KiB
JavaScript

/**
* GitHub Device Flow authentication for headless environments.
*
* Implements RFC 8628 / GitHub's Device Flow:
* https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#device-flow
*
* This allows CLI authentication without a browser redirect to localhost,
* enabling headless agents and remote servers to authenticate.
*/
const DEFAULT_SCOPE = "read write";
/**
* Request a device code from the ClawHub device flow endpoint.
*/
export async function requestDeviceCode(config) {
const url = new URL("/api/cli/device/code", config.apiUrl);
const body = {
scope: config.scope ?? DEFAULT_SCOPE,
site_url: config.siteUrl,
};
if (config.label) {
body.label = config.label;
}
if (config.clientId) {
body.client_id = config.clientId;
}
const response = await fetch(url.toString(), {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(body),
});
if (!response.ok) {
const text = await response.text().catch(() => "");
throw new Error(`Device code request failed (${response.status}): ${text || response.statusText}`);
}
const data = await response.json().catch(() => null);
if (!isDeviceCodeResponse(data)) {
throw new Error("Invalid device code response from server");
}
return data;
}
/**
* Poll for the device flow token until the user completes authorization,
* the code expires, or an unrecoverable error occurs.
*/
export async function pollForDeviceToken(config, deviceCode, options) {
const url = new URL("/api/cli/device/token", config.apiUrl);
const deadline = Date.now() + options.expiresIn * 1000;
const expirationMessage = "Device code expired (timeout). Please try again.";
let interval = options.interval * 1000;
const body = {
device_code: deviceCode,
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
};
if (config.clientId) {
body.client_id = config.clientId;
}
while (Date.now() < deadline) {
await sleep(interval);
const remainingMs = deadline - Date.now();
if (remainingMs <= 0) {
break;
}
const controller = new AbortController();
let timeout;
let response;
try {
response = await new Promise((resolve, reject) => {
timeout = setTimeout(() => {
controller.abort();
reject(new Error(expirationMessage));
}, remainingMs);
void fetch(url.toString(), {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(body),
signal: controller.signal,
}).then(resolve, reject);
});
}
catch (error) {
if (controller.signal.aborted) {
throw new Error(expirationMessage, { cause: error });
}
throw error;
}
finally {
if (timeout !== undefined) {
clearTimeout(timeout);
}
}
// Parse JSON once to avoid "body already read" errors
const data = await response.json().catch(() => null);
if (response.ok) {
if (!isDeviceTokenResponse(data)) {
throw new Error("Invalid device token response from server");
}
return data;
}
if (!isRecord(data)) {
throw new Error("Invalid device token response from server");
}
const errorData = data;
switch (errorData.error) {
case "authorization_pending":
// User hasn't completed auth yet — keep polling
break;
case "slow_down":
// Server requests longer interval
interval = (errorData.interval ?? Math.ceil(interval / 1000) + 5) * 1000;
break;
case "expired_token":
throw new Error("Device code expired. Please try again.");
case "access_denied":
throw new Error("Authorization denied by user.");
default:
throw new Error(`Device flow error: ${errorData.error_description || errorData.error || "unknown error"}`);
}
}
throw new Error(expirationMessage);
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function isRecord(value) {
return typeof value === "object" && value !== null;
}
function isNonEmptyString(value) {
return typeof value === "string" && value.trim().length > 0;
}
function isPositiveFiniteNumber(value) {
return typeof value === "number" && Number.isFinite(value) && value > 0;
}
function isDeviceCodeResponse(value) {
return (isRecord(value) &&
isNonEmptyString(value.device_code) &&
isNonEmptyString(value.user_code) &&
isNonEmptyString(value.verification_uri) &&
isPositiveFiniteNumber(value.expires_in) &&
isPositiveFiniteNumber(value.interval));
}
function isDeviceTokenResponse(value) {
return (isRecord(value) &&
isNonEmptyString(value.access_token) &&
isNonEmptyString(value.token_type) &&
isNonEmptyString(value.scope));
}
//# sourceMappingURL=deviceAuth.js.map