| 1 | import type { SdkCredentialStore } from "@cursor/sdk"; |
| 2 | import { ProviderSetupError, type ProviderInstanceId } from "@t3tools/contracts"; |
| 3 | import * as Effect from "effect/Effect"; |
| 4 | import * as FileSystem from "effect/FileSystem"; |
| 5 | import * as Option from "effect/Option"; |
| 6 | import * as Schema from "effect/Schema"; |
| 7 | |
| 8 | import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost"; |
| 9 | |
| 10 | const 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 | |
| 21 | const decodeCredentials = Schema.decodeUnknownEffect(Credentials); |
| 22 | const 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 | */ |
| 29 | export 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 | }); |