Skip to content

Commit 43bafe2

Browse files
authored
Add language switcher and Openclaw auth proxy (#426)
* Add language switcher and stabilize tool versions - Add locale selection to settings and translate key UI strings - Make sidebar cookie writes tolerant of missing cookieStore - Pin package typecheck, fmt, and lint commands to Bunx wrappers * Refine auth proxy and server event handling - simplify request and header handling in the auth router - make WebSocket HTTP routing await API handlers safely - clean up gateway waiter iteration and scope normalization
1 parent 0ca3a4b commit 43bafe2

20 files changed

Lines changed: 192 additions & 115 deletions

File tree

apps/desktop/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"dev:electron": "bun run scripts/dev-electron.mjs",
1010
"build": "tsdown",
1111
"start": "bun run scripts/start-electron.mjs",
12-
"typecheck": "tsc --noEmit",
12+
"typecheck": "bunx tsc@5.7.3 --noEmit",
1313
"test": "vitest run --passWithNoTests",
1414
"smoke-test": "node scripts/smoke-test.mjs",
1515
"release-smoke": "node ../../scripts/release-smoke.ts"

apps/marketing/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"preview": "next start",
1111
"start": "next start",
1212
"lint": "eslint .",
13-
"typecheck": "tsc --noEmit"
13+
"typecheck": "bunx tsc@5.7.3 --noEmit"
1414
},
1515
"dependencies": {
1616
"@hookform/resolvers": "^3.10.0",

apps/mobile/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"type": "module",
66
"scripts": {
77
"build": "bun run scripts/build-mobile-shell.mjs",
8-
"typecheck": "tsc --noEmit",
8+
"typecheck": "bunx tsc@5.7.3 --noEmit",
99
"test": "vitest run --passWithNoTests",
1010
"sync": "cap sync",
1111
"open:ios": "cap open ios",

apps/server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"build": "node scripts/cli.ts build",
2020
"start": "node dist/index.mjs",
2121
"prepare": "node -e \"process.exit(process.env.CI ? 0 : 1)\" || (node ../../scripts/patch-effect-language-service.ts && node ../../scripts/patch-effect-smol-peer-installs.mjs)",
22-
"typecheck": "tsc --noEmit",
22+
"typecheck": "bunx tsc@5.7.3 --noEmit",
2323
"test": "vitest run"
2424
},
2525
"dependencies": {

apps/server/src/api/authRouter.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ function respondJson(
2626
): void {
2727
res.writeHead(statusCode, {
2828
"Content-Type": "application/json",
29-
...(headers ?? {}),
29+
...headers,
3030
});
3131
res.end(JSON.stringify(body));
3232
}
@@ -52,7 +52,9 @@ function mergeAnthropicBetaHeader(value: string | null): string {
5252
return parts.join(",");
5353
}
5454

55-
async function readJsonRequestBody(req: http.IncomingMessage): Promise<Record<string, unknown> | null> {
55+
async function readJsonRequestBody(
56+
req: http.IncomingMessage,
57+
): Promise<Record<string, unknown> | null> {
5658
const chunks: Buffer[] = [];
5759
for await (const chunk of req) {
5860
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : Buffer.from(chunk));

apps/server/src/api/router.test.ts

Lines changed: 71 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -87,21 +87,24 @@ describe("createApiRouter", () => {
8787
tokenManager,
8888
});
8989

90-
await withServer((req, res) => {
91-
const url = new URL(req.url ?? "/", "http://127.0.0.1");
92-
void tryHandleApiRequest(req, res, url);
93-
}, async (baseUrl) => {
94-
const response = await request(baseUrl, "/api/pairing?ttl=300");
95-
const body = JSON.parse(response.body) as {
96-
pairingUrl: string;
97-
serverUrl: string;
98-
expiresAt: string;
99-
};
100-
expect(response.statusCode).toBe(200);
101-
expect(body.serverUrl).toBe("http://127.0.0.1:31337");
102-
expect(body.pairingUrl).toContain("okcode://pair?server=");
103-
expect(body.expiresAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
104-
});
90+
await withServer(
91+
(req, res) => {
92+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
93+
void tryHandleApiRequest(req, res, url);
94+
},
95+
async (baseUrl) => {
96+
const response = await request(baseUrl, "/api/pairing?ttl=300");
97+
const body = JSON.parse(response.body) as {
98+
pairingUrl: string;
99+
serverUrl: string;
100+
expiresAt: string;
101+
};
102+
expect(response.statusCode).toBe(200);
103+
expect(body.serverUrl).toBe("http://127.0.0.1:31337");
104+
expect(body.pairingUrl).toContain("okcode://pair?server=");
105+
expect(body.expiresAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
106+
},
107+
);
105108
});
106109

107110
it("proxies Anthropic message requests with the Claude Code envelope", async () => {
@@ -142,49 +145,50 @@ describe("createApiRouter", () => {
142145
anthropicBaseUrl: `http://127.0.0.1:${upstreamAddress.port}`,
143146
});
144147

145-
await withServer((req, res) => {
146-
const url = new URL(req.url ?? "/", "http://127.0.0.1");
147-
void tryHandleApiRequest(req, res, url);
148-
}, async (baseUrl) => {
149-
const response = await request(baseUrl, "/api/auth/anthropic/v1/messages", {
150-
method: "POST",
151-
headers: {
152-
"Content-Type": "application/json",
153-
"x-api-key": "test-key",
154-
"anthropic-version": "2023-06-01",
155-
"anthropic-beta": "tools-2024-04-04",
156-
},
157-
body: JSON.stringify({
148+
await withServer(
149+
(req, res) => {
150+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
151+
void tryHandleApiRequest(req, res, url);
152+
},
153+
async (baseUrl) => {
154+
const response = await request(baseUrl, "/api/auth/anthropic/v1/messages", {
155+
method: "POST",
156+
headers: {
157+
"Content-Type": "application/json",
158+
"x-api-key": "test-key",
159+
"anthropic-version": "2023-06-01",
160+
"anthropic-beta": "tools-2024-04-04",
161+
},
162+
body: JSON.stringify({
163+
model: "claude-sonnet-4-20250514",
164+
max_tokens: 64,
165+
system: "Original system prompt",
166+
messages: [{ role: "user", content: "Hello" }],
167+
stream: true,
168+
}),
169+
});
170+
171+
expect(response.statusCode).toBe(200);
172+
expect(JSON.parse(response.body)).toEqual({ ok: true, proxied: true });
173+
expect(upstreamHeaders?.["x-api-key"]).toBe("test-key");
174+
expect(upstreamHeaders?.["anthropic-version"]).toBe("2023-06-01");
175+
expect(upstreamHeaders?.["anthropic-beta"]).toBe("claude-code-20250219,tools-2024-04-04");
176+
expect(upstreamBody).toMatchObject({
158177
model: "claude-sonnet-4-20250514",
159-
max_tokens: 64,
160-
system: "Original system prompt",
161-
messages: [{ role: "user", content: "Hello" }],
162178
stream: true,
163-
}),
164-
});
165-
166-
expect(response.statusCode).toBe(200);
167-
expect(JSON.parse(response.body)).toEqual({ ok: true, proxied: true });
168-
expect(upstreamHeaders?.["x-api-key"]).toBe("test-key");
169-
expect(upstreamHeaders?.["anthropic-version"]).toBe("2023-06-01");
170-
expect(upstreamHeaders?.["anthropic-beta"]).toBe(
171-
"claude-code-20250219,tools-2024-04-04",
172-
);
173-
expect(upstreamBody).toMatchObject({
174-
model: "claude-sonnet-4-20250514",
175-
stream: true,
176-
});
177-
expect(upstreamBody?.system).toEqual([
178-
{
179-
type: "text",
180-
text: "You are Claude Code, Anthropic's official CLI for Claude.",
181-
},
182-
{
183-
type: "text",
184-
text: "Original system prompt",
185-
},
186-
]);
187-
});
179+
});
180+
expect(upstreamBody?.system).toEqual([
181+
{
182+
type: "text",
183+
text: "You are Claude Code, Anthropic's official CLI for Claude.",
184+
},
185+
{
186+
type: "text",
187+
text: "Original system prompt",
188+
},
189+
]);
190+
},
191+
);
188192
});
189193

190194
it("returns a JSON 404 for unknown API routes", async () => {
@@ -195,13 +199,16 @@ describe("createApiRouter", () => {
195199
tokenManager: new TokenManager(undefined),
196200
});
197201

198-
await withServer((req, res) => {
199-
const url = new URL(req.url ?? "/", "http://127.0.0.1");
200-
void tryHandleApiRequest(req, res, url);
201-
}, async (baseUrl) => {
202-
const response = await request(baseUrl, "/api/unknown");
203-
expect(response.statusCode).toBe(404);
204-
expect(JSON.parse(response.body)).toEqual({ error: "Not Found" });
205-
});
202+
await withServer(
203+
(req, res) => {
204+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
205+
void tryHandleApiRequest(req, res, url);
206+
},
207+
async (baseUrl) => {
208+
const response = await request(baseUrl, "/api/unknown");
209+
expect(response.statusCode).toBe(404);
210+
expect(JSON.parse(response.body)).toEqual({ error: "Not Found" });
211+
},
212+
);
206213
});
207214
});

apps/server/src/openclaw/GatewayClient.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -466,7 +466,7 @@ export class OpenclawGatewayClient {
466466

467467
if (frame.type === "event" && typeof frame.event === "string") {
468468
let matchedWaiter = false;
469-
for (const waiter of [...this.pendingEventWaiters]) {
469+
for (const waiter of this.pendingEventWaiters) {
470470
if (waiter.eventName === frame.event) {
471471
matchedWaiter = true;
472472
this.pendingEventWaiters.delete(waiter);

apps/server/src/persistence/Layers/OpenclawGatewayConfig.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ function normalizeScopes(scopes: ReadonlyArray<string> | undefined): string[] {
6868
unique.add(trimmed);
6969
}
7070
}
71-
return [...unique].sort((left, right) => left.localeCompare(right));
71+
return [...unique].toSorted((left, right) => left.localeCompare(right));
7272
}
7373

7474
function fromGeneratedIdentity(identity: ReturnType<typeof generateOpenclawDeviceIdentity>) {

apps/server/src/sme/authValidation.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,6 @@ import {
1818

1919
const OPENAI_MODEL_PROVIDERS = new Set(["openai"]);
2020

21-
function normalizeOptionalValue(value: string | undefined | null): string | null {
22-
const trimmed = value?.trim();
23-
return trimmed && trimmed.length > 0 ? trimmed : null;
24-
}
25-
2621
export function getAllowedSmeAuthMethods(provider: ProviderKind): readonly SmeAuthMethod[] {
2722
switch (provider) {
2823
case "claudeAgent":

apps/server/src/wsServer.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -647,7 +647,7 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return<
647647
});
648648

649649
// HTTP server — serves static files or redirects to Vite dev server
650-
const httpServer = http.createServer((req, res) => {
650+
const httpServer = http.createServer(async (req, res) => {
651651
const respond = (
652652
statusCode: number,
653653
headers: Record<string, string>,
@@ -660,7 +660,7 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return<
660660
void Effect.runPromise(
661661
Effect.gen(function* () {
662662
const url = new URL(req.url ?? "/", `http://localhost:${port}`);
663-
if (await tryHandleApiRequest(req, res, url)) {
663+
if (yield* Effect.promise(() => tryHandleApiRequest(req, res, url))) {
664664
return;
665665
}
666666

0 commit comments

Comments
 (0)