@cursor/july 0.1.26 → 0.1.27

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.
Files changed (277) hide show
  1. package/dist/ab.d.ts +6 -0
  2. package/dist/ab.d.ts.map +1 -1
  3. package/dist/ab.js +1 -0
  4. package/dist/artifacts.d.ts +34 -0
  5. package/dist/artifacts.d.ts.map +1 -0
  6. package/dist/artifacts.js +57 -0
  7. package/dist/bin/agent-serve.js +31 -9
  8. package/dist/channels/github/github-channel.d.ts +1 -1
  9. package/dist/channels/github/github-channel.d.ts.map +1 -1
  10. package/dist/channels/github/github-channel.js +14 -1
  11. package/dist/channels/github/types.d.ts +6 -1
  12. package/dist/channels/github/types.d.ts.map +1 -1
  13. package/dist/channels/slack/api.d.ts +2 -1
  14. package/dist/channels/slack/api.d.ts.map +1 -1
  15. package/dist/channels/slack/api.js +1 -0
  16. package/dist/channels/slack/cursor-account.d.ts +2 -1
  17. package/dist/channels/slack/cursor-account.d.ts.map +1 -1
  18. package/dist/channels/slack/cursor-account.js +1 -1
  19. package/dist/channels/slack/dispatch.d.ts +4 -1
  20. package/dist/channels/slack/dispatch.d.ts.map +1 -1
  21. package/dist/channels/slack/dispatch.js +3 -3
  22. package/dist/channels/slack/interactive.d.ts +2 -1
  23. package/dist/channels/slack/interactive.d.ts.map +1 -1
  24. package/dist/channels/slack/interactive.js +1 -0
  25. package/dist/channels/slack/slack-channel.d.ts.map +1 -1
  26. package/dist/channels/slack/slack-channel.js +4 -3
  27. package/dist/channels/slack/types.d.ts +6 -1
  28. package/dist/channels/slack/types.d.ts.map +1 -1
  29. package/dist/docs/404.html +1 -1
  30. package/dist/docs/ab.html +2 -2
  31. package/dist/docs/assets/{app.DYcC9FY-.js → app.BICLoJYF.js} +1 -1
  32. package/dist/docs/assets/chunks/@localSearchIndexroot.CQJytDwU.js +1 -0
  33. package/dist/docs/assets/chunks/{VPLocalSearchBox.o4N_knTV.js → VPLocalSearchBox.wN4Ko9dT.js} +1 -1
  34. package/dist/docs/assets/chunks/{theme.DQ-njyo0.js → theme.dnnTE-Co.js} +2 -2
  35. package/dist/docs/building-with-agents.html +2 -2
  36. package/dist/docs/concepts.html +2 -2
  37. package/dist/docs/deployment.html +2 -2
  38. package/dist/docs/evals.html +2 -2
  39. package/dist/docs/example-agents/approval-buddy.html +2 -2
  40. package/dist/docs/example-agents/benny.html +2 -2
  41. package/dist/docs/example-agents/bugbot.html +2 -2
  42. package/dist/docs/example-agents/codebase-wiki.html +2 -2
  43. package/dist/docs/example-agents/codeowners-review.html +2 -2
  44. package/dist/docs/example-agents/concierge.html +2 -2
  45. package/dist/docs/example-agents/fsd.html +2 -2
  46. package/dist/docs/example-agents/index.html +2 -2
  47. package/dist/docs/example-agents/knowledge-base.html +2 -2
  48. package/dist/docs/example-agents/oncall.html +2 -2
  49. package/dist/docs/example-agents/security-reviewer.html +2 -2
  50. package/dist/docs/example-agents/slack-agent.html +2 -2
  51. package/dist/docs/example-agents/weather-agent.html +2 -2
  52. package/dist/docs/guides/agent-to-agent.html +2 -2
  53. package/dist/docs/guides/cloud-runtime.html +2 -2
  54. package/dist/docs/guides/github.html +2 -2
  55. package/dist/docs/guides/human-in-the-loop.html +2 -2
  56. package/dist/docs/guides/mcp-oauth.html +2 -2
  57. package/dist/docs/guides/slack.html +2 -2
  58. package/dist/docs/guides/webhooks.html +2 -2
  59. package/dist/docs/hillclimbing.html +2 -2
  60. package/dist/docs/index.html +2 -2
  61. package/dist/docs/quickstart.html +2 -2
  62. package/dist/docs/reference/agent-config.html +2 -2
  63. package/dist/docs/reference/channels.html +2 -2
  64. package/dist/docs/reference/cli.html +2 -2
  65. package/dist/docs/reference/connections.html +2 -2
  66. package/dist/docs/reference/hooks.html +2 -2
  67. package/dist/docs/reference/http-api.html +2 -2
  68. package/dist/docs/reference/instructions.html +2 -2
  69. package/dist/docs/reference/playground.html +2 -2
  70. package/dist/docs/reference/project-layout.html +2 -2
  71. package/dist/docs/reference/prompt.html +2 -2
  72. package/dist/docs/reference/schedules.html +2 -2
  73. package/dist/docs/reference/sessions.html +2 -2
  74. package/dist/docs/reference/skills.html +2 -2
  75. package/dist/docs/reference/subagents.html +2 -2
  76. package/dist/docs/reference/tools.html +2 -2
  77. package/dist/docs/scaffolding-agents.html +2 -2
  78. package/dist/docs/storage.html +2 -2
  79. package/dist/docs/troubleshooting.html +2 -2
  80. package/dist/evals/assertions.d.ts +10 -1
  81. package/dist/evals/assertions.d.ts.map +1 -1
  82. package/dist/evals/assertions.js +12 -0
  83. package/dist/evals.d.ts +1 -1
  84. package/dist/evals.d.ts.map +1 -1
  85. package/dist/index.d.ts +1 -0
  86. package/dist/index.d.ts.map +1 -1
  87. package/dist/index.js +1 -0
  88. package/dist/internal/ab-fold.d.ts.map +1 -1
  89. package/dist/internal/ab-fold.js +4 -0
  90. package/dist/internal/ab-snapshot.d.ts.map +1 -1
  91. package/dist/internal/ab-snapshot.js +1 -0
  92. package/dist/internal/artifacts-store.d.ts +45 -0
  93. package/dist/internal/artifacts-store.d.ts.map +1 -0
  94. package/dist/internal/artifacts-store.js +250 -0
  95. package/dist/internal/builtin-tools/artifacts.d.ts +10 -0
  96. package/dist/internal/builtin-tools/artifacts.d.ts.map +1 -0
  97. package/dist/internal/builtin-tools/artifacts.js +73 -0
  98. package/dist/internal/builtin-tools/index.d.ts +8 -1
  99. package/dist/internal/builtin-tools/index.d.ts.map +1 -1
  100. package/dist/internal/builtin-tools/index.js +15 -0
  101. package/dist/internal/cli-ax.d.ts +11 -0
  102. package/dist/internal/cli-ax.d.ts.map +1 -1
  103. package/dist/internal/cli-ax.js +80 -0
  104. package/dist/internal/cli-slack.d.ts.map +1 -1
  105. package/dist/internal/cli-slack.js +14 -6
  106. package/dist/internal/discovery.d.ts.map +1 -1
  107. package/dist/internal/discovery.js +83 -2
  108. package/dist/internal/distribution.d.ts.map +1 -1
  109. package/dist/internal/distribution.js +1 -0
  110. package/dist/internal/model-pricing.d.ts +49 -0
  111. package/dist/internal/model-pricing.d.ts.map +1 -0
  112. package/dist/internal/model-pricing.js +377 -0
  113. package/dist/internal/reminder-runner.d.ts.map +1 -1
  114. package/dist/internal/reminder-runner.js +1 -0
  115. package/dist/internal/schedule-runner.d.ts.map +1 -1
  116. package/dist/internal/schedule-runner.js +1 -0
  117. package/dist/internal/sdk-runner.d.ts.map +1 -1
  118. package/dist/internal/sdk-runner.js +9 -1
  119. package/dist/internal/server.d.ts.map +1 -1
  120. package/dist/internal/server.js +39 -0
  121. package/dist/internal/session-cost.d.ts +49 -0
  122. package/dist/internal/session-cost.d.ts.map +1 -0
  123. package/dist/internal/session-cost.js +175 -0
  124. package/dist/internal/session-engine.d.ts +35 -1
  125. package/dist/internal/session-engine.d.ts.map +1 -1
  126. package/dist/internal/session-engine.js +128 -7
  127. package/dist/internal/storage-coordinator.d.ts +15 -0
  128. package/dist/internal/storage-coordinator.d.ts.map +1 -1
  129. package/dist/internal/storage-coordinator.js +52 -0
  130. package/dist/internal/trajectory.d.ts +9 -1
  131. package/dist/internal/trajectory.d.ts.map +1 -1
  132. package/dist/internal/trajectory.js +32 -5
  133. package/dist/playground/assets/index-BDCeW4uc.js +86 -0
  134. package/dist/playground/assets/index-Bul1WfrB.css +1 -0
  135. package/dist/playground/index.html +2 -2
  136. package/dist/storage.d.ts +4 -0
  137. package/dist/storage.d.ts.map +1 -1
  138. package/dist/storage.js +4 -0
  139. package/dist/types.d.ts +164 -1
  140. package/dist/types.d.ts.map +1 -1
  141. package/package.json +26 -26
  142. package/src/ab.ts +7 -0
  143. package/src/bin/agent-serve.ts +30 -2
  144. package/src/internal/ab-fold.ts +4 -0
  145. package/src/internal/ab-snapshot.ts +1 -0
  146. package/src/internal/cli-ax.ts +95 -0
  147. package/src/internal/model-pricing.ts +426 -0
  148. package/src/internal/sdk-runner.ts +10 -2
  149. package/src/internal/session-cost.ts +231 -0
  150. package/src/internal/session-engine.ts +28 -0
  151. package/src/internal/trajectory.ts +45 -3
  152. package/src/types.ts +24 -1
  153. package/dist/channels/github/instrument.d.ts +0 -20
  154. package/dist/channels/github/instrument.d.ts.map +0 -1
  155. package/dist/docs/assets/chunks/@localSearchIndexroot.BQTzJjR_.js +0 -1
  156. package/dist/internal/json-dir-store.d.ts +0 -32
  157. package/dist/internal/json-dir-store.d.ts.map +0 -1
  158. package/dist/internal/json-dir-store.js +0 -100
  159. package/dist/playground/assets/index-CjOQ4hN9.css +0 -1
  160. package/dist/playground/assets/index-DqXdAFGa.js +0 -85
  161. package/src/bin/agent-serve.version.test.ts +0 -64
  162. package/src/channels/github/api.test.ts +0 -64
  163. package/src/channels/github/auth.test.ts +0 -105
  164. package/src/channels/github/cursor-account.test.ts +0 -204
  165. package/src/channels/github/forward.test.ts +0 -457
  166. package/src/channels/github/github.test.ts +0 -985
  167. package/src/channels/github/replay.test.ts +0 -179
  168. package/src/channels/slack/api.post-message.test.ts +0 -148
  169. package/src/channels/slack/approvals.test.ts +0 -328
  170. package/src/channels/slack/block-actions.test.ts +0 -469
  171. package/src/channels/slack/bot-mentions.test.ts +0 -267
  172. package/src/channels/slack/channel-watch.test.ts +0 -363
  173. package/src/channels/slack/cursor-account.test.ts +0 -301
  174. package/src/channels/slack/defaults.final-post.test.ts +0 -182
  175. package/src/channels/slack/dispatch.test.ts +0 -875
  176. package/src/channels/slack/eval-directive.test.ts +0 -273
  177. package/src/channels/slack/message-body.test.ts +0 -54
  178. package/src/channels/slack/nudge-store.test.ts +0 -143
  179. package/src/channels/slack/slack.test.ts +0 -391
  180. package/src/channels/slack/stop.test.ts +0 -23
  181. package/src/channels/slack/thread-context.test.ts +0 -202
  182. package/src/evals/assertions.test.ts +0 -617
  183. package/src/evals/expect.test.ts +0 -144
  184. package/src/evals/judge.test.ts +0 -181
  185. package/src/evals/loaders.test.ts +0 -132
  186. package/src/evals/matchers.test.ts +0 -95
  187. package/src/evals/reporters.test.ts +0 -303
  188. package/src/evals/run-facts.test.ts +0 -259
  189. package/src/internal/ab-snapshot.test.ts +0 -325
  190. package/src/internal/approval-gate.test.ts +0 -49
  191. package/src/internal/approvals.integration.test.ts +0 -383
  192. package/src/internal/artifacts-store.test.ts +0 -283
  193. package/src/internal/authored-loaders.test.ts +0 -31
  194. package/src/internal/builtin-tools/artifacts.test.ts +0 -247
  195. package/src/internal/builtin-tools/reminders.test.ts +0 -201
  196. package/src/internal/channel-route-schema.test.ts +0 -294
  197. package/src/internal/chat-attach.test.ts +0 -262
  198. package/src/internal/cli-deploy.test.ts +0 -1991
  199. package/src/internal/cli-docs.test.ts +0 -161
  200. package/src/internal/cli-mcp.test.ts +0 -789
  201. package/src/internal/cli-skills.test.ts +0 -133
  202. package/src/internal/cli-slack.test.ts +0 -1654
  203. package/src/internal/cloud-merge.test.ts +0 -74
  204. package/src/internal/cron.test.ts +0 -22
  205. package/src/internal/cursor/account-mcp.test.ts +0 -807
  206. package/src/internal/cursor/backend-client.test.ts +0 -591
  207. package/src/internal/cursor/credentials.test.ts +0 -351
  208. package/src/internal/cursor/github-credentials.test.ts +0 -136
  209. package/src/internal/cursor-account-mcp-auth.test.ts +0 -310
  210. package/src/internal/cursor-account.integration.test.ts +0 -441
  211. package/src/internal/cursor-event-relay.test.ts +0 -746
  212. package/src/internal/cursor-github-credentials.integration.test.ts +0 -271
  213. package/src/internal/cursor-slack-relay.test.ts +0 -525
  214. package/src/internal/deploy-source.test.ts +0 -111
  215. package/src/internal/discovery.artifact-tool.test.ts +0 -136
  216. package/src/internal/discovery.artifacts.test.ts +0 -119
  217. package/src/internal/discovery.builtin-tools.test.ts +0 -94
  218. package/src/internal/discovery.concurrency.test.ts +0 -60
  219. package/src/internal/discovery.cursor-account.test.ts +0 -133
  220. package/src/internal/discovery.cwd.test.ts +0 -83
  221. package/src/internal/discovery.hosting.test.ts +0 -80
  222. package/src/internal/discovery.identity.test.ts +0 -44
  223. package/src/internal/docs-site.test.ts +0 -66
  224. package/src/internal/duration.test.ts +0 -29
  225. package/src/internal/eval-judge-model.test.ts +0 -187
  226. package/src/internal/eval-run-store.cancel.test.ts +0 -142
  227. package/src/internal/eval-run-store.storage.test.ts +0 -211
  228. package/src/internal/eval-runner.http.test.ts +0 -403
  229. package/src/internal/eval-runner.run.test.ts +0 -928
  230. package/src/internal/evals-client.test.ts +0 -307
  231. package/src/internal/event-mapper.test.ts +0 -243
  232. package/src/internal/github-fanout.test.ts +0 -213
  233. package/src/internal/handleAgentServeTrigger.test.ts +0 -185
  234. package/src/internal/host-kv.test.ts +0 -82
  235. package/src/internal/host-platforms.test.ts +0 -126
  236. package/src/internal/http-channel.test.ts +0 -402
  237. package/src/internal/init-project.test.ts +0 -270
  238. package/src/internal/install-cursor-skills.test.ts +0 -262
  239. package/src/internal/local-env.test.ts +0 -120
  240. package/src/internal/log-ring.test.ts +0 -31
  241. package/src/internal/logs-client.test.ts +0 -350
  242. package/src/internal/mcp-endpoint.test.ts +0 -436
  243. package/src/internal/mcp-host.test.ts +0 -298
  244. package/src/internal/mcp-oauth.test.ts +0 -148
  245. package/src/internal/net.test.ts +0 -17
  246. package/src/internal/peer-connections.test.ts +0 -128
  247. package/src/internal/peer-mcp.integration.test.ts +0 -289
  248. package/src/internal/playground/toolchain.test.ts +0 -53
  249. package/src/internal/playground-cli.test.ts +0 -187
  250. package/src/internal/playground-proxy.test.ts +0 -376
  251. package/src/internal/prompt-context.integration.test.ts +0 -232
  252. package/src/internal/prompt-context.test.ts +0 -127
  253. package/src/internal/reminder-runner.test.ts +0 -432
  254. package/src/internal/reminder-store.test.ts +0 -53
  255. package/src/internal/request-headers.test.ts +0 -27
  256. package/src/internal/resolve-prod-target.test.ts +0 -787
  257. package/src/internal/resolved-connections.test.ts +0 -295
  258. package/src/internal/router.test.ts +0 -57
  259. package/src/internal/sdk-runner.test.ts +0 -290
  260. package/src/internal/server.artifacts.test.ts +0 -334
  261. package/src/internal/session-engine.artifacts.test.ts +0 -293
  262. package/src/internal/session-engine.coalesce.test.ts +0 -169
  263. package/src/internal/session-engine.concurrency.test.ts +0 -250
  264. package/src/internal/session-engine.host-oauth-mcp.test.ts +0 -110
  265. package/src/internal/session-engine.interrupt.test.ts +0 -577
  266. package/src/internal/session-engine.storage.test.ts +0 -547
  267. package/src/internal/session-urls.test.ts +0 -28
  268. package/src/internal/sessions-client.test.ts +0 -518
  269. package/src/internal/storage-coordinator.test.ts +0 -517
  270. package/src/internal/tool-call.test.ts +0 -458
  271. package/src/internal/tool-result.test.ts +0 -52
  272. package/src/internal/trajectory.approvals.test.ts +0 -83
  273. package/src/internal/trajectory.subagents.test.ts +0 -198
  274. package/src/internal/turn-governor.test.ts +0 -137
  275. package/src/internal/update-check.test.ts +0 -485
  276. package/src/internal/workspace.test.ts +0 -207
  277. package/src/storage-backends/cursor-hosted.test.ts +0 -121
