Skip to content

Commit d1c49dc

Browse files
committed
fix kilobot findings
1 parent 6be66ec commit d1c49dc

2 files changed

Lines changed: 42 additions & 17 deletions

File tree

src/auth/device-auth.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,18 @@ export type DeviceAuthPollResult =
5555
* Create a device auth request and return the code + URL for the user to visit.
5656
* Call this once, show the result to the user, then poll with pollDeviceAuth().
5757
*
58-
* Note: the server returns a generic `/device-auth?code=...` URL in `verificationUrl`,
59-
* but we construct our own landing URL pointing at `/openclaw-advisor?code=...`.
60-
* The cloud side uses the path prefix to attribute Security Advisor signups and
61-
* layer a per-product signup bonus on top of the standard welcome credits.
62-
* Old plugin builds keep working against the server — they just land on the generic
63-
* URL and don't qualify for the bonus, which is the intended behavior.
58+
* The server returns a generic `/device-auth?code=...` URL in `verificationUrl`,
59+
* built from APP_URL (the user-facing host, e.g. https://app.kilo.ai in prod).
60+
* We rewrite only the PATH to `/openclaw-advisor?code=...`, keeping the origin
61+
* authoritative. Rebuilding the URL from `apiBase` would be wrong in production,
62+
* where the API host (https://api.kilo.ai) and the app host (https://app.kilo.ai)
63+
* are different — the user needs the app host to land on the signup flow.
64+
*
65+
* The cloud side uses the `/openclaw-advisor` path prefix to attribute Security
66+
* Advisor signups and layer a per-product signup bonus on top of the standard
67+
* welcome credits. Old plugin builds keep working against the server — they just
68+
* land on the generic `/device-auth` URL and don't qualify for the bonus, which
69+
* is the intended behavior.
6470
*/
6571
export async function startDeviceAuth(
6672
apiBase: string,
@@ -76,10 +82,12 @@ export async function startDeviceAuth(
7682
);
7783
}
7884
const data = (await resp.json()) as DeviceAuthInitResponse;
85+
const advisorUrl = new URL(data.verificationUrl);
86+
advisorUrl.pathname = "/openclaw-advisor";
7987
return {
8088
kind: "started",
8189
code: data.code,
82-
verificationUrl: `${apiBase}/openclaw-advisor?code=${encodeURIComponent(data.code)}`,
90+
verificationUrl: advisorUrl.toString(),
8391
expiresIn: data.expiresIn,
8492
};
8593
}

test/device-auth.test.ts

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,7 @@ function stubFetch(response: unknown, { ok = true, status = 200 } = {}): void {
2424
}
2525

2626
describe("startDeviceAuth", () => {
27-
test("constructs the openclaw-advisor verification URL from the returned code, ignoring the server-returned verificationUrl", async () => {
28-
// Server returns a legacy /device-auth?code=... URL — plugin should ignore it.
27+
test("rewrites the path on the server-provided URL to /openclaw-advisor", async () => {
2928
stubFetch({
3029
code: "ABCD-1234",
3130
verificationUrl: "https://app.kilo.ai/device-auth?code=ABCD-1234",
@@ -42,7 +41,24 @@ describe("startDeviceAuth", () => {
4241
expect(result.expiresIn).toBe(600);
4342
});
4443

45-
test("uses the caller-provided apiBase for the verification URL (dev loop)", async () => {
44+
test("preserves the server-provided origin, not apiBase, when they differ (prod)", async () => {
45+
// Regression: in production, apiBase is the API host (api.kilo.ai) but
46+
// the server builds verificationUrl from APP_URL (app.kilo.ai). Rebuilding
47+
// the link from apiBase would send users to a nonexistent endpoint.
48+
stubFetch({
49+
code: "QWER-7890",
50+
verificationUrl: "https://app.kilo.ai/device-auth?code=QWER-7890",
51+
expiresIn: 600,
52+
});
53+
54+
const result = await startDeviceAuth("https://api.kilo.ai");
55+
56+
expect(result.verificationUrl).toBe(
57+
"https://app.kilo.ai/openclaw-advisor?code=QWER-7890",
58+
);
59+
});
60+
61+
test("preserves the dev-loop origin (host.docker.internal / localhost)", async () => {
4662
stubFetch({
4763
code: "WXYZ-5678",
4864
verificationUrl:
@@ -57,19 +73,20 @@ describe("startDeviceAuth", () => {
5773
);
5874
});
5975

60-
test("url-encodes the code to defend against unexpected server responses", async () => {
61-
// Defense-in-depth: even if the server returned a malformed code, we must
62-
// not inject unescaped query chars into the verification URL.
76+
test("preserves the ?code= query verbatim from the server-provided URL", async () => {
77+
// The server is the source of truth for the query string. We only swap
78+
// the pathname; we never reconstruct the query from the bare `code` field.
6379
stubFetch({
64-
code: "A&B=C D",
65-
verificationUrl: "ignored",
80+
code: "UVWX-9999",
81+
verificationUrl:
82+
"https://app.kilo.ai/device-auth?code=UVWX-9999&state=extra",
6683
expiresIn: 600,
6784
});
6885

69-
const result = await startDeviceAuth("https://app.kilo.ai");
86+
const result = await startDeviceAuth("https://api.kilo.ai");
7087

7188
expect(result.verificationUrl).toBe(
72-
"https://app.kilo.ai/openclaw-advisor?code=A%26B%3DC%20D",
89+
"https://app.kilo.ai/openclaw-advisor?code=UVWX-9999&state=extra",
7390
);
7491
});
7592

0 commit comments

Comments
 (0)