Compare commits

...

2 Commits

Author SHA1 Message Date
Arvin Xu ddf058fb36 🐛 fix: forward serverUrl in WS auth for apiKey verification
The agent gateway verifies an apiKey by calling
\`\${serverUrl}/api/v1/users/me\` with the token, so \`serverUrl\` has to be
part of the WebSocket auth handshake. The device-gateway-client already
does this; \`lh agent run\` was missing it, producing
"Gateway auth failed: Missing serverUrl for apiKey auth".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 00:00:47 +08:00
Arvin Xu 78dda79f81 update cli version 2026-04-14 23:36:47 +08:00
5 changed files with 21 additions and 5 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
.\" Code generated by `npm run man:generate`; DO NOT EDIT.
.\" Manual command details come from the Commander command tree.
.TH LH 1 "" "@lobehub/cli 0.0.5" "User Commands"
.TH LH 1 "" "@lobehub/cli 0.0.6" "User Commands"
.SH NAME
lh \- LobeHub CLI \- manage and connect to LobeHub services
.SH SYNOPSIS
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@lobehub/cli",
"version": "0.0.5",
"version": "0.0.6",
"type": "module",
"bin": {
"lh": "./dist/index.js",
+1
View File
@@ -373,6 +373,7 @@ export function registerAgentCommand(program: Command) {
gatewayUrl: agentGatewayUrl,
json: options.json,
operationId,
serverUrl,
token,
tokenType,
verbose: options.verbose,
+7 -1
View File
@@ -279,6 +279,8 @@ describe('streamAgentEventsViaWebSocket', () => {
await flush();
const ws = capturedWs!;
// Note: serverUrl is not set here, and JSON.stringify drops undefined keys,
// so the parsed auth message will not contain a `serverUrl` field.
expect(ws.sent.map((s) => JSON.parse(s))).toEqual([
{ token: 'test-token', tokenType: 'jwt', type: 'auth' },
{ lastEventId: '', type: 'resume' },
@@ -288,10 +290,11 @@ describe('streamAgentEventsViaWebSocket', () => {
await promise;
});
it('should send tokenType=apiKey when the caller uses an API key', async () => {
it('should send tokenType=apiKey and serverUrl when the caller uses an API key', async () => {
const promise = streamAgentEventsViaWebSocket({
gatewayUrl: 'https://gw.test.com',
operationId: 'op-1',
serverUrl: 'https://app.lobehub.com',
token: 'lh_sk_abc',
tokenType: 'apiKey',
});
@@ -299,7 +302,10 @@ describe('streamAgentEventsViaWebSocket', () => {
await flush();
const ws = capturedWs!;
// serverUrl is forwarded so the gateway can call back to /api/v1/users/me
// to verify the API key.
expect(ws.sent.map((s) => JSON.parse(s))[0]).toEqual({
serverUrl: 'https://app.lobehub.com',
token: 'lh_sk_abc',
tokenType: 'apiKey',
type: 'auth',
+11 -2
View File
@@ -20,6 +20,12 @@ interface StreamOptions {
interface WebSocketStreamOptions extends StreamOptions {
gatewayUrl: string;
operationId: string;
/**
* LobeHub server URL the gateway should call back to when verifying
* an apiKey token (via `/api/v1/users/me`). Required when
* `tokenType === 'apiKey'`; ignored for JWT.
*/
serverUrl?: string;
token: string;
/**
* How the gateway should verify `token`. `jwt` is the default for
@@ -173,7 +179,7 @@ const HEARTBEAT_INTERVAL = 30_000;
export async function streamAgentEventsViaWebSocket(
options: WebSocketStreamOptions,
): Promise<void> {
const { gatewayUrl, operationId, token, tokenType = 'jwt', ...streamOpts } = options;
const { gatewayUrl, operationId, serverUrl, token, tokenType = 'jwt', ...streamOpts } = options;
const wsUrl = urlJoin(
gatewayUrl.replace(/^http/, 'ws'),
`/ws?operationId=${encodeURIComponent(operationId)}`,
@@ -197,7 +203,10 @@ export async function streamAgentEventsViaWebSocket(
};
ws.onopen = () => {
ws.send(JSON.stringify({ token, tokenType, type: 'auth' }));
// `serverUrl` is required so the gateway can call back to verify an
// apiKey token. Harmless (but unused) for JWT, so we always include it
// when available to match the device-gateway-client contract.
ws.send(JSON.stringify({ serverUrl, token, tokenType, type: 'auth' }));
};
ws.onmessage = (event) => {