openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
163 lines (162 loc) • 7.33 kB
JavaScript
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 };