1import type { SdkCredentialStore } from "@cursor/sdk";
2import { ProviderSetupError, type ProviderInstanceId } from "@t3tools/contracts";
3import * as Effect from "effect/Effect";
4import * as FileSystem from "effect/FileSystem";
5import * as Option from "effect/Option";
6import * as Schema from "effect/Schema";
7
8import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost";
9
10const Credentials = Schema.fromJsonString(
11 Schema.Struct({
12 version: Schema.Literal(1),
13 backendUrl: Schema.String,
14 apiKey: Schema.String,
15 createdAtMs: Schema.Finite,
16 apiKeyExpiresAtMs: Schema.optionalKey(Schema.Finite),
17 email: Schema.optionalKey(Schema.String),
18 }),
19);
20
21const decodeCredentials = Schema.decodeUnknownEffect(Credentials);
22const encodeCredentials = Schema.encodeEffect(Credentials);
23
24/**
25 * The SDK owns the credential format; persistence uses the environment's secret store.
26 * `legacyFile` is the SDK file store earlier versions used; its sign-in moves into
27 * the secret store once and the file is deleted.
28 */
29export const makeCursorCredentialStore = Effect.fn("makeCursorCredentialStore")(function* (
30 instanceId: ProviderInstanceId,
31 legacyFile?: string,
32) {
33 const credentials = yield* (yield* ProviderHost.ProviderHost).credentials("cursor", instanceId);
34 if (legacyFile !== undefined) {
35 const fileSystem = yield* FileSystem.FileSystem;
36 yield* Effect.gen(function* () {
37 const legacy = yield* fileSystem.readFileString(legacyFile).pipe(Effect.option);
38 if (Option.isNone(legacy)) return;
39 // A stored sign-in wins unless it is damaged; the SDK would ignore it anyway.
40 const stored = yield* credentials.get;
41 const storedIsValid =
42 Option.isSome(stored) &&
43 Option.isSome(
44 yield* decodeCredentials(new TextDecoder().decode(stored.value)).pipe(Effect.option),
45 );
46 if (
47 !storedIsValid &&
48 Option.isSome(yield* decodeCredentials(legacy.value).pipe(Effect.option))
49 ) {
50 yield* credentials.set(new TextEncoder().encode(legacy.value));
51 }
52 yield* fileSystem.remove(legacyFile);
53 }).pipe(
54 Effect.catch((error) =>
55 Effect.logWarning("Could not migrate legacy Cursor credentials", error),
56 ),
57 );
58 }
59 const store: SdkCredentialStore = {
60 load: () =>
61 Effect.runPromise(
62 credentials.get.pipe(
63 Effect.flatMap(
64 Option.match({
65 onNone: () => Effect.succeed(undefined),
66 onSome: (bytes) =>
67 decodeCredentials(new TextDecoder().decode(bytes)).pipe(
68 Effect.orElseSucceed(() => undefined),
69 ),
70 }),
71 ),
72 ),
73 ),
74 save: (value) =>
75 Effect.runPromise(
76 encodeCredentials(value).pipe(
77 Effect.mapError(
78 () =>
79 new ProviderSetupError({
80 instanceId,
81 operation: "credentials",
82 detail: "Cursor returned an unsupported credential format.",
83 }),
84 ),
85 Effect.flatMap((encoded) => credentials.set(new TextEncoder().encode(encoded))),
86 ),
87 ),
88 clear: () => Effect.runPromise(credentials.remove),
89 };
90 return { store, binding: credentials.binding };
91});