Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 27 additions & 6 deletions packages/provider-cursor/src/server/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,19 @@ interface AuthFlow {
fiber?: Fiber.Fiber<void>;
}

/** A Cursor login saved by T3's browser sign-in, as opposed to a configured API key. */
export type CursorStoredLogin =
| {
readonly status: "valid";
readonly apiKey: string;
readonly backendUrl: string;
}
| { readonly status: "expired" };

export interface CursorAuth {
readonly controller: ProviderAuthController;
readonly readApiKey: Effect.Effect<string | undefined, ProviderSetupError>;
readonly readStoredLogin: Effect.Effect<CursorStoredLogin | undefined, ProviderSetupError>;
readonly requireApiKey: Effect.Effect<string, ProviderSetupError>;
readonly usesApiKey: boolean;
readonly withAccess: <A, E, R>(
Expand Down Expand Up @@ -74,8 +84,7 @@ export const makeCursorAuth = Effect.fn("makeCursorAuth")(function* (options: Cu
state: { ...current.state, ...patch },
}));

const readApiKey = Effect.gen(function* () {
if (configuredKey) return configuredKey;
const readStoredLogin: CursorAuth["readStoredLogin"] = Effect.gen(function* () {
const credentials = yield* Effect.tryPromise({
try: () => options.store.load(),
catch: (cause) =>
Expand All @@ -86,13 +95,24 @@ export const makeCursorAuth = Effect.fn("makeCursorAuth")(function* (options: Cu
cause,
}),
});
if (!credentials) return undefined;
const now = yield* Clock.currentTimeMillis;
return credentials &&
(credentials.apiKeyExpiresAtMs === undefined || credentials.apiKeyExpiresAtMs > now)
? credentials.apiKey
: undefined;
if (credentials.apiKeyExpiresAtMs !== undefined && credentials.apiKeyExpiresAtMs <= now) {
return { status: "expired" };
}
return {
status: "valid",
apiKey: credentials.apiKey,
backendUrl: credentials.backendUrl,
};
});

const readApiKey = configuredKey
? Effect.succeed(configuredKey)
: readStoredLogin.pipe(
Effect.map((login) => (login?.status === "valid" ? login.apiKey : undefined)),
);

const requireApiKey = Effect.gen(function* () {
if (operation !== "idle") {
return yield* new ProviderSetupError({
Expand Down Expand Up @@ -497,6 +517,7 @@ export const makeCursorAuth = Effect.fn("makeCursorAuth")(function* (options: Cu
return {
controller,
readApiKey,
readStoredLogin,
requireApiKey,
withAccess,
usesApiKey: configuredKey !== undefined,
Expand Down
113 changes: 112 additions & 1 deletion packages/provider-cursor/src/server/driver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Stream from "effect/Stream";
import { vi } from "vite-plus/test";
import * as HttpClientResponse from "effect/http/HttpClientResponse";
import * as HttpClient from "effect/http/HttpClient";

import {
Expand All @@ -29,6 +30,7 @@ import {
import * as ProviderEventLoggers from "@t3tools/provider-core/server/ProviderEventLoggers";
import * as TestProviderHost from "@t3tools/provider-testing/TestProviderHost";
import { CursorDriver } from "./driver.ts";
import { makeCursorCredentialStore } from "./credentialStore.ts";
import * as CursorAgentSdk from "./CursorAgentSdk.ts";
import * as CursorSdk from "./CursorSdk.ts";
import * as CursorKeychain from "./CursorKeychain.ts";
Expand Down Expand Up @@ -107,7 +109,30 @@ it.layer(layerTest)("CursorDriver", (it) => {
};
const openedKeys: Array<string | undefined> = [];
let closed = 0;
const requests: string[] = [];
let failureUrl: string | undefined;
let failureStatus = 500;
const usageClient = HttpClient.make((request) => {
requests.push(request.url);
const exchange = request.url.endsWith("/auth/exchange_user_api_key");
expect(request.headers.authorization).toBe(
exchange ? "Bearer instance-browser-key" : "Bearer browser-access-token",
);
return Effect.succeed(
HttpClientResponse.fromWeb(
request,
request.url === failureUrl
? new Response(null, { status: failureStatus })
: Response.json(
exchange
? { accessToken: "browser-access-token" }
: { planUsage: { totalPercentUsed: 23 } },
),
),
);
});
const instance = yield* CursorDriver.create(input).pipe(
Effect.provideService(HttpClient.HttpClient, usageClient),
Effect.provideService(CursorAgentSdk.CursorAgentSdkRunner, {
assertComplete: Effect.void,
open: (request) =>
Expand All @@ -134,6 +159,7 @@ it.layer(layerTest)("CursorDriver", (it) => {
expect(me).toHaveBeenCalledWith({ apiKey: "instance-browser-key" });
expect(yield* instance.snapshot.getSnapshot).toMatchObject({
status: "ready",
usageLimits: { windows: [{ id: "totalPercentUsed", usedPercent: 23 }] },
auth: {
status: "authenticated",
type: "browser",
Expand All @@ -158,8 +184,43 @@ it.layer(layerTest)("CursorDriver", (it) => {
yield* runtime.ensureThread({ threadId, modelSelection, runtimePolicy });
expect(openedKeys).toEqual(["instance-browser-key"]);
expect(closed).toBe(0);
const recreated = yield* CursorDriver.create(input);
expect(requests.slice(0, 2)).toEqual([
"https://api2.cursor.sh/auth/exchange_user_api_key",
"https://api2.cursor.sh/aiserver.v1.DashboardService/GetCurrentPeriodUsage",
]);
const recreated = yield* CursorDriver.create(input).pipe(
Effect.provideService(HttpClient.HttpClient, usageClient),
);
expect((yield* recreated.snapshot.refresh).auth.status).toBe("authenticated");
for (const url of requests.slice(0, 2)) {
failureUrl = url;
const snapshot = yield* instance.snapshot.refresh;
expect(snapshot).toMatchObject({
status: "ready",
auth: { status: "authenticated" },
usageLimits: { windows: [{ id: "totalPercentUsed", usedPercent: 23 }] },
});
expect(snapshot.message).toBeUndefined();
}
failureUrl = requests[0];
failureStatus = 401;
expect(yield* instance.snapshot.refresh).toMatchObject({
status: "ready",
auth: { status: "authenticated" },
usageLimits: { windows: [], unavailable: { reason: "unsupported" } },
});
failureUrl = undefined;
expect((yield* instance.snapshot.refresh).usageLimits?.windows[0]?.usedPercent).toBe(23);
const { store } = yield* makeCursorCredentialStore(input.instanceId);
yield* Effect.promise(async () => {
const credentials = await store.load();
await store.save({ ...credentials!, apiKeyExpiresAtMs: 0 });
});
const requestCount = requests.length;
const expired = yield* instance.snapshot.refresh;
expect(expired.auth.status).toBe("unauthenticated");
expect(expired.usageLimits).toBeUndefined();
expect(requests).toHaveLength(requestCount);
yield* instance.auth!.logout(Effect.void);
expect(closed).toBe(1);
expect((yield* instance.snapshot.getSnapshot).auth.status).toBe("unauthenticated");
Expand All @@ -173,6 +234,56 @@ it.layer(layerTest)("CursorDriver", (it) => {
}).pipe(Effect.scoped),
);

it.effect(
"keeps a configured CURSOR_API_KEY instance off its stored browser login for limits",
() =>
Effect.gen(function* () {
const me = vi.spyOn(Cursor, "me").mockResolvedValue({
apiKeyName: "Configured key",
createdAt: "2026-01-01T00:00:00.000Z",
});
const models = vi.spyOn(Cursor.models, "list").mockResolvedValue([]);
yield* Effect.addFinalizer(() =>
Effect.sync(() => {
me.mockRestore();
models.mockRestore();
}),
);
const instanceId = ProviderInstanceId.make("cursor-configured-key-with-login");
const { store } = yield* makeCursorCredentialStore(instanceId);
yield* Effect.promise(() =>
store.save({
version: 1,
backendUrl: "https://api2.cursor.sh",
apiKey: "stored-browser-key",
createdAtMs: 0,
apiKeyExpiresAtMs: 4_000_000_000_000,
}),
);
const requests: string[] = [];
const instance = yield* CursorDriver.create({
instanceId,
displayName: "Configured Cursor",
enabled: true,
environment: [{ name: "CURSOR_API_KEY", value: "configured-key", sensitive: true }],
config: CursorDriver.defaultConfig(),
}).pipe(
Effect.provideService(
HttpClient.HttpClient,
HttpClient.make((request) => {
requests.push(request.url);
return Effect.die("must not exchange credentials or request usage");
}),
),
);
expect((yield* instance.snapshot.refresh).usageLimits?.unavailable?.reason).toBe(
"unsupported",
);
expect(requests).toEqual([]);
expect(me).toHaveBeenCalledWith({ apiKey: "configured-key" });
}).pipe(Effect.scoped),
);

it.effect("budgets handoffs for the SDK model across catalog refreshes and option changes", () =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
Expand Down
16 changes: 13 additions & 3 deletions packages/provider-cursor/src/server/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,9 +213,18 @@ export const CursorDriver: ProviderDriver<
auth.withAccess,
);

const checkProvider = auth.readApiKey.pipe(
Effect.orElseSucceed(() => undefined),
Effect.flatMap((apiKey) =>
const checkProvider = Effect.gen(function* () {
const storedLogin = auth.usesApiKey
? undefined
: yield* auth.readStoredLogin.pipe(Effect.orElseSucceed(() => undefined));
const apiKey = auth.usesApiKey
? yield* auth.readApiKey.pipe(Effect.orElseSucceed(() => undefined))
: storedLogin?.status === "valid"
? storedLogin.apiKey
: undefined;
return { storedLogin, apiKey };
}).pipe(
Effect.flatMap(({ storedLogin, apiKey }) =>
checkCursorProviderStatus(
effectiveConfig,
{
Expand All @@ -238,6 +247,7 @@ export const CursorDriver: ProviderDriver<
effectiveConfig,
{ ...processEnv, CURSOR_API_KEY: apiKey },
settings.cursorKeychainUsageEnabled,
storedLogin,
),
),
Effect.map((usageLimits) => ({ ...snapshot, usageLimits })),
Expand Down
100 changes: 100 additions & 0 deletions packages/provider-cursor/src/server/usageLimits.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";
import * as HttpClient from "effect/http/HttpClient";
import type * as HttpClientRequest from "effect/http/HttpClientRequest";
import * as HttpClientResponse from "effect/http/HttpClientResponse";
import * as CursorKeychain from "./CursorKeychain.ts";
import { cursorUsageResponseToLimits, readCursorUsageLimits } from "./usageLimits.ts";
Expand Down Expand Up @@ -268,4 +269,103 @@ describe("Cursor usage limits", () => {
}
}),
);

describe("T3 browser login", () => {
const exchangeUrl = "https://instance.example/auth/exchange_user_api_key";
const usageUrl = "https://instance.example/aiserver.v1.DashboardService/GetCurrentPeriodUsage";
const login = {
status: "valid",
apiKey: "stored-browser-key",
backendUrl: "https://instance.example/path",
} as const;
// A stored login names this instance's account, so host CLI files and Keychain must stay untouched.
const readWithLogin = (
storedLogin: Parameters<typeof readCursorUsageLimits>[3],
respond: (request: HttpClientRequest.HttpClientRequest) => Response,
) => {
const requests: Array<{
url: string;
authorization: string | undefined;
body: string | null;
}> = [];
const read = withNodeServices(
readCursorUsageLimits(
{ apiEndpoint: "https://settings.example" },
{ CURSOR_AUTH_TOKEN: "other-host-account-token" },
true,
storedLogin,
).pipe(
Effect.provideService(HostProcess.Platform, "darwin"),
Effect.provideService(
FileSystem.FileSystem,
FileSystem.makeNoop({
readFileString: () => Effect.die("must not read the host CLI login"),
}),
),
Effect.provideService(
HttpClient.HttpClient,
HttpClient.make((request) => {
requests.push({
url: request.url,
authorization: request.headers.authorization,
body:
request.body._tag === "Uint8Array"
? new TextDecoder().decode(request.body.body)
: null,
});
return Effect.succeed(HttpClientResponse.fromWeb(request, respond(request)));
}),
),
),
);
return { read, requests };
};

it.effect(
"exchanges the stored key at the stored backend and reads usage with the result",
() =>
Effect.gen(function* () {
const { read, requests } = readWithLogin(login, (request) =>
request.url === exchangeUrl
? Response.json({ accessToken: "exchanged-access-token" })
: Response.json({ planUsage: { totalPercentUsed: 37 } }),
);
expect((yield* read).windows[0]?.usedPercent).toBe(37);
expect(requests).toEqual([
{ url: exchangeUrl, authorization: "Bearer stored-browser-key", body: "{}" },
{ url: usageUrl, authorization: "Bearer exchanged-access-token", body: "{}" },
]);
}),
);

it.effect(
"clears expired or rejected login usage and reports transient failures without using host logins",
() =>
Effect.gen(function* () {
const expired = readWithLogin({ status: "expired" }, () => {
throw new Error("must not request usage for an expired login");
});
const rejected = readWithLogin(login, () => new Response("rejected", { status: 401 }));
for (const { read } of [expired, rejected]) {
expect(yield* read).toMatchObject({
windows: [],
unavailable: {
reason: "unsupported",
message: expect.stringContaining("Sign in again"),
},
});
}
for (const failedUrl of [exchangeUrl, usageUrl]) {
const transient = readWithLogin(login, (request) =>
request.url === failedUrl
? new Response(null, { status: 500 })
: Response.json({ accessToken: "exchanged-access-token" }),
);
expect((yield* transient.read).unavailable?.reason).toBe("probeFailed");
}
expect(expired.requests).toEqual([]);
expect(rejected.requests.map((request) => request.url)).toEqual([exchangeUrl]);
}),
);
});
});
Loading
Loading