UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

163 lines (162 loc) • 7.33 kB
import { t as createDeferredCore } from "./deferred-D0La5CRk.mjs"; import { r as resolveGlobalMap } from "./global-singleton-0m-JziJW.mjs"; import { i as renderOAuthPage } from "./oauth-page-CdAMKCs_.mjs"; import { t as OAUTH_PAGE_CSP } from "./oauth-page-csp-MU9ZpjXc.mjs"; import { s as getGatewayRestartDrainSignal } from "./gateway-work-admission-rc4ZgKq_.mjs"; import { s as isLoopbackHost } from "./net-C6WEu3pd.mjs"; import { r as isGatewayHostBrowserOrigin } from "./origin-check-BATVw76z.mjs"; import { t as getTailscalePublishedOrigin } from "./tailscale-published-origin-BvNdSLZR.mjs"; //#region src/gateway/provider-browser-auth.ts const PROVIDER_OAUTH_CALLBACK_PATH = "/oauth/provider/callback"; const CALLBACK_MAX_URL_BYTES = 8192; const pendingAuthorizations = resolveGlobalMap(Symbol.for("openclaw.providerBrowserAuthorizations"), (pending) => { for (const authorization of pending.values()) authorization.reject(/* @__PURE__ */ new Error("Browser sign-in ended because the Gateway restarted.")); pending.clear(); }); var ProviderBrowserSignInUnavailableError = class extends Error { constructor() { super("Browser sign-in needs a secure Gateway address reachable from your browser. Enable Gateway Tailscale Serve, then retry /login; or use the CLI sign-in flow."); } }; function resolveBrowserAuthOrigin(browser, signal) { const published = getTailscalePublishedOrigin(); if (!browser || browser.origin === published?.origin) return published; if (browser.origin && browser.isLocalClient && isGatewayHostBrowserOrigin(browser)) { const url = new URL(browser.origin); if ((url.protocol === "http:" || url.protocol === "https:") && isLoopbackHost(url.hostname)) return { origin: url.origin, signal }; } } function createProviderBrowserAuthSession(params) { const lifetime = new AbortController(); const browserOriginSignal = params.browserOrigin ? resolveBrowserAuthOrigin(params.browserOrigin, lifetime.signal)?.signal : void 0; const signal = AbortSignal.any([ lifetime.signal, getGatewayRestartDrainSignal(), ...params.signal ? [params.signal] : [], ...browserOriginSignal ? [browserOriginSignal] : [] ]); let origin; let deadline; let expiresAt; let authorizationStarted = false; const originClosed = () => lifetime.abort(/* @__PURE__ */ new Error("Browser sign-in ended because the Gateway address changed. Retry /login.")); const assertCurrent = () => { signal.throwIfAborted(); origin?.signal.throwIfAborted(); if (expiresAt !== void 0 && Date.now() >= expiresAt) throw new Error("Browser sign-in expired. Retry /login."); }; const authorizePrepared = async ({ timeoutMs, prepare }) => { assertCurrent(); const published = resolveBrowserAuthOrigin(params.browserOrigin, lifetime.signal); if (!published) throw new ProviderBrowserSignInUnavailableError(); if (authorizationStarted) throw new Error("Browser sign-in requires a unique authorization state."); authorizationStarted = true; origin = published; origin.signal.addEventListener("abort", originClosed, { once: true }); deadline = setTimeout(() => lifetime.abort(/* @__PURE__ */ new Error("Browser sign-in expired. Retry /login.")), timeoutMs); deadline.unref(); expiresAt = Date.now() + timeoutMs; const prepared = await prepare(new URL(PROVIDER_OAUTH_CALLBACK_PATH, published.origin).href); assertCurrent(); if ("result" in prepared) return prepared.result; const { state } = prepared; if (!state || pendingAuthorizations.has(state)) throw new Error("Browser sign-in requires a unique authorization state."); const callback = createDeferredCore(); const callbackExpiresAt = expiresAt; const requestSignal = signal; const onAbort = () => callback.reject(requestSignal.reason); const pending = { accept: callback.resolve, reject: callback.reject, isCurrent: () => !requestSignal.aborted && Date.now() < callbackExpiresAt && !published.signal.aborted }; pendingAuthorizations.set(state, pending); requestSignal.addEventListener("abort", onAbort, { once: true }); try { const authorizationUrl = new URL(prepared.authorizationUrl); if (authorizationUrl.protocol !== "https:" || authorizationUrl.username || authorizationUrl.password) throw new Error("Provider sign-in requires an HTTPS authorization URL."); const [result] = await Promise.all([callback.promise, Promise.resolve().then(() => params.openUrl(authorizationUrl.href))]); assertCurrent(); if (!pending.isCurrent()) throw new Error("Browser sign-in expired. Retry /login."); return result; } finally { requestSignal.removeEventListener("abort", onAbort); if (pendingAuthorizations.get(state) === pending) pendingAuthorizations.delete(state); } }; const authorize = ({ state, timeoutMs, buildAuthorizationUrl }) => authorizePrepared({ timeoutMs, prepare: (redirectUrl) => ({ state, authorizationUrl: buildAuthorizationUrl(redirectUrl) }) }); return { get available() { return Boolean(resolveBrowserAuthOrigin(params.browserOrigin, lifetime.signal)); }, authorize, authorizePrepared, signal, assertCurrent, close: () => { clearTimeout(deadline); origin?.signal.removeEventListener("abort", originClosed); lifetime.abort(/* @__PURE__ */ new Error("Browser sign-in session closed.")); } }; } function respond(res, status, message) { res.statusCode = status; res.setHeader("Content-Type", "text/html; charset=utf-8"); res.setHeader("Cache-Control", "no-store"); res.setHeader("Referrer-Policy", "no-referrer"); res.setHeader("Content-Security-Policy", OAUTH_PAGE_CSP); res.end(renderOAuthPage({ title: "Provider sign-in", heading: message, message: "Return to OpenClaw for the sign-in result." })); } function handleProviderOAuthCallback(req, res) { const rawUrl = req.url ?? "/"; const url = new URL(rawUrl, "http://localhost"); if (url.pathname !== "/oauth/provider/callback") return false; if (req.method !== "GET" || Buffer.byteLength(rawUrl) > CALLBACK_MAX_URL_BYTES) { respond(res, 400, "Invalid sign-in response."); return true; } const state = url.searchParams.get("state"); const pending = state ? pendingAuthorizations.get(state) : void 0; if (!state || !pending || !pending.isCurrent()) { if (state && pending) { pendingAuthorizations.delete(state); pending.reject(/* @__PURE__ */ new Error("Browser sign-in expired. Retry /login.")); } respond(res, 410, "This sign-in link expired or was already used."); return true; } const code = url.searchParams.get("code"); const denied = url.searchParams.has("error"); if (url.searchParams.getAll("state").length !== 1 || (denied ? Boolean(code) : !code?.trim() || url.searchParams.getAll("code").length !== 1)) { respond(res, 400, "Invalid sign-in response."); return true; } pendingAuthorizations.delete(state); if (denied) { pending.reject(/* @__PURE__ */ new Error("Provider sign-in was declined. Retry /login when you are ready.")); respond(res, 400, "Sign-in was not completed."); } else { pending.accept({ code, state }); respond(res, 200, "Sign-in response received."); } return true; } //#endregion export { resolveBrowserAuthOrigin as a, handleProviderOAuthCallback as i, ProviderBrowserSignInUnavailableError as n, createProviderBrowserAuthSession as r, PROVIDER_OAUTH_CALLBACK_PATH as t };