@velum-labs/cursorkit 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,8 @@
1
1
  import { type ChildProcess } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import { Command } from "commander";
4
- import { type LocalModelConfig } from "./config.js";
4
+ import { buildLocalDesktopModelEntry, mergeLocalAgentBackendUrlsIntoApplicationUser, mergeLocalDesktopModelsIntoApplicationUser } from "./cursorDesktopState.js";
5
+ export { buildLocalDesktopModelEntry, mergeLocalAgentBackendUrlsIntoApplicationUser, mergeLocalDesktopModelsIntoApplicationUser, };
5
6
  import { type DesktopConnectProxy } from "./desktopConnectProxy.js";
6
7
  export type CkCommand = "launch" | "test" | "doctor" | "cert" | "route" | "stop";
7
8
  export type CkProfileMode = "isolated" | "default";
@@ -155,9 +156,5 @@ export declare function registerCk(program: Command): void;
155
156
  export declare function chooseBridgePort(): Promise<number>;
156
157
  export declare function seedCursorAuthFromDefault(plan: Pick<CkLaunchPlan, "profileMode" | "userDataDir" | "seedAuthFromDefault">): CursorAuthSeedStatus;
157
158
  export declare function seedLocalModelsIntoCursorState(plan: Pick<CkLaunchPlan, "agentHttpPort" | "bridge" | "profileMode" | "userDataDir">): CursorLocalModelSeedStatus;
158
- export declare function mergeLocalAgentBackendUrlsIntoApplicationUser(applicationUser: Record<string, unknown>, agentOrigin: string): void;
159
- export declare function mergeLocalDesktopModelsIntoApplicationUser(applicationUser: Record<string, unknown>, models: LocalModelConfig[]): void;
160
- export declare function buildLocalDesktopModelEntry(model: LocalModelConfig): Record<string, unknown>;
161
159
  export declare function cleanupIsolatedCursorProcesses(userDataDir: string): number[];
162
160
  export declare function bridgeProcessMatchesState(command: string, state?: Pick<CkState, "bridgeCommand">): boolean;
163
- export {};
@@ -6,6 +6,10 @@ import path from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { Command, InvalidArgumentError } from "commander";
8
8
  import { loadConfig } from "./config.js";
9
+ import { buildLocalDesktopModelEntry, mergeLocalAgentBackendUrlsIntoApplicationUser, mergeLocalDesktopModelsIntoApplicationUser, } from "./cursorDesktopState.js";
10
+ // Re-exported for backwards compatibility (callers/tests import these from
11
+ // ckLauncher); the implementations now live in cursorDesktopState.
12
+ export { buildLocalDesktopModelEntry, mergeLocalAgentBackendUrlsIntoApplicationUser, mergeLocalDesktopModelsIntoApplicationUser, };
9
13
  import { startDesktopConnectProxy, } from "./desktopConnectProxy.js";
10
14
  import { DESKTOP_CERT_PATH, DESKTOP_HOSTNAME, DESKTOP_HOSTNAMES, DESKTOP_KEY_PATH, desktopCertificateStatus, desktopDnsStatus, desktopEnv, desktopTrustCommand, localModelBackendStatus, upstreamReachabilityStatus, writeDesktopCertificate, } from "./desktop.js";
11
15
  import { AGENT_RUN_PATH, AGENT_RUN_SSE_PATH, AVAILABLE_MODELS_PATH, BIDI_APPEND_PATH, GET_DEFAULT_MODEL_FOR_CLI_PATH, GET_USABLE_MODELS_PATH, STREAM_CHAT_WITH_TOOLS_PATH, } from "./routes.js";
