Skip to content

Commit 6be66ec

Browse files
committed
feat(openclaw-security-advisor) adds unique signup path for tracking and promotions
1 parent acd6a9a commit 6be66ec

3 files changed

Lines changed: 92 additions & 2 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ KiloCode account:
132132
To run a security checkup, connect your KiloCode account.
133133
134134
1. Open this URL in your browser:
135-
https://app.kilo.ai/device-auth?code=XXXX-XXXX
135+
https://app.kilo.ai/openclaw-advisor?code=XXXX-XXXX
136136
137137
2. Enter this code: XXXX-XXXX
138138

src/auth/device-auth.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,13 @@ export type DeviceAuthPollResult =
5454
/**
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().
57+
*
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.
5764
*/
5865
export async function startDeviceAuth(
5966
apiBase: string,
@@ -72,7 +79,7 @@ export async function startDeviceAuth(
7279
return {
7380
kind: "started",
7481
code: data.code,
75-
verificationUrl: data.verificationUrl,
82+
verificationUrl: `${apiBase}/openclaw-advisor?code=${encodeURIComponent(data.code)}`,
7683
expiresIn: data.expiresIn,
7784
};
7885
}

test/device-auth.test.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
2+
import { startDeviceAuth } from "../src/auth/device-auth";
3+
4+
let originalFetch: typeof globalThis.fetch;
5+
6+
beforeEach(() => {
7+
originalFetch = globalThis.fetch;
8+
});
9+
10+
afterEach(() => {
11+
globalThis.fetch = originalFetch;
12+
});
13+
14+
function stubFetch(response: unknown, { ok = true, status = 200 } = {}): void {
15+
const stub: typeof fetch = async () =>
16+
new Response(JSON.stringify(response), {
17+
status,
18+
headers: { "Content-Type": "application/json" },
19+
});
20+
// ok is derived from status in Response, so we rely on status here.
21+
// Allow callers to force ok=false by passing status>=400.
22+
void ok;
23+
globalThis.fetch = stub;
24+
}
25+
26+
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.
29+
stubFetch({
30+
code: "ABCD-1234",
31+
verificationUrl: "https://app.kilo.ai/device-auth?code=ABCD-1234",
32+
expiresIn: 600,
33+
});
34+
35+
const result = await startDeviceAuth("https://app.kilo.ai");
36+
37+
expect(result.kind).toBe("started");
38+
expect(result.code).toBe("ABCD-1234");
39+
expect(result.verificationUrl).toBe(
40+
"https://app.kilo.ai/openclaw-advisor?code=ABCD-1234",
41+
);
42+
expect(result.expiresIn).toBe(600);
43+
});
44+
45+
test("uses the caller-provided apiBase for the verification URL (dev loop)", async () => {
46+
stubFetch({
47+
code: "WXYZ-5678",
48+
verificationUrl:
49+
"http://host.docker.internal:3000/device-auth?code=WXYZ-5678",
50+
expiresIn: 600,
51+
});
52+
53+
const result = await startDeviceAuth("http://host.docker.internal:3000");
54+
55+
expect(result.verificationUrl).toBe(
56+
"http://host.docker.internal:3000/openclaw-advisor?code=WXYZ-5678",
57+
);
58+
});
59+
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.
63+
stubFetch({
64+
code: "A&B=C D",
65+
verificationUrl: "ignored",
66+
expiresIn: 600,
67+
});
68+
69+
const result = await startDeviceAuth("https://app.kilo.ai");
70+
71+
expect(result.verificationUrl).toBe(
72+
"https://app.kilo.ai/openclaw-advisor?code=A%26B%3DC%20D",
73+
);
74+
});
75+
76+
test("throws a descriptive error when the server rejects the request", async () => {
77+
stubFetch({}, { status: 500 });
78+
79+
await expect(startDeviceAuth("https://app.kilo.ai")).rejects.toThrow(
80+
/HTTP 500/,
81+
);
82+
});
83+
});

0 commit comments

Comments
 (0)