Spaces:
Paused
Paused
Upload 2526 files
Browse files- src/agents/bash-tools.exec.pty-fallback.test.ts +1 -1
- src/agents/bash-tools.process.send-keys.test.ts +2 -2
- src/agents/bash-tools.process.ts +2 -2
- src/agents/bash-tools.test.ts +1 -0
- src/agents/pi-tools.workspace-paths.test.ts +6 -2
- src/cli/program.smoke.test.ts +1 -0
- src/gateway/control-ui.ts +109 -37
- src/gateway/server/ws-connection/message-handler.ts +3 -1
src/agents/bash-tools.exec.pty-fallback.test.ts
CHANGED
|
@@ -19,7 +19,7 @@ test("exec falls back when PTY spawn fails", async () => {
|
|
| 19 |
const { createExecTool } = await import("./bash-tools.exec");
|
| 20 |
const tool = createExecTool({ allowBackground: false });
|
| 21 |
const result = await tool.execute("toolcall", {
|
| 22 |
-
command: "
|
| 23 |
pty: true,
|
| 24 |
});
|
| 25 |
|
|
|
|
| 19 |
const { createExecTool } = await import("./bash-tools.exec");
|
| 20 |
const tool = createExecTool({ allowBackground: false });
|
| 21 |
const result = await tool.execute("toolcall", {
|
| 22 |
+
command: "echo ok",
|
| 23 |
pty: true,
|
| 24 |
});
|
| 25 |
|
src/agents/bash-tools.process.send-keys.test.ts
CHANGED
|
@@ -29,7 +29,7 @@ test("process send-keys encodes Enter for pty sessions", async () => {
|
|
| 29 |
keys: ["h", "i", "Enter"],
|
| 30 |
});
|
| 31 |
|
| 32 |
-
const deadline = Date.now() + (process.platform === "win32" ?
|
| 33 |
while (Date.now() < deadline) {
|
| 34 |
await wait(50);
|
| 35 |
const poll = await processTool.execute("toolcall", { action: "poll", sessionId });
|
|
@@ -63,7 +63,7 @@ test("process submit sends Enter for pty sessions", async () => {
|
|
| 63 |
sessionId,
|
| 64 |
});
|
| 65 |
|
| 66 |
-
const deadline = Date.now() + (process.platform === "win32" ?
|
| 67 |
while (Date.now() < deadline) {
|
| 68 |
await wait(50);
|
| 69 |
const poll = await processTool.execute("toolcall", { action: "poll", sessionId });
|
|
|
|
| 29 |
keys: ["h", "i", "Enter"],
|
| 30 |
});
|
| 31 |
|
| 32 |
+
const deadline = Date.now() + (process.platform === "win32" ? 10000 : 5000);
|
| 33 |
while (Date.now() < deadline) {
|
| 34 |
await wait(50);
|
| 35 |
const poll = await processTool.execute("toolcall", { action: "poll", sessionId });
|
|
|
|
| 63 |
sessionId,
|
| 64 |
});
|
| 65 |
|
| 66 |
+
const deadline = Date.now() + (process.platform === "win32" ? 10000 : 5000);
|
| 67 |
while (Date.now() < deadline) {
|
| 68 |
await wait(50);
|
| 69 |
const poll = await processTool.execute("toolcall", { action: "poll", sessionId });
|
src/agents/bash-tools.process.ts
CHANGED
|
@@ -476,7 +476,7 @@ export function createProcessTool(
|
|
| 476 |
};
|
| 477 |
}
|
| 478 |
await new Promise<void>((resolve, reject) => {
|
| 479 |
-
stdin.write("\r", (err) => {
|
| 480 |
if (err) {
|
| 481 |
reject(err);
|
| 482 |
} else {
|
|
@@ -488,7 +488,7 @@ export function createProcessTool(
|
|
| 488 |
content: [
|
| 489 |
{
|
| 490 |
type: "text",
|
| 491 |
-
text: `Submitted session ${params.sessionId} (sent
|
| 492 |
},
|
| 493 |
],
|
| 494 |
details: {
|
|
|
|
| 476 |
};
|
| 477 |
}
|
| 478 |
await new Promise<void>((resolve, reject) => {
|
| 479 |
+
stdin.write("\r\n", (err) => {
|
| 480 |
if (err) {
|
| 481 |
reject(err);
|
| 482 |
} else {
|
|
|
|
| 488 |
content: [
|
| 489 |
{
|
| 490 |
type: "text",
|
| 491 |
+
text: `Submitted session ${params.sessionId} (sent CRLF).`,
|
| 492 |
},
|
| 493 |
],
|
| 494 |
details: {
|
src/agents/bash-tools.test.ts
CHANGED
|
@@ -193,6 +193,7 @@ describe("exec tool backgrounding", () => {
|
|
| 193 |
elevated: { enabled: true, allowed: false, defaultLevel: "on" },
|
| 194 |
backgroundMs: 1000,
|
| 195 |
timeoutSec: 5,
|
|
|
|
| 196 |
});
|
| 197 |
|
| 198 |
const result = await customBash.execute("call1", {
|
|
|
|
| 193 |
elevated: { enabled: true, allowed: false, defaultLevel: "on" },
|
| 194 |
backgroundMs: 1000,
|
| 195 |
timeoutSec: 5,
|
| 196 |
+
allowBackground: false,
|
| 197 |
});
|
| 198 |
|
| 199 |
const result = await customBash.execute("call1", {
|
src/agents/pi-tools.workspace-paths.test.ts
CHANGED
|
@@ -19,7 +19,9 @@ function getTextContent(result?: { content?: Array<{ type: string; text?: string
|
|
| 19 |
}
|
| 20 |
|
| 21 |
describe("workspace path resolution", () => {
|
| 22 |
-
it(
|
|
|
|
|
|
|
| 23 |
await withTempDir("openclaw-ws-", async (workspaceDir) => {
|
| 24 |
await withTempDir("openclaw-cwd-", async (otherDir) => {
|
| 25 |
const prevCwd = process.cwd();
|
|
@@ -40,7 +42,9 @@ describe("workspace path resolution", () => {
|
|
| 40 |
}
|
| 41 |
});
|
| 42 |
});
|
| 43 |
-
|
|
|
|
|
|
|
| 44 |
|
| 45 |
it("writes relative paths against workspaceDir even after cwd changes", async () => {
|
| 46 |
await withTempDir("openclaw-ws-", async (workspaceDir) => {
|
|
|
|
| 19 |
}
|
| 20 |
|
| 21 |
describe("workspace path resolution", () => {
|
| 22 |
+
it(
|
| 23 |
+
"reads relative paths against workspaceDir even after cwd changes",
|
| 24 |
+
async () => {
|
| 25 |
await withTempDir("openclaw-ws-", async (workspaceDir) => {
|
| 26 |
await withTempDir("openclaw-cwd-", async (otherDir) => {
|
| 27 |
const prevCwd = process.cwd();
|
|
|
|
| 42 |
}
|
| 43 |
});
|
| 44 |
});
|
| 45 |
+
},
|
| 46 |
+
240_000,
|
| 47 |
+
);
|
| 48 |
|
| 49 |
it("writes relative paths against workspaceDir even after cwd changes", async () => {
|
| 50 |
await withTempDir("openclaw-ws-", async (workspaceDir) => {
|
src/cli/program.smoke.test.ts
CHANGED
|
@@ -49,6 +49,7 @@ vi.mock("../gateway/call.js", () => ({
|
|
| 49 |
message: "Gateway target: ws://127.0.0.1:1234",
|
| 50 |
}),
|
| 51 |
}));
|
|
|
|
| 52 |
vi.mock("./deps.js", () => ({ createDefaultDeps: () => ({}) }));
|
| 53 |
|
| 54 |
const { buildProgram } = await import("./program.js");
|
|
|
|
| 49 |
message: "Gateway target: ws://127.0.0.1:1234",
|
| 50 |
}),
|
| 51 |
}));
|
| 52 |
+
vi.mock("./plugin-registry.js", () => ({ ensurePluginRegistryLoaded: () => {} }));
|
| 53 |
vi.mock("./deps.js", () => ({ createDefaultDeps: () => ({}) }));
|
| 54 |
|
| 55 |
const { buildProgram } = await import("./program.js");
|
src/gateway/control-ui.ts
CHANGED
|
@@ -209,7 +209,9 @@ function normalizeIdentifier(value: string) {
|
|
| 209 |
|
| 210 |
function parseCsvSet(value: string | undefined): Set<string> | null {
|
| 211 |
const raw = value?.trim();
|
| 212 |
-
if (!raw)
|
|
|
|
|
|
|
| 213 |
const items = raw
|
| 214 |
.split(/[,\n]/g)
|
| 215 |
.map((item) => normalizeIdentifier(item))
|
|
@@ -265,14 +267,20 @@ function signToken(secret: string, payload: unknown) {
|
|
| 265 |
|
| 266 |
function verifyToken<T>(secret: string, token: string): { ok: true; value: T } | { ok: false } {
|
| 267 |
const parts = token.split(".");
|
| 268 |
-
if (parts.length !== 2)
|
|
|
|
|
|
|
| 269 |
const [body, sig] = parts;
|
| 270 |
const expected = crypto.createHmac("sha256", secret).update(body).digest("base64url");
|
| 271 |
try {
|
| 272 |
const a = Buffer.from(sig);
|
| 273 |
const b = Buffer.from(expected);
|
| 274 |
-
if (a.length !== b.length)
|
| 275 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 276 |
} catch {
|
| 277 |
return { ok: false };
|
| 278 |
}
|
|
@@ -285,14 +293,20 @@ function verifyToken<T>(secret: string, token: string): { ok: true; value: T } |
|
|
| 285 |
}
|
| 286 |
|
| 287 |
function parseCookies(header: string | undefined): Record<string, string> {
|
| 288 |
-
if (!header)
|
|
|
|
|
|
|
| 289 |
const out: Record<string, string> = {};
|
| 290 |
for (const part of header.split(";")) {
|
| 291 |
const idx = part.indexOf("=");
|
| 292 |
-
if (idx === -1)
|
|
|
|
|
|
|
| 293 |
const k = part.slice(0, idx).trim();
|
| 294 |
const v = part.slice(idx + 1).trim();
|
| 295 |
-
if (!k)
|
|
|
|
|
|
|
| 296 |
out[k] = decodeURIComponent(v);
|
| 297 |
}
|
| 298 |
return out;
|
|
@@ -314,7 +328,9 @@ function appendSetCookie(res: ServerResponse, value: string) {
|
|
| 314 |
function isSecureRequest(req: IncomingMessage) {
|
| 315 |
const xfProto = req.headers["x-forwarded-proto"];
|
| 316 |
const proto = Array.isArray(xfProto) ? xfProto[0] : xfProto;
|
| 317 |
-
if (proto && proto.toLowerCase().includes("https"))
|
|
|
|
|
|
|
| 318 |
return Boolean((req.socket as { encrypted?: boolean }).encrypted);
|
| 319 |
}
|
| 320 |
|
|
@@ -326,7 +342,7 @@ function getRequestOrigin(req: IncomingMessage) {
|
|
| 326 |
return `${proto}://${host}`;
|
| 327 |
}
|
| 328 |
|
| 329 |
-
function controlUiCookiePath(
|
| 330 |
return "/";
|
| 331 |
}
|
| 332 |
|
|
@@ -347,7 +363,9 @@ function setCookie(
|
|
| 347 |
if (typeof opts.maxAgeSeconds === "number") {
|
| 348 |
parts.push(`Max-Age=${Math.max(0, Math.floor(opts.maxAgeSeconds))}`);
|
| 349 |
}
|
| 350 |
-
if (opts.secure)
|
|
|
|
|
|
|
| 351 |
appendSetCookie(res, parts.join("; "));
|
| 352 |
}
|
| 353 |
|
|
@@ -380,15 +398,21 @@ function isAllowedAccount(cfg: ControlUiOauthConfig, payload: ControlUiSessionPa
|
|
| 380 |
}
|
| 381 |
|
| 382 |
if (cfg.allowedEmails) {
|
| 383 |
-
if (!email || !cfg.allowedEmails.has(email))
|
|
|
|
|
|
|
| 384 |
}
|
| 385 |
|
| 386 |
if (payload.provider === "google" && cfg.allowedGoogleEmails) {
|
| 387 |
-
if (!email || !cfg.allowedGoogleEmails.has(email))
|
|
|
|
|
|
|
| 388 |
}
|
| 389 |
|
| 390 |
if (payload.provider === "github" && cfg.allowedGithubLogins) {
|
| 391 |
-
if (!login || !cfg.allowedGithubLogins.has(login))
|
|
|
|
|
|
|
| 392 |
}
|
| 393 |
|
| 394 |
return true;
|
|
@@ -397,15 +421,29 @@ function isAllowedAccount(cfg: ControlUiOauthConfig, payload: ControlUiSessionPa
|
|
| 397 |
function readSessionFromRequest(req: IncomingMessage, cfg: ControlUiOauthConfig): ControlUiSessionPayload | null {
|
| 398 |
const cookies = parseCookies(req.headers.cookie);
|
| 399 |
const raw = cookies[CONTROL_UI_SESSION_COOKIE];
|
| 400 |
-
if (!raw)
|
|
|
|
|
|
|
| 401 |
const verified = verifyToken<ControlUiSessionPayload>(cfg.sessionSecret, raw);
|
| 402 |
-
if (!verified.ok)
|
|
|
|
|
|
|
| 403 |
const value = verified.value;
|
| 404 |
-
if (!value || value.v !== 1)
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
if (typeof value.
|
| 408 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 409 |
return value;
|
| 410 |
}
|
| 411 |
|
|
@@ -421,9 +459,13 @@ export function readControlUiOauthSessionIdentityFromRequest(
|
|
| 421 |
req: IncomingMessage,
|
| 422 |
): ControlUiOauthSessionIdentity | null {
|
| 423 |
const cfg = resolveControlUiOauthConfig();
|
| 424 |
-
if (!isControlUiOauthEnabled(cfg))
|
|
|
|
|
|
|
| 425 |
const session = readSessionFromRequest(req, cfg);
|
| 426 |
-
if (!session)
|
|
|
|
|
|
|
| 427 |
return {
|
| 428 |
provider: session.provider,
|
| 429 |
sub: session.sub,
|
|
@@ -446,15 +488,27 @@ function issueOAuthState(res: ServerResponse, cfg: ControlUiOauthConfig, basePat
|
|
| 446 |
function consumeOAuthState(req: IncomingMessage, res: ServerResponse, cfg: ControlUiOauthConfig, basePath: string, secure: boolean) {
|
| 447 |
const cookies = parseCookies(req.headers.cookie);
|
| 448 |
const raw = cookies[CONTROL_UI_OAUTH_STATE_COOKIE];
|
| 449 |
-
if (!raw)
|
|
|
|
|
|
|
| 450 |
const verified = verifyToken<OAuthStatePayload>(cfg.sessionSecret, raw);
|
| 451 |
clearCookie(res, { name: CONTROL_UI_OAUTH_STATE_COOKIE, basePath, secure });
|
| 452 |
-
if (!verified.ok)
|
|
|
|
|
|
|
| 453 |
const value = verified.value;
|
| 454 |
-
if (!value || value.v !== 1)
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
if (typeof value.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 458 |
return { token: raw, payload: value };
|
| 459 |
}
|
| 460 |
|
|
@@ -528,7 +582,9 @@ async function exchangeGoogleUser(opts: {
|
|
| 528 |
}
|
| 529 |
const tokenJson = (await tokenRes.json()) as { access_token?: unknown };
|
| 530 |
const accessToken = typeof tokenJson.access_token === "string" ? tokenJson.access_token : null;
|
| 531 |
-
if (!accessToken)
|
|
|
|
|
|
|
| 532 |
|
| 533 |
const userRes = await fetch("https://www.googleapis.com/oauth2/v3/userinfo", {
|
| 534 |
headers: { Authorization: `Bearer ${accessToken}` },
|
|
@@ -548,8 +604,12 @@ async function exchangeGoogleUser(opts: {
|
|
| 548 |
const emailVerified = userJson.email_verified === true;
|
| 549 |
const name = typeof userJson.name === "string" ? userJson.name : null;
|
| 550 |
const picture = typeof userJson.picture === "string" ? userJson.picture : null;
|
| 551 |
-
if (!sub)
|
| 552 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 553 |
return { sub, email, name, picture };
|
| 554 |
}
|
| 555 |
|
|
@@ -569,7 +629,9 @@ async function exchangeGithubUser(opts: { code: string; redirectUri: string; cli
|
|
| 569 |
}
|
| 570 |
const tokenJson = (await tokenRes.json()) as { access_token?: unknown; token_type?: unknown };
|
| 571 |
const accessToken = typeof tokenJson.access_token === "string" ? tokenJson.access_token : null;
|
| 572 |
-
if (!accessToken)
|
|
|
|
|
|
|
| 573 |
|
| 574 |
const userRes = await fetch("https://api.github.com/user", {
|
| 575 |
headers: {
|
|
@@ -578,13 +640,17 @@ async function exchangeGithubUser(opts: { code: string; redirectUri: string; cli
|
|
| 578 |
"User-Agent": "openclaw-control-ui",
|
| 579 |
},
|
| 580 |
});
|
| 581 |
-
if (!userRes.ok)
|
|
|
|
|
|
|
| 582 |
const userJson = (await userRes.json()) as { id?: unknown; login?: unknown; name?: unknown; avatar_url?: unknown };
|
| 583 |
const sub = typeof userJson.id === "number" ? String(userJson.id) : typeof userJson.id === "string" ? userJson.id : null;
|
| 584 |
const login = typeof userJson.login === "string" ? userJson.login : null;
|
| 585 |
const name = typeof userJson.name === "string" ? userJson.name : null;
|
| 586 |
const picture = typeof userJson.avatar_url === "string" ? userJson.avatar_url : null;
|
| 587 |
-
if (!sub || !login)
|
|
|
|
|
|
|
| 588 |
|
| 589 |
const emailsRes = await fetch("https://api.github.com/user/emails", {
|
| 590 |
headers: {
|
|
@@ -620,11 +686,17 @@ export async function handleControlUiAuthRequest(
|
|
| 620 |
opts?: ControlUiRequestOptions,
|
| 621 |
): Promise<boolean> {
|
| 622 |
const urlRaw = req.url;
|
| 623 |
-
if (!urlRaw)
|
| 624 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 625 |
|
| 626 |
const cfg = resolveControlUiOauthConfig();
|
| 627 |
-
if (!isControlUiOauthEnabled(cfg))
|
|
|
|
|
|
|
| 628 |
|
| 629 |
const url = new URL(urlRaw, "http://localhost");
|
| 630 |
const basePath = normalizeControlUiBasePath(opts?.basePath);
|
|
|
|
| 209 |
|
| 210 |
function parseCsvSet(value: string | undefined): Set<string> | null {
|
| 211 |
const raw = value?.trim();
|
| 212 |
+
if (!raw) {
|
| 213 |
+
return null;
|
| 214 |
+
}
|
| 215 |
const items = raw
|
| 216 |
.split(/[,\n]/g)
|
| 217 |
.map((item) => normalizeIdentifier(item))
|
|
|
|
| 267 |
|
| 268 |
function verifyToken<T>(secret: string, token: string): { ok: true; value: T } | { ok: false } {
|
| 269 |
const parts = token.split(".");
|
| 270 |
+
if (parts.length !== 2) {
|
| 271 |
+
return { ok: false };
|
| 272 |
+
}
|
| 273 |
const [body, sig] = parts;
|
| 274 |
const expected = crypto.createHmac("sha256", secret).update(body).digest("base64url");
|
| 275 |
try {
|
| 276 |
const a = Buffer.from(sig);
|
| 277 |
const b = Buffer.from(expected);
|
| 278 |
+
if (a.length !== b.length) {
|
| 279 |
+
return { ok: false };
|
| 280 |
+
}
|
| 281 |
+
if (!crypto.timingSafeEqual(a, b)) {
|
| 282 |
+
return { ok: false };
|
| 283 |
+
}
|
| 284 |
} catch {
|
| 285 |
return { ok: false };
|
| 286 |
}
|
|
|
|
| 293 |
}
|
| 294 |
|
| 295 |
function parseCookies(header: string | undefined): Record<string, string> {
|
| 296 |
+
if (!header) {
|
| 297 |
+
return {};
|
| 298 |
+
}
|
| 299 |
const out: Record<string, string> = {};
|
| 300 |
for (const part of header.split(";")) {
|
| 301 |
const idx = part.indexOf("=");
|
| 302 |
+
if (idx === -1) {
|
| 303 |
+
continue;
|
| 304 |
+
}
|
| 305 |
const k = part.slice(0, idx).trim();
|
| 306 |
const v = part.slice(idx + 1).trim();
|
| 307 |
+
if (!k) {
|
| 308 |
+
continue;
|
| 309 |
+
}
|
| 310 |
out[k] = decodeURIComponent(v);
|
| 311 |
}
|
| 312 |
return out;
|
|
|
|
| 328 |
function isSecureRequest(req: IncomingMessage) {
|
| 329 |
const xfProto = req.headers["x-forwarded-proto"];
|
| 330 |
const proto = Array.isArray(xfProto) ? xfProto[0] : xfProto;
|
| 331 |
+
if (proto && proto.toLowerCase().includes("https")) {
|
| 332 |
+
return true;
|
| 333 |
+
}
|
| 334 |
return Boolean((req.socket as { encrypted?: boolean }).encrypted);
|
| 335 |
}
|
| 336 |
|
|
|
|
| 342 |
return `${proto}://${host}`;
|
| 343 |
}
|
| 344 |
|
| 345 |
+
function controlUiCookiePath(_basePath: string) {
|
| 346 |
return "/";
|
| 347 |
}
|
| 348 |
|
|
|
|
| 363 |
if (typeof opts.maxAgeSeconds === "number") {
|
| 364 |
parts.push(`Max-Age=${Math.max(0, Math.floor(opts.maxAgeSeconds))}`);
|
| 365 |
}
|
| 366 |
+
if (opts.secure) {
|
| 367 |
+
parts.push("Secure");
|
| 368 |
+
}
|
| 369 |
appendSetCookie(res, parts.join("; "));
|
| 370 |
}
|
| 371 |
|
|
|
|
| 398 |
}
|
| 399 |
|
| 400 |
if (cfg.allowedEmails) {
|
| 401 |
+
if (!email || !cfg.allowedEmails.has(email)) {
|
| 402 |
+
return false;
|
| 403 |
+
}
|
| 404 |
}
|
| 405 |
|
| 406 |
if (payload.provider === "google" && cfg.allowedGoogleEmails) {
|
| 407 |
+
if (!email || !cfg.allowedGoogleEmails.has(email)) {
|
| 408 |
+
return false;
|
| 409 |
+
}
|
| 410 |
}
|
| 411 |
|
| 412 |
if (payload.provider === "github" && cfg.allowedGithubLogins) {
|
| 413 |
+
if (!login || !cfg.allowedGithubLogins.has(login)) {
|
| 414 |
+
return false;
|
| 415 |
+
}
|
| 416 |
}
|
| 417 |
|
| 418 |
return true;
|
|
|
|
| 421 |
function readSessionFromRequest(req: IncomingMessage, cfg: ControlUiOauthConfig): ControlUiSessionPayload | null {
|
| 422 |
const cookies = parseCookies(req.headers.cookie);
|
| 423 |
const raw = cookies[CONTROL_UI_SESSION_COOKIE];
|
| 424 |
+
if (!raw) {
|
| 425 |
+
return null;
|
| 426 |
+
}
|
| 427 |
const verified = verifyToken<ControlUiSessionPayload>(cfg.sessionSecret, raw);
|
| 428 |
+
if (!verified.ok) {
|
| 429 |
+
return null;
|
| 430 |
+
}
|
| 431 |
const value = verified.value;
|
| 432 |
+
if (!value || value.v !== 1) {
|
| 433 |
+
return null;
|
| 434 |
+
}
|
| 435 |
+
if (typeof value.exp !== "number" || value.exp <= Math.floor(Date.now() / 1000)) {
|
| 436 |
+
return null;
|
| 437 |
+
}
|
| 438 |
+
if (value.provider !== "google" && value.provider !== "github") {
|
| 439 |
+
return null;
|
| 440 |
+
}
|
| 441 |
+
if (typeof value.sub !== "string" || !value.sub) {
|
| 442 |
+
return null;
|
| 443 |
+
}
|
| 444 |
+
if (!isAllowedAccount(cfg, value)) {
|
| 445 |
+
return null;
|
| 446 |
+
}
|
| 447 |
return value;
|
| 448 |
}
|
| 449 |
|
|
|
|
| 459 |
req: IncomingMessage,
|
| 460 |
): ControlUiOauthSessionIdentity | null {
|
| 461 |
const cfg = resolveControlUiOauthConfig();
|
| 462 |
+
if (!isControlUiOauthEnabled(cfg)) {
|
| 463 |
+
return null;
|
| 464 |
+
}
|
| 465 |
const session = readSessionFromRequest(req, cfg);
|
| 466 |
+
if (!session) {
|
| 467 |
+
return null;
|
| 468 |
+
}
|
| 469 |
return {
|
| 470 |
provider: session.provider,
|
| 471 |
sub: session.sub,
|
|
|
|
| 488 |
function consumeOAuthState(req: IncomingMessage, res: ServerResponse, cfg: ControlUiOauthConfig, basePath: string, secure: boolean) {
|
| 489 |
const cookies = parseCookies(req.headers.cookie);
|
| 490 |
const raw = cookies[CONTROL_UI_OAUTH_STATE_COOKIE];
|
| 491 |
+
if (!raw) {
|
| 492 |
+
return null;
|
| 493 |
+
}
|
| 494 |
const verified = verifyToken<OAuthStatePayload>(cfg.sessionSecret, raw);
|
| 495 |
clearCookie(res, { name: CONTROL_UI_OAUTH_STATE_COOKIE, basePath, secure });
|
| 496 |
+
if (!verified.ok) {
|
| 497 |
+
return null;
|
| 498 |
+
}
|
| 499 |
const value = verified.value;
|
| 500 |
+
if (!value || value.v !== 1) {
|
| 501 |
+
return null;
|
| 502 |
+
}
|
| 503 |
+
if (typeof value.exp !== "number" || value.exp <= Math.floor(Date.now() / 1000)) {
|
| 504 |
+
return null;
|
| 505 |
+
}
|
| 506 |
+
if (value.provider !== "google" && value.provider !== "github") {
|
| 507 |
+
return null;
|
| 508 |
+
}
|
| 509 |
+
if (typeof value.nonce !== "string" || !value.nonce) {
|
| 510 |
+
return null;
|
| 511 |
+
}
|
| 512 |
return { token: raw, payload: value };
|
| 513 |
}
|
| 514 |
|
|
|
|
| 582 |
}
|
| 583 |
const tokenJson = (await tokenRes.json()) as { access_token?: unknown };
|
| 584 |
const accessToken = typeof tokenJson.access_token === "string" ? tokenJson.access_token : null;
|
| 585 |
+
if (!accessToken) {
|
| 586 |
+
throw new Error("google access token missing");
|
| 587 |
+
}
|
| 588 |
|
| 589 |
const userRes = await fetch("https://www.googleapis.com/oauth2/v3/userinfo", {
|
| 590 |
headers: { Authorization: `Bearer ${accessToken}` },
|
|
|
|
| 604 |
const emailVerified = userJson.email_verified === true;
|
| 605 |
const name = typeof userJson.name === "string" ? userJson.name : null;
|
| 606 |
const picture = typeof userJson.picture === "string" ? userJson.picture : null;
|
| 607 |
+
if (!sub) {
|
| 608 |
+
throw new Error("google subject missing");
|
| 609 |
+
}
|
| 610 |
+
if (email && !emailVerified) {
|
| 611 |
+
throw new Error("google email not verified");
|
| 612 |
+
}
|
| 613 |
return { sub, email, name, picture };
|
| 614 |
}
|
| 615 |
|
|
|
|
| 629 |
}
|
| 630 |
const tokenJson = (await tokenRes.json()) as { access_token?: unknown; token_type?: unknown };
|
| 631 |
const accessToken = typeof tokenJson.access_token === "string" ? tokenJson.access_token : null;
|
| 632 |
+
if (!accessToken) {
|
| 633 |
+
throw new Error("github access token missing");
|
| 634 |
+
}
|
| 635 |
|
| 636 |
const userRes = await fetch("https://api.github.com/user", {
|
| 637 |
headers: {
|
|
|
|
| 640 |
"User-Agent": "openclaw-control-ui",
|
| 641 |
},
|
| 642 |
});
|
| 643 |
+
if (!userRes.ok) {
|
| 644 |
+
throw new Error(`github user fetch failed: ${userRes.status}`);
|
| 645 |
+
}
|
| 646 |
const userJson = (await userRes.json()) as { id?: unknown; login?: unknown; name?: unknown; avatar_url?: unknown };
|
| 647 |
const sub = typeof userJson.id === "number" ? String(userJson.id) : typeof userJson.id === "string" ? userJson.id : null;
|
| 648 |
const login = typeof userJson.login === "string" ? userJson.login : null;
|
| 649 |
const name = typeof userJson.name === "string" ? userJson.name : null;
|
| 650 |
const picture = typeof userJson.avatar_url === "string" ? userJson.avatar_url : null;
|
| 651 |
+
if (!sub || !login) {
|
| 652 |
+
throw new Error("github user identity missing");
|
| 653 |
+
}
|
| 654 |
|
| 655 |
const emailsRes = await fetch("https://api.github.com/user/emails", {
|
| 656 |
headers: {
|
|
|
|
| 686 |
opts?: ControlUiRequestOptions,
|
| 687 |
): Promise<boolean> {
|
| 688 |
const urlRaw = req.url;
|
| 689 |
+
if (!urlRaw) {
|
| 690 |
+
return false;
|
| 691 |
+
}
|
| 692 |
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
| 693 |
+
return false;
|
| 694 |
+
}
|
| 695 |
|
| 696 |
const cfg = resolveControlUiOauthConfig();
|
| 697 |
+
if (!isControlUiOauthEnabled(cfg)) {
|
| 698 |
+
return false;
|
| 699 |
+
}
|
| 700 |
|
| 701 |
const url = new URL(urlRaw, "http://localhost");
|
| 702 |
const basePath = normalizeControlUiBasePath(opts?.basePath);
|
src/gateway/server/ws-connection/message-handler.ts
CHANGED
|
@@ -625,7 +625,9 @@ export function attachGatewayWsMessageHandler(params: {
|
|
| 625 |
return;
|
| 626 |
}
|
| 627 |
|
| 628 |
-
const
|
|
|
|
|
|
|
| 629 |
if (device && devicePublicKey && !skipPairing) {
|
| 630 |
const requirePairing = async (reason: string, _paired?: { deviceId: string }) => {
|
| 631 |
const pairing = await requestDevicePairing({
|
|
|
|
| 625 |
return;
|
| 626 |
}
|
| 627 |
|
| 628 |
+
const hasControlUiOauth =
|
| 629 |
+
isControlUi && authResult.ok && authResult.method === "control-ui-oauth";
|
| 630 |
+
const skipPairing = (allowControlUiBypass && hasSharedAuth) || hasControlUiOauth;
|
| 631 |
if (device && devicePublicKey && !skipPairing) {
|
| 632 |
const requirePairing = async (reason: string, _paired?: { deviceId: string }) => {
|
| 633 |
const pairing = await requestDevicePairing({
|