@@ -767,101 +771,24 @@ export function seedLocalModelsIntoCursorState(plan) {
767
771
  if (typeof agentPublicOrigin === "string") {
768
772
  mergeLocalAgentBackendUrlsIntoApplicationUser(applicationUser, agentPublicOrigin);
769
773
  }
774
+ // The seed file mirrors the user's Cursor credentials (cursorCreds), so it is
775
+ // written with owner-only permissions and removed once sqlite3 has imported
776
+ // it rather than being left on disk.
770
777
  const seedPath = path.join(globalStorageDir, "applicationUser.seed.json");
771
- fs.writeFileSync(seedPath, JSON.stringify(applicationUser));
772
- const escapedSeedPath = seedPath.replaceAll("'", "''");
773
- const seed = spawnSync("sqlite3", [
774
- targetDb,
775
- `insert or replace into ItemTable(key, value) values('${key}', cast(readfile('${escapedSeedPath}') as text));`,
776
- ], { encoding: "utf8" });
777
- if (seed.status !== 0) {
778
- return "sqlite-unavailable";
779
- }
780
- return "seeded";
781
- }
782
- export function mergeLocalAgentBackendUrlsIntoApplicationUser(applicationUser, agentOrigin) {
783
- const cursorCreds = ensureRecord(applicationUser, "cursorCreds");
784
- const urls = { default: agentOrigin };
785
- cursorCreds.agentBackendUrlPrivacy = urls;
786
- cursorCreds.agentBackendUrlNonPrivacy = urls;
787
- }
788
- export function mergeLocalDesktopModelsIntoApplicationUser(applicationUser, models) {
789
- const localModelIds = new Set(models.map((model) => model.id));
790
- const current = Array.isArray(applicationUser.availableDefaultModels2)
791
- ? applicationUser.availableDefaultModels2.filter((item) => {
792
- if (!isPlainRecord(item) || typeof item.name !== "string") {
793
- return true;
794
- }
795
- return !localModelIds.has(item.name);
796
- })
797
- : [];
798
- applicationUser.availableDefaultModels2 = current;
799
- const aiSettings = ensureRecord(applicationUser, "aiSettings");
800
- for (const model of models) {
801
- appendUnique(ensureStringArray(aiSettings, "userAddedModels"), model.id);
802
- appendUnique(ensureStringArray(aiSettings, "modelOverrideEnabled"), model.id);
803
- removeValue(ensureStringArray(aiSettings, "modelOverrideDisabled"), model.id);
804
- }
805
- const preferences = ensureRecord(aiSettings, "modelParameterPreferences");
806
- const updatedAt = new Date().toISOString();
807
- for (const model of models) {
808
- preferences[model.id] = {
809
- modelId: model.id,
810
- parameters: localDesktopParameterValues(true),
811
- updatedAt,
812
- };
813
- }
814
- const firstModel = models[0];
815
- if (firstModel !== undefined) {
816
- applicationUser.useOpenAIKey = true;
817
- applicationUser.openAIBaseUrl = firstModel.baseUrl;
818
- applicationUser.openAIKey = firstModel.apiKey;
819
- const modelConfig = ensureRecord(aiSettings, "modelConfig");
820
- const selectedModel = {
821
- modelId: firstModel.id,
822
- parameters: localDesktopParameterValues(true),
823
- };
824
- for (const key of ["composer", "background-composer"]) {
825
- modelConfig[key] = {
826
- modelName: firstModel.id,
827
- maxMode: true,
828
- selectedModels: [selectedModel],
829
- };
830
- }
831
- }
832
- const featureModelConfigs = ensureRecord(applicationUser, "featureModelConfigs");
833
- for (const value of Object.values(featureModelConfigs)) {
834
- if (!isPlainRecord(value)) {
835
- continue;
836
- }
837
- const fallbackModels = ensureStringArray(value, "fallbackModels");
838
- for (const model of models) {
839
- appendUnique(fallbackModels, model.id);
778
+ fs.writeFileSync(seedPath, JSON.stringify(applicationUser), { mode: 0o600 });
779
+ try {
780
+ const escapedSeedPath = seedPath.replaceAll("'", "''");
781
+ const seed = spawnSync("sqlite3", [
782
+ targetDb,
783
+ `insert or replace into ItemTable(key, value) values('${key}', cast(readfile('${escapedSeedPath}') as text));`,
784
+ ], { encoding: "utf8" });
785
+ if (seed.status !== 0) {
786
+ return "sqlite-unavailable";
840
787
  }
788
+ return "seeded";
841
789
  }
842
- }
843
- function ensureRecord(target, key) {
844
- if (!isPlainRecord(target[key])) {
845
- target[key] = {};
846
- }
847
- return target[key];
848
- }
849
- function ensureStringArray(target, key) {
850
- const values = Array.isArray(target[key])
851
- ? target[key].filter((value) => typeof value === "string")
852
- : [];
853
- target[key] = values;
854
- return values;
855
- }
856
- function appendUnique(values, value) {
857
- if (!values.includes(value)) {
858
- values.push(value);
859
- }
860
- }
861
- function removeValue(values, value) {
862
- const index = values.indexOf(value);
863
- if (index !== -1) {
864
- values.splice(index, 1);
790
+ finally {
791
+ fs.rmSync(seedPath, { force: true });
865
792
  }
866
793
  }
867
794
  function defaultCursorStateDbPath() {
@@ -881,119 +808,6 @@ function readCursorStateValue(dbPath, key) {
881
808
  }
882
809
  return result.stdout;
883
810
  }
884
- export function buildLocalDesktopModelEntry(model) {
885
- const tooltipData = {
886
- primaryText: "",
887
- secondaryText: "",
888
- secondaryWarningText: false,
889
- icon: "",
890
- tertiaryText: "",
891
- tertiaryTextUrl: "",
892
- markdownContent: `**${model.displayName}**<br />Local OpenAI-compatible model served by cursorkit.<br /><br />${model.contextTokenLimit.toLocaleString()} token context window`,
893
- };
894
- return {
895
- name: model.id,
896
- serverModelName: model.id,
897
- clientDisplayName: model.displayName,
898
- inputboxShortModelName: model.displayName,
899
- vendorName: "local",
900
- vendor: { displayName: "Local" },
901
- supportsAgent: true,
902
- supportsCmdK: false,
903
- supportsImages: false,
904
- supportsMaxMode: true,
905
- supportsNonMaxMode: true,
906
- supportsPlanMode: true,
907
- supportsSandboxing: false,
908
- supportsThinking: false,
909
- cloudAgentEffortModes: [],
910
- defaultOn: true,
911
- degradationStatus: 0,
912
- isRecommendedForBackgroundComposer: false,
913
- legacySlugs: [model.id],
914
- idAliases: [model.id, model.displayName],
915
- parameterDefinitions: localDesktopParameterDefinitions(),
916
- namedModelSectionIndex: 10_000,
917
- visibleInRoutedModelView: true,
918
- tooltipData,
919
- tooltipDataForMaxMode: tooltipData,
920
- variants: [
921
- localDesktopVariantConfig(model, tooltipData, false),
922
- localDesktopVariantConfig(model, tooltipData, true),
923
- ],
924
- };
925
- }
926
- function localDesktopVariantConfig(model, tooltipData, isMaxMode) {
927
- return {
928
- parameterValues: localDesktopParameterValues(isMaxMode),
929
- displayName: model.displayName,
930
- isMaxMode,
931
- isDefaultMaxConfig: isMaxMode,
932
- isDefaultNonMaxConfig: !isMaxMode,
933
- tooltipData,
934
- displayNameOutsidePicker: model.displayName,
935
- variantStringRepresentation: localDesktopVariantString(model.id, isMaxMode),
936
- legacySlug: model.id,
937
- };
938
- }
939
- function localDesktopVariantString(modelId, isMaxMode) {
940
- const context = isMaxMode ? "1m" : "272k";
941
- return `${modelId}[context=${context},reasoning=medium,fast=false]`;
942
- }
943
- function localDesktopParameterValues(isMaxMode) {
944
- return [
945
- { id: "context", value: isMaxMode ? "1m" : "272k" },
946
- { id: "reasoning", value: "medium" },
947
- { id: "fast", value: "false" },
948
- ];
949
- }
950
- function localDesktopParameterDefinitions() {
951
- return [
952
- {
953
- id: "context",
954
- name: "Context",
955
- markdownTooltip: "Context size the model has available.",
956
- parameterType: {
957
- enumParameter: {
958
- values: [
959
- { value: "272k", displayName: "272K" },
960
- { value: "1m", displayName: "1M" },
961
- ],
962
- },
963
- },
964
- },
965
- {
966
- id: "reasoning",
967
- name: "Reasoning",
968
- markdownTooltip: "Reasoning effort the model uses to generate its response.",
969
- parameterType: {
970
- enumParameter: {
971
- values: [
972
- { value: "none", displayName: "None" },
973
- { value: "low", displayName: "Low" },
974
- { value: "medium", displayName: "Medium" },
975
- { value: "high", displayName: "High" },
976
- { value: "extra-high", displayName: "Extra High" },
977
- ],
978
- },
979
- },
980
- isCycleableByHotkey: true,
981
- },
982
- {
983
- id: "fast",
984
- name: "Fast",
985
- markdownTooltip: "Use the provider's fast lane when supported.",
986
- parameterType: {
987
- booleanParameter: {
988
- values: [{ value: "false" }, { value: "true", displayName: "Fast" }],
989
- },
990
- },
991
- },
992
- ];
993
- }
994
- function isPlainRecord(value) {
995
- return typeof value === "object" && value !== null && !Array.isArray(value);
996
- }
997
811
  export function cleanupIsolatedCursorProcesses(userDataDir) {
998
812
  const killed = terminateIsolatedCursorProcesses(userDataDir, "SIGTERM");
999
813
  spawnSync("sleep", ["1"]);
@@ -1196,9 +1010,14 @@ function configureCursorNodeTlsEnv(plan) {
1196
1010
  if (plan.profileMode !== "isolated") {
1197
1011
  return;
1198
1012
  }
1013
+ // Trust only the bridge's self-signed certificate via NODE_EXTRA_CA_CERTS
1014
+ // instead of globally disabling TLS verification. This keeps certificate
1015
+ // validation intact for real upstream traffic (e.g. api2.cursor.sh) while
1016
+ // allowing the spawned Cursor process to connect to the local bridge.
1017
+ const bridgeCertPath = plan.bridge.env?.BRIDGE_CERT_PATH ?? DESKTOP_CERT_PATH;
1199
1018
  plan.cursor.env = {
1200
1019
  ...plan.cursor.env,
1201
- NODE_TLS_REJECT_UNAUTHORIZED: "0",
1020
+ NODE_EXTRA_CA_CERTS: bridgeCertPath,
1202
1021
  };
1203
1022
  }
1204
1023
  function printPlan(plan) {
@@ -0,0 +1,10 @@
1
+ import type { LocalModelConfig } from "./config.js";
2
+ /**
3
+ * Builders that shape the Cursor desktop `applicationUser` state and local
4
+ * model catalog entries. Extracted from `ckLauncher` so the (large) launcher
5
+ * module is not also responsible for the desktop state-seed schema. These are
6
+ * pure data transforms over plain JSON-shaped records.
7
+ */
8
+ export declare function mergeLocalAgentBackendUrlsIntoApplicationUser(applicationUser: Record<string, unknown>, agentOrigin: string): void;
9
+ export declare function mergeLocalDesktopModelsIntoApplicationUser(applicationUser: Record<string, unknown>, models: LocalModelConfig[]): void;
10
+ export declare function buildLocalDesktopModelEntry(model: LocalModelConfig): Record<string, unknown>;
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Builders that shape the Cursor desktop `applicationUser` state and local
3
+ * model catalog entries. Extracted from `ckLauncher` so the (large) launcher
4
+ * module is not also responsible for the desktop state-seed schema. These are
5
+ * pure data transforms over plain JSON-shaped records.
6
+ */
7
+ export function mergeLocalAgentBackendUrlsIntoApplicationUser(applicationUser, agentOrigin) {
8
+ const cursorCreds = ensureRecord(applicationUser, "cursorCreds");
9
+ const urls = { default: agentOrigin };
10
+ cursorCreds.agentBackendUrlPrivacy = urls;
11
+ cursorCreds.agentBackendUrlNonPrivacy = urls;
12
+ }
13
+ export function mergeLocalDesktopModelsIntoApplicationUser(applicationUser, models) {
14
+ const localModelIds = new Set(models.map((model) => model.id));
15
+ const current = Array.isArray(applicationUser.availableDefaultModels2)
16
+ ? applicationUser.availableDefaultModels2.filter((item) => {
17
+ if (!isPlainRecord(item) || typeof item.name !== "string") {
18
+ return true;
19
+ }
20
+ return !localModelIds.has(item.name);
21
+ })
22
+ : [];
23
+ applicationUser.availableDefaultModels2 = current;
24
+ const aiSettings = ensureRecord(applicationUser, "aiSettings");
25
+ for (const model of models) {
26
+ appendUnique(ensureStringArray(aiSettings, "userAddedModels"), model.id);
27
+ appendUnique(ensureStringArray(aiSettings, "modelOverrideEnabled"), model.id);
28
+ removeValue(ensureStringArray(aiSettings, "modelOverrideDisabled"), model.id);
29
+ }
30
+ const preferences = ensureRecord(aiSettings, "modelParameterPreferences");
31
+ const updatedAt = new Date().toISOString();
32
+ for (const model of models) {
33
+ preferences[model.id] = {
34
+ modelId: model.id,
35
+ parameters: localDesktopParameterValues(true),
36
+ updatedAt,
37
+ };
38
+ }
39
+ const firstModel = models[0];
40
+ if (firstModel !== undefined) {
41
+ applicationUser.useOpenAIKey = true;
42
+ applicationUser.openAIBaseUrl = firstModel.baseUrl;
43
+ applicationUser.openAIKey = firstModel.apiKey;
44
+ const modelConfig = ensureRecord(aiSettings, "modelConfig");
45
+ const selectedModel = {
46
+ modelId: firstModel.id,
47
+ parameters: localDesktopParameterValues(true),
48
+ };
49
+ for (const key of ["composer", "background-composer"]) {
50
+ modelConfig[key] = {
51
+ modelName: firstModel.id,
52
+ maxMode: true,
53
+ selectedModels: [selectedModel],
54
+ };
55
+ }
56
+ }
57
+ const featureModelConfigs = ensureRecord(applicationUser, "featureModelConfigs");
58
+ for (const value of Object.values(featureModelConfigs)) {
59
+ if (!isPlainRecord(value)) {
60
+ continue;
61
+ }
62
+ const fallbackModels = ensureStringArray(value, "fallbackModels");
63
+ for (const model of models) {
64
+ appendUnique(fallbackModels, model.id);
65
+ }
66
+ }
67
+ }
68
+ export function buildLocalDesktopModelEntry(model) {
69
+ const tooltipData = {
70
+ primaryText: "",
71
+ secondaryText: "",
72
+ secondaryWarningText: false,
73
+ icon: "",
74
+ tertiaryText: "",
75
+ tertiaryTextUrl: "",
76
+ markdownContent: `**${model.displayName}**<br />Local OpenAI-compatible model served by cursorkit.<br /><br />${model.contextTokenLimit.toLocaleString()} token context window`,
77
+ };
78
+ return {
79
+ name: model.id,
80
+ serverModelName: model.id,
81
+ clientDisplayName: model.displayName,
82
+ inputboxShortModelName: model.displayName,
83
+ vendorName: "local",
84
+ vendor: { displayName: "Local" },
85
+ supportsAgent: true,
86
+ supportsCmdK: false,
87
+ supportsImages: false,
88
+ supportsMaxMode: true,
89
+ supportsNonMaxMode: true,
90
+ supportsPlanMode: true,
91
+ supportsSandboxing: false,
92
+ supportsThinking: false,
93
+ cloudAgentEffortModes: [],
94
+ defaultOn: true,
95
+ degradationStatus: 0,
96
+ isRecommendedForBackgroundComposer: false,
97
+ legacySlugs: [model.id],
98
+ idAliases: [model.id, model.displayName],
99
+ parameterDefinitions: localDesktopParameterDefinitions(),
100
+ namedModelSectionIndex: 10_000,
101
+ visibleInRoutedModelView: true,
102
+ tooltipData,
103
+ tooltipDataForMaxMode: tooltipData,
104
+ variants: [
105
+ localDesktopVariantConfig(model, tooltipData, false),
106
+ localDesktopVariantConfig(model, tooltipData, true),
107
+ ],
108
+ };
109
+ }
110
+ function localDesktopVariantConfig(model, tooltipData, isMaxMode) {
111
+ return {
112
+ parameterValues: localDesktopParameterValues(isMaxMode),
113
+ displayName: model.displayName,
114
+ isMaxMode,
115
+ isDefaultMaxConfig: isMaxMode,
116
+ isDefaultNonMaxConfig: !isMaxMode,
117
+ tooltipData,
118
+ displayNameOutsidePicker: model.displayName,
119
+ variantStringRepresentation: localDesktopVariantString(model.id, isMaxMode),
120
+ legacySlug: model.id,
121
+ };
122
+ }
123
+ function localDesktopVariantString(modelId, isMaxMode) {
124
+ const context = isMaxMode ? "1m" : "272k";
125
+ return `${modelId}[context=${context},reasoning=medium,fast=false]`;
126
+ }
127
+ function localDesktopParameterValues(isMaxMode) {
128
+ return [
129
+ { id: "context", value: isMaxMode ? "1m" : "272k" },
130
+ { id: "reasoning", value: "medium" },
131
+ { id: "fast", value: "false" },
132
+ ];
133
+ }
134
+ function localDesktopParameterDefinitions() {
135
+ return [
136
+ {
137
+ id: "context",
138
+ name: "Context",
139
+ markdownTooltip: "Context size the model has available.",
140
+ parameterType: {
141
+ enumParameter: {
142
+ values: [
143
+ { value: "272k", displayName: "272K" },
144
+ { value: "1m", displayName: "1M" },
145
+ ],
146
+ },
147
+ },
148
+ },
149
+ {
150
+ id: "reasoning",
151
+ name: "Reasoning",
152
+ markdownTooltip: "Reasoning effort the model uses to generate its response.",
153
+ parameterType: {
154
+ enumParameter: {
155
+ values: [
156
+ { value: "none", displayName: "None" },
157
+ { value: "low", displayName: "Low" },
158
+ { value: "medium", displayName: "Medium" },
159
+ { value: "high", displayName: "High" },
160
+ { value: "extra-high", displayName: "Extra High" },
161
+ ],
162
+ },
163
+ },
164
+ isCycleableByHotkey: true,
165
+ },
166
+ {
167
+ id: "fast",
168
+ name: "Fast",
169
+ markdownTooltip: "Use the provider's fast lane when supported.",
170
+ parameterType: {
171
+ booleanParameter: {
172
+ values: [{ value: "false" }, { value: "true", displayName: "Fast" }],
173
+ },
174
+ },
175
+ },
176
+ ];
177
+ }
178
+ function ensureRecord(target, key) {
179
+ if (!isPlainRecord(target[key])) {
180
+ target[key] = {};
181
+ }
182
+ return target[key];
183
+ }
184
+ function ensureStringArray(target, key) {
185
+ const values = Array.isArray(target[key])
186
+ ? target[key].filter((value) => typeof value === "string")
187
+ : [];
188
+ target[key] = values;
189
+ return values;
190
+ }
191
+ function appendUnique(values, value) {
192
+ if (!values.includes(value)) {
193
+ values.push(value);
194
+ }
195
+ }
196
+ function removeValue(values, value) {
197
+ const index = values.indexOf(value);
198
+ if (index !== -1) {
199
+ values.splice(index, 1);
200
+ }
201
+ }
202
+ function isPlainRecord(value) {
203
+ return typeof value === "object" && value !== null && !Array.isArray(value);
204
+ }
@@ -5,9 +5,19 @@ export interface DesktopConnectProxyOptions {
5
5
  bridgeHost: string;
6
6
  bridgePort: number;
7
7
  logPath?: string;
8
+ /**
9
+ * When true, CONNECT requests to non-Cursor hosts are tunneled through to
10
+ * their destination, turning this into an open forwarder. Defaults to false
11
+ * so only Cursor backends (cursorHostnames) are reachable.
12
+ */
8
13
  passthrough?: boolean;
9
14
  cursorHostnames?: readonly string[];
10
15
  headerTimeoutMs?: number;
16
+ /**
17
+ * Allow binding to a non-loopback host. Off by default to avoid exposing the
18
+ * proxy beyond the local machine.
19
+ */
20
+ allowExternalHost?: boolean;
11
21
  }
12
22
  export interface DesktopConnectProxy {
13
23
  server: net.Server;
@@ -5,8 +5,11 @@ const CONNECT_LINE_PATTERN = /^CONNECT\s+([^\s]+)\s+HTTP\/\d(?:\.\d)?$/i;
5
5
  const DEFAULT_HEADER_TIMEOUT_MS = 5_000;
6
6
  const MAX_CONNECT_HEADER_BYTES = 32 * 1024;
7
7
  export async function startDesktopConnectProxy(options) {
8
+ if (options.allowExternalHost !== true && !isLoopbackHost(options.host)) {
9
+ throw new Error(`Refusing to bind CONNECT proxy to non-loopback host ${options.host}. Set allowExternalHost: true to override.`);
10
+ }
8
11
  const cursorHostnames = new Set(options.cursorHostnames ?? DESKTOP_HOSTNAMES);
9
- const passthrough = options.passthrough ?? true;
12
+ const passthrough = options.passthrough ?? false;
10
13
  const activeSockets = new Set();
11
14
  const server = net.createServer((client) => {
12
15
  activeSockets.add(client);
@@ -155,6 +158,9 @@ function handleClient(client, options, cursorHostnames, passthrough, activeSocke
155
158
  });
156
159
  });
157
160
  }
161
+ function isLoopbackHost(host) {
162
+ return host === "127.0.0.1" || host === "::1" || host === "localhost";
163
+ }
158
164
  function parseConnectDestination(value) {
159
165
  if (value.startsWith("[")) {
160
166
  const closeBracket = value.indexOf("]");
@@ -1,3 +1,4 @@
1
+ import { timingSafeEqual } from "node:crypto";
1
2
  import http from "node:http";
2
3
  import http2 from "node:http2";
3
4
  import https from "node:https";
@@ -101,7 +102,9 @@ export async function createBridgeRuntime(config, logger) {
101
102
  }
102
103
  export async function startServer(runtime) {
103
104
  const listener = (request, response) => {
104
- void handleRequest(runtime, request, response);
105
+ handleRequest(runtime, request, response).catch((error) => {
106
+ handleRequestFailure(runtime, response, error);
107
+ });
105
108
  };
106
109
  const server = runtime.config.useTls
107
110
  ? runtime.config.desktopMode
@@ -252,6 +255,35 @@ async function handleRequest(runtime, request, response) {
252
255
  }
253
256
  proxyRequest(request, response, runtime.config, runtime.logger);
254
257
  }
258
+ function handleRequestFailure(runtime, response, error) {
259
+ runtime.logger.error("request handler crashed", {
260
+ error: error instanceof Error ? error.message : String(error),
261
+ });
262
+ try {
263
+ if (response.headersSent) {
264
+ response.destroy();
265
+ return;
266
+ }
267
+ response.writeHead(500, { "content-type": "application/json" });
268
+ response.end(JSON.stringify({ error: "internal bridge error" }));
269
+ }
270
+ catch {
271
+ response.destroy();
272
+ }
273
+ }
274
+ /**
275
+ * Length-independent constant-time comparison so bridge-token checks do not leak
276
+ * the secret via early-mismatch timing. A missing header never matches.
277
+ */
278
+ function constantTimeEquals(value, expected) {
279
+ if (value === undefined)
280
+ return false;
281
+ const valueBuf = Buffer.from(value);
282
+ const expectedBuf = Buffer.from(expected);
283
+ if (valueBuf.length !== expectedBuf.length)
284
+ return false;
285
+ return timingSafeEqual(valueBuf, expectedBuf);
286
+ }
255
287
  function authorizeRequest(runtime, request, response) {
256
288
  const token = runtime.config.authToken;
257
289
  if (token === undefined) {
@@ -259,7 +291,8 @@ function authorizeRequest(runtime, request, response) {
259
291
  }
260
292
  const authorization = headerValue(request.headers.authorization);
261
293
  const bridgeToken = headerValue(request.headers["x-cursor-rpc-auth"]);
262
- const authorized = authorization === `Bearer ${token}` || bridgeToken === token;
294
+ const authorized = constantTimeEquals(authorization, `Bearer ${token}`) ||
295
+ constantTimeEquals(bridgeToken, token);
263
296
  if (authorized) {
264
297
  return true;
265
298
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velum-labs/cursorkit",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Unofficial research bridge for experimenting with Cursor's ConnectRPC protocol against a self-hosted OpenAI-compatible model.",
@@ -62,6 +62,7 @@
62
62
  "@bufbuild/protoc-gen-es": "^2.12.0",
63
63
  "@types/node": "24.10.1",
64
64
  "@velum-labs/model-fusion-protocol": "0.1.1",
65
+ "@vitest/coverage-v8": "4.1.8",
65
66
  "prettier": "3.8.4",
66
67
  "tsx": "4.22.4",
67
68
  "typescript": "6.0.3",
@@ -78,6 +79,7 @@
78
79
  "baseline:generate": "tsx src/tools/baselineInventory.ts",
79
80
  "baseline:check": "tsx src/tools/baselineInventory.ts --check",
80
81
  "test": "vitest run tests",
82
+ "test:coverage": "vitest run --coverage",
81
83
  "e2e:cursor-agent": "python3 tests/e2e/cursor-agent-smoke.py",
82
84
  "model-fusion:protocol:check": "tsx src/tools/checkModelFusionProtocol.ts",
83
85
  "release:publish:check": "tsx src/tools/checkReleasePublishConfig.ts",