Skip to content
Source
Sign inSign up

credentialStore.ts

111 lines4.0 KB

1import type { SdkCredentialStore } from "@cursor/sdk";
2import { ProviderSetupError, type ProviderInstanceId } from "@t3tools/contracts";
3import * as Clock from "effect/Clock";
4import * as Effect from "effect/Effect";
5import * as FileSystem from "effect/FileSystem";
6import * as Option from "effect/Option";
7import * as Schema from "effect/Schema";
8​
9import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost";
10​
11const Credentials = Schema.fromJsonString(
12 Schema.Struct({
13 version: Schema.Literal(1),
14 backendUrl: Schema.String,
15 apiKey: Schema.String,
16 createdAtMs: Schema.Finite,
17 apiKeyExpiresAtMs: Schema.optionalKey(Schema.Finite),
18 email: Schema.optionalKey(Schema.String),
19 }),
20);
21​
22const decodeCredentials = Schema.decodeUnknownEffect(Credentials);
23const encodeCredentials = Schema.encodeEffect(Credentials);
24​
25export const makeCursorCredentialStore = Effect.fn("makeCursorCredentialStore")(function* (
26 instanceId: ProviderInstanceId,
27 legacyFile?: string,
28) {
29 const credentials = yield* (yield* ProviderHost.ProviderHost).credentials("cursor", instanceId);
30 if (legacyFile !== undefined) {
31 const fileSystem = yield* FileSystem.FileSystem;
32 yield* Effect.gen(function* () {
33 const legacy = yield* fileSystem.readFileString(legacyFile).pipe(Effect.option);
34 if (Option.isNone(legacy)) return;
35 const stored = yield* credentials.get;
36 const storedIsValid =
37 Option.isSome(stored) &&
38 Option.isSome(
39 yield* decodeCredentials(new TextDecoder().decode(stored.value)).pipe(Effect.option),
40 );
41 if (
42 !storedIsValid &&
43 Option.isSome(yield* decodeCredentials(legacy.value).pipe(Effect.option))
44 ) {
45 yield* credentials.set(new TextEncoder().encode(legacy.value));
46 }
47 yield* fileSystem.remove(legacyFile);
48 }).pipe(
49 Effect.catch((error) =>
50 Effect.logWarning("Could not migrate legacy Cursor credentials", error),
51 ),
52 );
53 }
54 const runPromise = Effect.runPromiseWith(yield* Effect.context<never>());
55 const store: SdkCredentialStore = {
56 load: () =>
57 runPromise(
58 credentials.get.pipe(
59 Effect.flatMap(
60 Option.match({
61 onNone: () => Effect.undefined,
62 onSome: (bytes) =>
63 decodeCredentials(new TextDecoder().decode(bytes)).pipe(
64 Effect.orElseSucceed(() => undefined),
65 ),
66 }),
67 ),
68 ),
69 ),
70 // The SDK can supply undefined optional fields despite its exact optional types.
71 save: ({ email, apiKeyExpiresAtMs, ...requiredFields }) =>
72 runPromise(
73 encodeCredentials({
74 ...requiredFields,
75 ...(email === undefined ? {} : { email }),
76 ...(apiKeyExpiresAtMs === undefined ? {} : { apiKeyExpiresAtMs }),
77 }).pipe(
78 Effect.mapError(
79 () =>
80 new ProviderSetupError({
81 instanceId,
82 operation: "credentials",
83 detail: "Cursor returned an unsupported credential format.",
84 }),
85 ),
86 Effect.flatMap((encoded) => credentials.set(new TextEncoder().encode(encoded))),
87 ),
88 ),
89 clear: () => runPromise(credentials.remove),
90 };
91 return { store, binding: credentials.binding };
92});
93​
94/** The unexpired API key from this instance's T3 sign-in, read without loading the SDK. */
95export const readStoredCursorApiKey = Effect.fn("readStoredCursorApiKey")(function* (
96 instanceId: ProviderInstanceId,
97) {
98 const stored = yield* (yield* (yield* ProviderHost.ProviderHost).credentials(
99 "cursor",
100 instanceId,
101 )).get;
102 if (Option.isNone(stored)) return undefined;
103 const credentials = yield* decodeCredentials(new TextDecoder().decode(stored.value)).pipe(
104 Effect.option,
105 );
106 if (Option.isNone(credentials)) return undefined;
107 const { apiKey, apiKeyExpiresAtMs } = credentials.value;
108 return apiKeyExpiresAtMs === undefined || apiKeyExpiresAtMs > (yield* Clock.currentTimeMillis)
109 ? apiKey
110 : undefined;
111});