UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

197 lines (196 loc) 7.24 kB
import { p as finiteSecondsToTimerSafeMilliseconds } from "./number-coercion-CJQ8TR--.js"; import { r as fetchWithSsrFGuard } from "./fetch-guard-BttkNCLm.js"; import "./media-runtime-fgpmX6eL.js"; import { t as renderQrTerminal } from "./qr-terminal-zFHurnm4.js"; import "./number-runtime-DBLVDypr.js"; import "./ssrf-runtime-BOGN5pUi.js"; //#region extensions/feishu/src/app-registration.ts /** * Feishu app registration via OAuth device-code flow. * * Migrated from feishu-plugin-cli's `feishu-auth.ts` and `install-prompts.ts`. * Replaces axios with native fetch, removes inquirer/ora/chalk in favor of * the openclaw WizardPrompter surface. */ const FEISHU_ACCOUNTS_URL = "https://accounts.feishu.cn"; const LARK_ACCOUNTS_URL = "https://accounts.larksuite.com"; const REGISTRATION_PATH = "/oauth/v1/app/registration"; const REQUEST_TIMEOUT_MS = 1e4; const DEFAULT_REGISTRATION_POLL_INTERVAL_SECONDS = 5; const DEFAULT_REGISTRATION_EXPIRE_SECONDS = 600; function accountsBaseUrl(domain) { return domain === "lark" ? LARK_ACCOUNTS_URL : FEISHU_ACCOUNTS_URL; } async function postRegistration(baseUrl, body) { return await fetchFeishuJson({ url: `${baseUrl}${REGISTRATION_PATH}`, init: { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams(body).toString(), signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) }, auditContext: "feishu.app-registration.post" }); } async function fetchFeishuJson(params) { const { response, release } = await fetchWithSsrFGuard({ url: params.url, init: params.init, policy: { allowedHostnames: [new URL(params.url).hostname] }, auditContext: params.auditContext }); try { return await response.json(); } finally { await release(); } } /** * Step 1: Initialize registration and verify the environment supports * `client_secret` auth. * * @throws If the environment does not support `client_secret`. */ async function initAppRegistration(domain = "feishu") { if (!(await postRegistration(accountsBaseUrl(domain), { action: "init" })).supported_auth_methods?.includes("client_secret")) throw new Error("Current environment does not support client_secret auth method"); } /** * Step 2: Begin the device-code flow. Returns a device code and a QR URL * that the user should scan with Feishu/Lark mobile app. */ async function beginAppRegistration(domain = "feishu") { const res = await postRegistration(accountsBaseUrl(domain), { action: "begin", archetype: "PersonalAgent", auth_method: "client_secret", request_user_info: "open_id" }); const qrUrl = new URL(res.verification_uri_complete); qrUrl.searchParams.set("from", "oc_onboard"); qrUrl.searchParams.set("tp", "ob_cli_app"); return { deviceCode: res.device_code, qrUrl: qrUrl.toString(), userCode: res.user_code, interval: finiteSecondsToTimerSafeMilliseconds(res.interval) === void 0 ? DEFAULT_REGISTRATION_POLL_INTERVAL_SECONDS : res.interval, expireIn: finiteSecondsToTimerSafeMilliseconds(res.expire_in) === void 0 ? DEFAULT_REGISTRATION_EXPIRE_SECONDS : res.expire_in }; } /** * Step 3: Poll for authorization result until success, denial, expiry, or * timeout. Automatically handles domain switching when `tenant_brand` is * detected as "lark". */ async function pollAppRegistration(params) { const { deviceCode, expireIn, initialDomain = "feishu", abortSignal, tp } = params; let currentInterval = params.interval; let domain = initialDomain; let domainSwitched = false; const expireInMs = finiteSecondsToTimerSafeMilliseconds(expireIn) ?? finiteSecondsToTimerSafeMilliseconds(DEFAULT_REGISTRATION_EXPIRE_SECONDS) ?? REQUEST_TIMEOUT_MS; const deadline = Date.now() + expireInMs; while (Date.now() < deadline) { if (abortSignal?.aborted) return { status: "timeout" }; const baseUrl = accountsBaseUrl(domain); let pollRes; try { pollRes = await postRegistration(baseUrl, { action: "poll", device_code: deviceCode, ...tp ? { tp } : {} }); } catch { await sleepRegistrationPollInterval(currentInterval); continue; } if (pollRes.user_info?.tenant_brand) { const isLark = pollRes.user_info.tenant_brand === "lark"; if (!domainSwitched && isLark) { domain = "lark"; domainSwitched = true; continue; } } if (pollRes.client_id && pollRes.client_secret) return { status: "success", result: { appId: pollRes.client_id, appSecret: pollRes.client_secret, domain, openId: pollRes.user_info?.open_id } }; if (pollRes.error) if (pollRes.error === "authorization_pending") {} else if (pollRes.error === "slow_down") currentInterval += 5; else if (pollRes.error === "access_denied") return { status: "access_denied" }; else if (pollRes.error === "expired_token") return { status: "expired" }; else return { status: "error", message: `${pollRes.error}: ${pollRes.error_description ?? "unknown"}` }; await sleepRegistrationPollInterval(currentInterval); } return { status: "timeout" }; } /** * Print QR code directly to stdout. * * QR codes must be printed without any surrounding box/border decoration, * otherwise the pattern is corrupted and cannot be scanned. */ async function printQrCode(url) { const output = await renderQrTerminal(url); process.stdout.write(output.endsWith("\n") ? output : `${output}\n`); } /** * Fetch the app owner's open_id using the application.v6.application.get API. * * Used during setup to auto-populate security policy allowlists. * Returns undefined on any failure (fail-open). */ async function getAppOwnerOpenId(params) { const baseUrl = params.domain === "lark" ? "https://open.larksuite.com" : "https://open.feishu.cn"; try { const tokenData = await fetchFeishuJson({ url: `${baseUrl}/open-apis/auth/v3/tenant_access_token/internal`, init: { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ app_id: params.appId, app_secret: params.appSecret }), signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) }, auditContext: "feishu.app-registration.owner-token" }); if (!tokenData.tenant_access_token) return; const appData = await fetchFeishuJson({ url: `${baseUrl}/open-apis/application/v6/applications/${params.appId}?user_id_type=open_id`, init: { method: "GET", headers: { Authorization: `Bearer ${tokenData.tenant_access_token}`, "Content-Type": "application/json" }, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) }, auditContext: "feishu.app-registration.owner-app" }); if (appData.code !== 0) return; const app = appData.data?.app; const owner = app?.owner; return (owner?.owner_type ?? owner?.type) === 2 && owner?.owner_id ? owner.owner_id : app?.creator_id ?? owner?.owner_id; } catch { return; } } function sleep(ms) { return new Promise((resolve) => { setTimeout(resolve, ms); }); } function sleepRegistrationPollInterval(intervalSeconds) { return sleep(finiteSecondsToTimerSafeMilliseconds(intervalSeconds) ?? finiteSecondsToTimerSafeMilliseconds(DEFAULT_REGISTRATION_POLL_INTERVAL_SECONDS) ?? REQUEST_TIMEOUT_MS); } //#endregion export { beginAppRegistration, getAppOwnerOpenId, initAppRegistration, pollAppRegistration, printQrCode };