1import type { SdkLoginOptions, StoredSdkCredentials } from "@cursor/sdk";
2import * as NodeServices from "@effect/platform-node/NodeServices";
3import { expect, it } from "@effect/vitest";
4import { ProviderInstanceId, ProviderSetupError, type ProviderAuthState } from "@t3tools/contracts";
5import * as Clock from "effect/Clock";
6import * as Deferred from "effect/Deferred";
7import * as Effect from "effect/Effect";
8import * as Exit from "effect/Exit";
9import * as Fiber from "effect/Fiber";
10import * as Option from "effect/Option";
11import * as Scope from "effect/Scope";
12import * as Stream from "effect/Stream";
13import * as TestClock from "effect/testing/TestClock";
14
15import { makeCursorAuth, type CursorAuthOptions } from "./auth.ts";
16import { InMemoryCredentialStore } from "./sdk.ts";
17
18const instanceId = ProviderInstanceId.make("cursor-personal");
19const owner = "owner-client";
20const otherOwner = "other-client";
21const authorizationUrl = "https://cursor.com/loginDeepControl?challenge=test-challenge";
22
23const makeHarness = Effect.fn("makeCursorAuthHarness")(function* (
24 overrides: Partial<CursorAuthOptions> = {},
25) {
26 const store = new InMemoryCredentialStore();
27 const complete = Promise.withResolvers<void>();
28 const returned = Promise.withResolvers<void>();
29 const now = yield* Clock.currentTimeMillis;
30 const credentials: StoredSdkCredentials = {
31 version: 1,
32 backendUrl: "https://api2.cursor.sh",
33 apiKey: "browser-key",
34 createdAtMs: now,
35 apiKeyExpiresAtMs: now + 86_400_000,
36 email: "cursor@example.com",
37 };
38 let loginOptions: SdkLoginOptions | undefined;
39 const changes: boolean[] = [];
40 const auth = yield* makeCursorAuth({
41 instanceId,
42 displayName: "Personal Cursor",
43 enabled: true,
44 store,
45 onChanged: (signedIn) =>
46 Effect.sync(() => {
47 changes.push(signedIn);
48 }),
49 login: async (options) => {
50 loginOptions = options;
51 options.onLoginUrl?.(authorizationUrl);
52 await complete.promise;
53 await options.store?.save(credentials);
54 returned.resolve();
55 return {
56 apiKey: credentials.apiKey,
57 apiKeyExpiresAtMs: credentials.apiKeyExpiresAtMs!,
58 email: credentials.email!,
59 };
60 },
61 ...overrides,
62 });
63 return {
64 auth,
65 store,
66 complete,
67 returned,
68 credentials,
69 changes,
70 loginOptions: () => loginOptions,
71 };
72});
73
74type Auth = Effect.Success<ReturnType<typeof makeCursorAuth>>;
75const phase = (auth: Auth, phase: ProviderAuthState["phase"], session = owner) =>
76 auth.controller.subscribe(session).pipe(
77 Stream.filter((state) => state.phase === phase),
78 Stream.runHead,
79 Effect.map(Option.getOrThrow),
80 );
81
82it.layer(NodeServices.layer)("CursorAuth", (it) => {
83 it.effect("closes SDK sessions and interrupts in-flight credential work before signing out", () =>
84 Effect.gen(function* () {
85 const harness = yield* makeHarness();
86 yield* Effect.promise(() => harness.store.save(harness.credentials));
87 const acquired = yield* Deferred.make<void>();
88 let closedSession = false;
89 let closedRequest = false;
90 yield* harness.auth.withAccess(
91 Effect.acquireRelease(Effect.succeed("session"), () =>
92 Effect.sync(() => {
93 closedSession = true;
94 }),
95 ),
96 );
97 const request = yield* harness.auth
98 .withAccess(
99 Effect.gen(function* () {
100 yield* Effect.addFinalizer(() =>
101 Effect.sync(() => {
102 closedRequest = true;
103 }),
104 );
105 yield* Deferred.succeed(acquired, undefined);
106 return yield* Effect.never;
107 }),
108 )
109 .pipe(Effect.forkScoped);
110 yield* Deferred.await(acquired);
111 yield* harness.auth.controller.logout(Effect.void);
112 expect(closedSession).toBe(true);
113 expect(closedRequest).toBe(true);
114 expect(Exit.isFailure(yield* Fiber.await(request))).toBe(true);
115 expect(yield* harness.auth.readApiKey).toBeUndefined();
116 }).pipe(Effect.scoped),
117 );
118
119 it.effect("completes a remote browser login, saves its key and refreshes provider status", () =>
120 Effect.gen(function* () {
121 const harness = yield* makeHarness();
122 let stopped = false;
123 expect(yield* harness.auth.readApiKey).toBeUndefined();
124 const starting = yield* harness.auth.controller.start(
125 owner,
126 Effect.sync(() => {
127 stopped = true;
128 }),
129 );
130 const waiting = yield* phase(harness.auth, "waiting");
131 expect(stopped).toBe(true);
132 expect(waiting.authorizationUrl).toBe(authorizationUrl);
133 expect(waiting.flowId).toBe(starting.flowId);
134 expect(harness.loginOptions()?.openBrowser).toBe(false);
135 expect((yield* harness.auth.controller.start(owner)).flowId).toBe(starting.flowId);
136 expect(Exit.isFailure(yield* Effect.exit(harness.auth.requireApiKey))).toBe(true);
137 yield* Effect.sync(() => harness.complete.resolve());
138 const succeeded = yield* phase(harness.auth, "succeeded");
139 expect(succeeded.authorizationUrl).toBeNull();
140 expect(yield* harness.auth.requireApiKey).toBe("browser-key");
141 expect(harness.changes).toEqual([true]);
142 expect(Object.values(succeeded)).not.toContain("browser-key");
143 expect(yield* Effect.promise(() => harness.store.load())).toEqual(harness.credentials);
144 }).pipe(Effect.scoped),
145 );
146
147 it.effect("keeps the login link private to its owner and rejects other clients' mutations", () =>
148 Effect.gen(function* () {
149 const { auth } = yield* makeHarness();
150 yield* auth.controller.start(owner);
151 const waiting = yield* phase(auth, "waiting");
152 const foreign = yield* phase(auth, "waiting", otherOwner);
153 expect(foreign).toMatchObject({ flowId: null, authorizationUrl: null, expiresAt: null });
154 expect(Exit.isFailure(yield* Effect.exit(auth.controller.start(otherOwner)))).toBe(true);
155 expect(
156 Exit.isFailure(yield* Effect.exit(auth.controller.cancel(otherOwner, waiting.flowId!))),
157 ).toBe(true);
158 expect(
159 Exit.isFailure(
160 yield* Effect.exit(
161 auth.controller.complete(owner, {
162 flowId: waiting.flowId!,
163 callbackUrl: "http://localhost/callback",
164 }),
165 ),
166 ),
167 ).toBe(true);
168 expect((yield* phase(auth, "waiting")).authorizationUrl).toBe(authorizationUrl);
169 }).pipe(Effect.scoped),
170 );
171
172 it.effect("aborts cancellation and ignores a late successful SDK login", () =>
173 Effect.gen(function* () {
174 const harness = yield* makeHarness();
175 yield* harness.auth.controller.start(owner);
176 const waiting = yield* phase(harness.auth, "waiting");
177 const cancelled = yield* harness.auth.controller.cancel(owner, waiting.flowId!);
178 expect(cancelled.phase).toBe("cancelled");
179 expect(cancelled.authorizationUrl).toBeNull();
180 expect(harness.loginOptions()?.signal?.aborted).toBe(true);
181 yield* Effect.sync(() => harness.complete.resolve());
182 yield* Effect.promise(() => harness.returned.promise);
183 expect(yield* harness.auth.readApiKey).toBeUndefined();
184 expect(harness.changes).toEqual([]);
185 }).pipe(Effect.scoped),
186 );
187
188 it.effect("times out pending logins and permits a retry", () =>
189 Effect.gen(function* () {
190 const harness = yield* makeHarness();
191 yield* harness.auth.controller.start(owner);
192 const waiting = yield* phase(harness.auth, "waiting");
193 yield* TestClock.adjust(300_000);
194 expect((yield* phase(harness.auth, "failed")).authorizationUrl).toBeNull();
195 expect(harness.loginOptions()?.signal?.aborted).toBe(true);
196 const retry = yield* harness.auth.controller.start(owner);
197 expect(retry.flowId).not.toBe(waiting.flowId);
198 }).pipe(Effect.scoped),
199 );
200
201 it.effect("finishes saving a completed login atomically across the timeout", () =>
202 Effect.gen(function* () {
203 const store = new InMemoryCredentialStore();
204 const saving = Promise.withResolvers<void>();
205 const finishSave = Promise.withResolvers<void>();
206 const scope = yield* Scope.make();
207 yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
208 const harness = yield* makeHarness({
209 store: {
210 load: () => store.load(),
211 clear: () => store.clear(),
212 save: async (credentials) => {
213 saving.resolve();
214 await finishSave.promise;
215 await store.save(credentials);
216 },
217 },
218 }).pipe(Effect.provideService(Scope.Scope, scope));
219 yield* Effect.addFinalizer(() => Effect.sync(() => finishSave.resolve()));
220 yield* harness.auth.controller.start(owner);
221 yield* phase(harness.auth, "waiting");
222 yield* Effect.sync(() => harness.complete.resolve());
223 yield* Effect.promise(() => saving.promise);
224 yield* TestClock.adjust(300_000);
225 yield* Effect.sync(() => finishSave.resolve());
226 yield* phase(harness.auth, "succeeded");
227 expect(yield* harness.auth.requireApiKey).toBe("browser-key");
228 yield* Scope.close(scope, Exit.void);
229 const state = yield* harness.auth.controller.subscribe(owner).pipe(Stream.runHead);
230 expect(Option.getOrThrow(state).phase).toBe("succeeded");
231 expect(harness.changes).toEqual([true]);
232 }).pipe(Effect.scoped),
233 );
234
235 it.effect("discards a credential rejected by verification and permits a fresh login", () =>
236 Effect.gen(function* () {
237 let rejected = true;
238 const harness = yield* makeHarness({
239 onChanged: (signedIn) =>
240 signedIn && rejected
241 ? Effect.fail(
242 new ProviderSetupError({
243 instanceId,
244 operation: "start",
245 detail: "Could not verify the Cursor sign-in.",
246 }),
247 )
248 : Effect.void,
249 });
250 yield* harness.auth.controller.start(owner);
251 yield* phase(harness.auth, "waiting");
252 yield* Effect.sync(() => harness.complete.resolve());
253 yield* phase(harness.auth, "failed");
254 expect(yield* Effect.promise(() => harness.store.load())).toBeUndefined();
255 expect(yield* harness.auth.readApiKey).toBeUndefined();
256 expect(Exit.isFailure(yield* Effect.exit(harness.auth.requireApiKey))).toBe(true);
257 rejected = false;
258 yield* harness.auth.controller.start(owner);
259 yield* phase(harness.auth, "succeeded");
260 expect(yield* harness.auth.requireApiKey).toBe("browser-key");
261 }).pipe(Effect.scoped),
262 );
263
264 it.effect("clears only this instance's credential after its sessions stop", () =>
265 Effect.gen(function* () {
266 const harness = yield* makeHarness();
267 const other = yield* makeHarness({ instanceId: ProviderInstanceId.make("cursor-work") });
268 yield* Effect.promise(() => harness.store.save(harness.credentials));
269 yield* Effect.promise(() => other.store.save({ ...other.credentials, apiKey: "other-key" }));
270 const state = yield* harness.auth.controller.logout(
271 Effect.gen(function* () {
272 expect(yield* harness.auth.readApiKey).toBe("browser-key");
273 expect(Exit.isFailure(yield* Effect.exit(harness.auth.requireApiKey))).toBe(true);
274 }),
275 );
276 expect(state.message).toBe("Signed out of Cursor.");
277 expect(yield* harness.auth.readApiKey).toBeUndefined();
278 expect(yield* other.auth.requireApiKey).toBe("other-key");
279 expect(harness.changes).toEqual([false]);
280 }).pipe(Effect.scoped),
281 );
282
283 it.effect("cancels a pending sign-in on logout without saving its eventual key", () =>
284 Effect.gen(function* () {
285 const harness = yield* makeHarness();
286 yield* harness.auth.controller.start(owner);
287 yield* phase(harness.auth, "waiting");
288 yield* harness.auth.controller.logout(Effect.void);
289 expect(harness.loginOptions()?.signal?.aborted).toBe(true);
290 yield* Effect.sync(() => harness.complete.resolve());
291 yield* Effect.promise(() => harness.returned.promise);
292 expect(yield* harness.auth.readApiKey).toBeUndefined();
293 }).pipe(Effect.scoped),
294 );
295
296 it.effect("retains credentials if sessions cannot be stopped for logout", () =>
297 Effect.gen(function* () {
298 const harness = yield* makeHarness();
299 yield* Effect.promise(() => harness.store.save(harness.credentials));
300 const result = yield* Effect.exit(
301 harness.auth.controller.logout(
302 Effect.fail(
303 new ProviderSetupError({
304 instanceId,
305 operation: "stop",
306 detail: "Unable to stop sessions",
307 }),
308 ),
309 ),
310 );
311 expect(Exit.isFailure(result)).toBe(true);
312 expect(yield* harness.auth.requireApiKey).toBe("browser-key");
313 }).pipe(Effect.scoped),
314 );
315
316 it.effect(
317 "prefers explicit API keys and prevents browser login from silently overriding them",
318 () =>
319 Effect.gen(function* () {
320 const harness = yield* makeHarness({ apiKey: " explicit-key " });
321 yield* Effect.promise(() => harness.store.save(harness.credentials));
322 expect(yield* harness.auth.requireApiKey).toBe("explicit-key");
323 expect(Exit.isFailure(yield* Effect.exit(harness.auth.controller.start(owner)))).toBe(true);
324 expect(
325 Exit.isFailure(yield* Effect.exit(harness.auth.controller.logout(Effect.void))),
326 ).toBe(true);
327 expect(harness.loginOptions()).toBeUndefined();
328 expect((yield* Effect.promise(() => harness.store.load()))?.apiKey).toBe("browser-key");
329 }).pipe(Effect.scoped),
330 );
331
332 it.effect("ignores expired browser credentials and rejects disabled sign-in", () =>
333 Effect.gen(function* () {
334 const harness = yield* makeHarness({ enabled: false });
335 yield* Effect.promise(() =>
336 harness.store.save({ ...harness.credentials, apiKeyExpiresAtMs: 0 }),
337 );
338 expect(yield* harness.auth.readApiKey).toBeUndefined();
339 expect(Exit.isFailure(yield* Effect.exit(harness.auth.controller.start(owner)))).toBe(true);
340 }).pipe(Effect.scoped),
341 );
342
343 it.effect("aborts pending login when the provider instance is disposed", () =>
344 Effect.gen(function* () {
345 const scope = yield* Scope.make();
346 const harness = yield* makeHarness().pipe(Effect.provideService(Scope.Scope, scope));
347 yield* harness.auth.controller.start(owner);
348 yield* phase(harness.auth, "waiting");
349 yield* Scope.close(scope, Exit.void);
350 expect(harness.loginOptions()?.signal?.aborted).toBe(true);
351 yield* Effect.sync(() => harness.complete.resolve());
352 yield* Effect.promise(() => harness.returned.promise);
353 expect(yield* harness.auth.readApiKey).toBeUndefined();
354 }),
355 );
356});