@@ -1,351 +0,0 @@
1
- import { mkdtemp, readFile, rm, stat } from "node:fs/promises";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
- import {
6
- clearStoredCredentials,
7
- credentialsFilePath,
8
- cursorBackendUrl,
9
- cursorExternalApiUrl,
10
- pollForLoginTokens,
11
- readStoredCredentials,
12
- resolveApiKey,
13
- startBrowserLogin,
14
- writeStoredCredentials,
15
- } from "./credentials.js";
16
-
17
- let configDir: string;
18
- const savedEnv: Record<string, string | undefined> = {};
19
-
20
- beforeEach(async () => {
21
- configDir = await mkdtemp(join(tmpdir(), "agent-serve-creds-"));
22
- for (const key of [
23
- "AGENT_SERVE_CONFIG_DIR",
24
- "CURSOR_API_KEY",
25
- "CURSOR_API_BASE_URL",
26
- "AGENT_SERVE_CURSOR_BACKEND_URL",
27
- "CURSOR_WEBSITE_URL",
28
- ]) {
29
- savedEnv[key] = process.env[key];
30
- delete process.env[key];
31
- }
32
- process.env.AGENT_SERVE_CONFIG_DIR = configDir;
33
- });
34
-
35
- afterEach(async () => {
36
- for (const [key, value] of Object.entries(savedEnv)) {
37
- if (value === undefined) {
38
- delete process.env[key];
39
- } else {
40
- process.env[key] = value;
41
- }
42
- }
43
- await rm(configDir, { recursive: true, force: true });
44
- });
45
-
46
- describe("base URLs", () => {
47
- it("defaults the external API host separately from the backend", () => {
48
- expect(cursorBackendUrl()).toBe("https://api2.cursor.sh");
49
- expect(cursorExternalApiUrl()).toBe("https://api.cursor.com");
50
- });
51
-
52
- it("CURSOR_API_BASE_URL overrides both for local dev", () => {
53
- process.env.CURSOR_API_BASE_URL = "http://localhost:8888/";
54
- expect(cursorBackendUrl()).toBe("http://localhost:8888");
55
- expect(cursorExternalApiUrl()).toBe("http://localhost:8888");
56
- });
57
-
58
- it("AGENT_SERVE_CURSOR_BACKEND_URL re-pins only the backend family (hosted pods)", () => {
59
- process.env.CURSOR_API_BASE_URL = "https://api.cursor.com";
60
- process.env.AGENT_SERVE_CURSOR_BACKEND_URL = "https://api2.cursor.sh/";
61
- expect(cursorBackendUrl()).toBe("https://api2.cursor.sh");
62
- expect(cursorExternalApiUrl()).toBe("https://api.cursor.com");
63
- });
64
- });
65
-
66
- describe("stored credentials", () => {
67
- it("round-trips through the credentials file with 0600 permissions", async () => {
68
- await writeStoredCredentials({
69
- backendUrl: "https://api2.cursor.sh",
70
- apiKey: "crsr_test",
71
- });
72
- expect(await readStoredCredentials()).toEqual({
73
- backendUrl: "https://api2.cursor.sh",
74
- apiKey: "crsr_test",
75
- });
76
- const mode = (await stat(credentialsFilePath())).mode & 0o777;
77
- expect(mode).toBe(0o600);
78
- expect(await readFile(credentialsFilePath(), "utf8")).toContain(
79
- "crsr_test"
80
- );
81
-
82
- expect(await clearStoredCredentials()).toBe(true);
83
- expect(await readStoredCredentials()).toBeUndefined();
84
- expect(await clearStoredCredentials()).toBe(false);
85
- });
86
-
87
- it("treats a corrupt credentials file as signed out", async () => {
88
- await writeStoredCredentials({
89
- backendUrl: "https://api2.cursor.sh",
90
- apiKey: "crsr_test",
91
- });
92
- const { writeFile } = await import("node:fs/promises");
93
- await writeFile(credentialsFilePath(), "not json");
94
- expect(await readStoredCredentials()).toBeUndefined();
95
- });
96
- });
97
-
98
- describe("resolveApiKey", () => {
99
- it("prefers the explicit option, then env, then the stored login", async () => {
100
- await writeStoredCredentials({
101
- backendUrl: cursorBackendUrl(),
102
- apiKey: "crsr_stored",
103
- });
104
- process.env.CURSOR_API_KEY = "crsr_env";
105
-
106
- expect(await resolveApiKey({ apiKey: "crsr_option" })).toMatchObject({
107
- apiKey: "crsr_option",
108
- source: "option",
109
- });
110
- expect(await resolveApiKey()).toMatchObject({
111
- apiKey: "crsr_env",
112
- source: "env",
113
- });
114
- delete process.env.CURSOR_API_KEY;
115
- expect(await resolveApiKey()).toMatchObject({
116
- apiKey: "crsr_stored",
117
- source: "login",
118
- });
119
- });
120
-
121
- it("ignores a stored key minted against a different backend", async () => {
122
- await writeStoredCredentials({
123
- backendUrl: "https://localhost:8000",
124
- apiKey: "crsr_dev_backend",
125
- });
126
- expect(await resolveApiKey()).toBeUndefined();
127
- expect(
128
- await resolveApiKey({ backendUrl: "https://localhost:8000" })
129
- ).toMatchObject({ apiKey: "crsr_dev_backend", source: "login" });
130
- });
131
-
132
- it("resolves to undefined with no credentials anywhere", async () => {
133
- expect(await resolveApiKey()).toBeUndefined();
134
- });
135
- });
136
-
137
- describe("browser login", () => {
138
- it("builds a PKCE deep link on the configured website", () => {
139
- process.env.CURSOR_WEBSITE_URL = "https://localhost:4443";
140
- const start = startBrowserLogin();
141
- const url = new URL(start.loginUrl);
142
- expect(url.origin).toBe("https://localhost:4443");
143
- expect(url.pathname).toBe("/loginDeepControl");
144
- expect(url.searchParams.get("uuid")).toBe(start.uuid);
145
- expect(url.searchParams.get("mode")).toBe("login");
146
- expect(url.searchParams.get("redirectTarget")).toBe("cli");
147
- // The challenge is derived from (and never equals) the verifier.
148
- expect(url.searchParams.get("challenge")).not.toBe(start.verifier);
149
- expect(url.searchParams.get("challenge")).toMatch(/^[A-Za-z0-9_-]{43}$/);
150
- });
151
-
152
- // The verifier is redeemable for a durable API key, so it must never reach a
153
- // URL: POST keeps it in the body, out of access logs.
154
- it("polls POST /auth/poll with the verifier in the body, never the URL", async () => {
155
- const seen: Array<{ url: string; init?: RequestInit }> = [];
156
- let calls = 0;
157
- const fetchImpl = (async (input: RequestInfo | URL, init?: RequestInit) => {
158
- seen.push({ url: String(input), init });
159
- calls++;
160
- if (calls < 3) {
161
- return new Response("Not found", { status: 404 });
162
- }
163
- return Response.json({ accessToken: "session-token", refreshToken: "r" });
164
- }) as typeof fetch;
165
-
166
- const result = await pollForLoginTokens({
167
- uuid: "uuid-1",
168
- verifier: "verifier-1",
169
- backendUrl: "https://backend.example",
170
- fetchImpl,
171
- });
172
- expect(result).toEqual({
173
- status: "signed_in",
174
- accessToken: "session-token",
175
- });
176
- expect(calls).toBe(3);
177
- for (const { url, init } of seen) {
178
- expect(init?.method).toBe("POST");
179
- // The whole point: no verifier anywhere in the request URL.
180
- expect(url).toBe("https://backend.example/auth/poll");
181
- expect(JSON.parse(String(init?.body))).toEqual({
182
- uuid: "uuid-1",
183
- verifier: "verifier-1",
184
- });
185
- }
186
- });
187
-
188
- // Production has not deployed POST /auth/poll yet (the route landed in
189
- // #167554). Until it does, the login has to work over GET.
190
- it("falls back to GET, once and stickily, when the backend has no POST route", async () => {
191
- const seen: Array<{ url: string; method?: string }> = [];
192
- const warnings: string[] = [];
193
- let calls = 0;
194
- const fetchImpl = (async (input: RequestInfo | URL, init?: RequestInit) => {
195
- seen.push({ url: String(input), method: init?.method });
196
- calls++;
197
- if (calls === 1) {
198
- return Response.json(
199
- {
200
- message: "Route POST:/auth/poll not found",
201
- error: "Not Found",
202
- statusCode: 404,
203
- },
204
- { status: 404 }
205
- );
206
- }
207
- if (calls === 2) {
208
- return new Response("Not found", { status: 404 });
209
- }
210
- return Response.json({ accessToken: "session-token" });
211
- }) as typeof fetch;
212
-
213
- const result = await pollForLoginTokens({
214
- uuid: "uuid-1",
215
- verifier: "verifier-1",
216
- backendUrl: "https://backend.example",
217
- fetchImpl,
218
- logger: (line) => warnings.push(line),
219
- });
220
- expect(result).toEqual({
221
- status: "signed_in",
222
- accessToken: "session-token",
223
- });
224
- expect(seen.map((s) => s.method)).toEqual(["POST", "GET", "GET"]);
225
- // The fallback carries the PKCE material in the query string — the exposure
226
- // POST exists to avoid — so it must say so out loud, exactly once.
227
- expect(warnings).toHaveLength(1);
228
- expect(warnings[0]).toContain("no POST /auth/poll");
229
- expect(new URL(seen[1]?.url ?? "").searchParams.get("verifier")).toBe(
230
- "verifier-1"
231
- );
232
- });
233
-
234
- it("fails clearly when the backend serves neither verb", async () => {
235
- let calls = 0;
236
- const fetchImpl = (async (
237
- _input: RequestInfo | URL,
238
- init?: RequestInit
239
- ) => {
240
- calls++;
241
- return Response.json(
242
- {
243
- message: `Route ${init?.method ?? "GET"}:/auth/poll not found`,
244
- error: "Not Found",
245
- statusCode: 404,
246
- },
247
- { status: 404 }
248
- );
249
- }) as typeof fetch;
250
-
251
- const result = await pollForLoginTokens({
252
- uuid: "uuid-1",
253
- verifier: "verifier-1",
254
- backendUrl: "https://backend.example",
255
- fetchImpl,
256
- });
257
- expect(result).toMatchObject({
258
- status: "failed",
259
- message: expect.stringContaining("neither POST nor GET"),
260
- });
261
- // POST, then the GET retry. No backoff loop on a route that cannot exist.
262
- expect(calls).toBe(2);
263
- });
264
-
265
- // Fastify's 404 body echoes the full request URL, so on the GET fallback it
266
- // contains the verifier. Error text is printed to terminals and pasted into
267
- // bug reports; it must never carry a redeemable secret.
268
- it("redacts the verifier from error messages", async () => {
269
- const verifier = "sUpErSeCrEtVeRiFiEr";
270
- const fetchImpl = (async () =>
271
- new Response(
272
- `upstream error for /auth/poll?uuid=u&verifier=${verifier}`,
273
- { status: 500 }
274
- )) as typeof fetch;
275
-
276
- const result = await pollForLoginTokens({
277
- uuid: "u",
278
- verifier,
279
- backendUrl: "https://backend.example",
280
- fetchImpl,
281
- });
282
- expect(result.status).toBe("failed");
283
- const message = result.status === "failed" ? result.message : "";
284
- expect(message).not.toContain(verifier);
285
- expect(message).toContain("[verifier redacted]");
286
- expect(message).toContain("500");
287
- });
288
-
289
- // Regression marker: an earlier poll treated any unrecognized 404 body as
290
- // fatal, so a body-shape change looked exactly like "you signed in too
291
- // slowly". On the GET path, an unknown 404 body means "keep waiting".
292
- it("treats an unrecognized 404 body on the GET fallback as still-pending", async () => {
293
- let calls = 0;
294
- const fetchImpl = (async (
295
- _input: RequestInfo | URL,
296
- init?: RequestInit
297
- ) => {
298
- calls++;
299
- if (init?.method === "POST") {
300
- // Force the GET fallback.
301
- return Response.json(
302
- { message: "Route POST:/auth/poll not found", statusCode: 404 },
303
- { status: 404 }
304
- );
305
- }
306
- if (calls < 4) {
307
- return Response.json({ error: "not_found" }, { status: 404 });
308
- }
309
- return Response.json({ accessToken: "session-token" });
310
- }) as typeof fetch;
311
-
312
- const result = await pollForLoginTokens({
313
- uuid: "u",
314
- verifier: "v",
315
- backendUrl: "https://backend.example",
316
- fetchImpl,
317
- });
318
- expect(result).toEqual({
319
- status: "signed_in",
320
- accessToken: "session-token",
321
- });
322
- });
323
-
324
- it("gives up after repeated hard errors, naming the status", async () => {
325
- const fetchImpl = (async () =>
326
- new Response("nope", { status: 500 })) as typeof fetch;
327
- const result = await pollForLoginTokens({
328
- uuid: "u",
329
- verifier: "v",
330
- backendUrl: "https://backend.example",
331
- fetchImpl,
332
- });
333
- expect(result).toMatchObject({
334
- status: "failed",
335
- message: expect.stringContaining("500"),
336
- });
337
- });
338
-
339
- it("times out when the browser sign-in never lands", async () => {
340
- const fetchImpl = (async () =>
341
- new Response("Not found", { status: 404 })) as typeof fetch;
342
- const result = await pollForLoginTokens({
343
- uuid: "u",
344
- verifier: "v",
345
- backendUrl: "https://backend.example",
346
- fetchImpl,
347
- maxAttempts: 1,
348
- });
349
- expect(result).toEqual({ status: "timed_out" });
350
- });
351
- });
@@ -1,136 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
- import { parseCursorGithubRepoScope } from "../../channels/github/cursor-account.js";
3
- import { CursorGithubCredentialProvider } from "./github-credentials.js";
4
-
5
- describe("CursorGithubCredentialProvider", () => {
6
- let savedGhToken: string | undefined;
7
-
8
- beforeEach(() => {
9
- savedGhToken = process.env.GH_TOKEN;
10
- process.env.GH_TOKEN = "previous-token";
11
- });
12
-
13
- afterEach(() => {
14
- if (savedGhToken === undefined) {
15
- delete process.env.GH_TOKEN;
16
- } else {
17
- process.env.GH_TOKEN = savedGhToken;
18
- }
19
- });
20
-
21
- it("caches, refreshes, and exports the scoped token for gh", async () => {
22
- let now = Date.parse("2026-07-16T00:00:00.000Z");
23
- let mint = 0;
24
- const getGithubToken = vi.fn(async () => {
25
- mint++;
26
- return {
27
- token: `ghs_scoped_${mint}`,
28
- expiresAt: new Date(now + 45 * 60_000).toISOString(),
29
- };
30
- });
31
- const provider = new CursorGithubCredentialProvider({
32
- client: { getGithubToken },
33
- scope: parseCursorGithubRepoScope(["Acme/Widgets", "acme/widgets"]),
34
- now: () => now,
35
- });
36
-
37
- await provider.start();
38
- expect(process.env.GH_TOKEN).toBe("ghs_scoped_1");
39
- await expect(provider.getToken()).resolves.toBe("ghs_scoped_1");
40
- expect(getGithubToken).toHaveBeenCalledOnce();
41
- expect(getGithubToken).toHaveBeenCalledWith(["acme/widgets"], {
42
- permissions: "pr-write",
43
- });
44
-
45
- now += 41 * 60_000;
46
- await expect(provider.getToken()).resolves.toBe("ghs_scoped_2");
47
- expect(process.env.GH_TOKEN).toBe("ghs_scoped_2");
48
- expect(getGithubToken).toHaveBeenCalledTimes(2);
49
-
50
- provider.stop();
51
- expect(process.env.GH_TOKEN).toBe("previous-token");
52
- await expect(provider.getToken()).rejects.toThrow(/have been stopped/);
53
- });
54
-
55
- it("single-flights concurrent token requests", async () => {
56
- const expiresAt = new Date(Date.now() + 45 * 60_000).toISOString();
57
- const getGithubToken = vi.fn(async () => ({
58
- token: "ghs_one",
59
- expiresAt,
60
- }));
61
- const provider = new CursorGithubCredentialProvider({
62
- client: { getGithubToken },
63
- scope: parseCursorGithubRepoScope(["acme/widgets"]),
64
- });
65
-
66
- await expect(
67
- Promise.all([provider.getToken(), provider.getToken()])
68
- ).resolves.toEqual(["ghs_one", "ghs_one"]);
69
- expect(getGithubToken).toHaveBeenCalledOnce();
70
- });
71
-
72
- it("activates a token fetched before the provider starts", async () => {
73
- const getGithubToken = vi.fn(async () => ({
74
- token: "ghs_prefetched",
75
- expiresAt: new Date(Date.now() + 45 * 60_000).toISOString(),
76
- }));
77
- const provider = new CursorGithubCredentialProvider({
78
- client: { getGithubToken },
79
- scope: parseCursorGithubRepoScope(["acme/widgets"]),
80
- });
81
-
82
- await provider.getToken();
83
- expect(process.env.GH_TOKEN).toBe("previous-token");
84
- await provider.start();
85
- expect(process.env.GH_TOKEN).toBe("ghs_prefetched");
86
- provider.stop();
87
- });
88
-
89
- it("keeps the previous token when an early refresh is transiently unavailable", async () => {
90
- let now = Date.parse("2026-07-16T00:00:00.000Z");
91
- const getGithubToken = vi
92
- .fn()
93
- .mockResolvedValueOnce({
94
- token: "ghs_still_valid",
95
- expiresAt: new Date(now + 45 * 60_000).toISOString(),
96
- })
97
- .mockRejectedValueOnce(new Error("backend unavailable"));
98
- const provider = new CursorGithubCredentialProvider({
99
- client: { getGithubToken },
100
- scope: parseCursorGithubRepoScope(["acme/widgets"]),
101
- now: () => now,
102
- });
103
-
104
- await expect(provider.getToken()).resolves.toBe("ghs_still_valid");
105
- now += 41 * 60_000;
106
- await expect(provider.getToken()).resolves.toBe("ghs_still_valid");
107
- });
108
-
109
- it("evicts an expired lease and negative-caches hard failures", async () => {
110
- let now = Date.parse("2026-07-16T00:00:00.000Z");
111
- const getGithubToken = vi
112
- .fn()
113
- .mockResolvedValueOnce({
114
- token: "ghs_expired",
115
- expiresAt: new Date(now + 45 * 60_000).toISOString(),
116
- })
117
- .mockRejectedValue(new Error("GitHub connection missing"));
118
- const provider = new CursorGithubCredentialProvider({
119
- client: { getGithubToken },
120
- scope: parseCursorGithubRepoScope(["acme/widgets"]),
121
- now: () => now,
122
- });
123
-
124
- await expect(provider.getToken()).resolves.toBe("ghs_expired");
125
- now += 46 * 60_000;
126
- await expect(provider.getToken()).rejects.toThrow(
127
- /GitHub connection missing/
128
- );
129
- expect(getGithubToken).toHaveBeenCalledTimes(2);
130
- // Negative cache: do not hot-loop the backend before the backoff elapses.
131
- await expect(provider.getToken()).rejects.toThrow(
132
- /GitHub connection missing/
133
- );
134
- expect(getGithubToken).toHaveBeenCalledTimes(2);
135
- });
136
- });