agent-dealer 1.2.1 → 1.2.3
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.
- package/bundle/server/dist/adapters/linear-inbox.js +9 -5
- package/bundle/server/dist/capacity/adapter.js +2 -0
- package/bundle/server/dist/capacity/claude-events.js +45 -10
- package/bundle/server/dist/capacity/claude-events.test.js +7 -0
- package/bundle/server/dist/capacity/claude-local-cache.js +788 -0
- package/bundle/server/dist/capacity/claude-local-cache.test.js +520 -0
- package/bundle/server/dist/capacity/codex-app-server.js +100 -10
- package/bundle/server/dist/capacity/codex-app-server.test.js +195 -2
- package/bundle/server/dist/capacity/cursor-individual-credentials.js +425 -0
- package/bundle/server/dist/capacity/cursor-individual-credentials.test.js +369 -0
- package/bundle/server/dist/capacity/cursor-individual.js +1037 -0
- package/bundle/server/dist/capacity/cursor-individual.test.js +1081 -0
- package/bundle/server/dist/capacity/muse-host.js +748 -0
- package/bundle/server/dist/capacity/muse-host.test.js +480 -0
- package/bundle/server/dist/capacity/muse-lifecycle.test.js +97 -0
- package/bundle/server/dist/capacity/muse.js +389 -91
- package/bundle/server/dist/capacity/muse.test.js +100 -61
- package/bundle/server/dist/capacity/service.js +1 -0
- package/bundle/server/dist/coordinator/muse-spawn.js +143 -1
- package/bundle/server/dist/db/index.js +30 -0
- package/bundle/server/dist/db/schema.sql +27 -0
- package/bundle/server/dist/index.js +17 -2
- package/bundle/server/dist/repository/cursor-individual-billing.js +84 -0
- package/bundle/server/dist/repository/repository-mappings.js +46 -0
- package/bundle/server/dist/repository/repository-mappings.test.js +83 -0
- package/bundle/server/dist/repository/runtime-capacity.js +8 -3
- package/bundle/server/dist/repository/runtime-capacity.test.js +4 -0
- package/bundle/server/dist/routes/cursor-individual-billing.test.js +80 -0
- package/bundle/server/dist/routes/index.js +58 -9
- package/bundle/server/dist/routes/intake-linear.test.js +55 -1
- package/bundle/server/dist/routes/repository-mappings.js +22 -0
- package/bundle/server/dist/routes/repository-mappings.test.js +110 -0
- package/bundle/server/dist/routes/runtime-capacity.test.js +84 -8
- package/bundle/server/dist/runners/muse-code-jsonl.js +7 -1
- package/bundle/server/dist/runners/muse-serve-session.js +313 -0
- package/bundle/server/dist/runners/muse-serve-session.test.js +325 -0
- package/bundle/server/package.json +2 -2
- package/bundle/server/static-ui/assets/codex-YZSpL-SB.png +0 -0
- package/bundle/server/static-ui/assets/index-CYqRh_-S.css +1 -0
- package/bundle/server/static-ui/assets/index-UO4lHZw4.js +60 -0
- package/bundle/server/static-ui/assets/muse-CtdcC6ve.svg +33 -0
- package/bundle/server/static-ui/index.html +2 -2
- package/bundle/shared/dist/index.d.ts +6 -6
- package/bundle/shared/dist/linear-intake.d.ts +71 -0
- package/bundle/shared/dist/linear-intake.js +90 -0
- package/bundle/shared/dist/linear-intake.test.js +80 -1
- package/bundle/shared/dist/runtime-capacity.d.ts +89 -0
- package/bundle/shared/dist/runtime-capacity.js +60 -0
- package/bundle/shared/dist/runtime-capacity.test.js +1 -0
- package/bundle/shared/package.json +1 -1
- package/dist/doctor.d.ts +57 -0
- package/dist/doctor.js +211 -0
- package/dist/doctor.test.d.ts +1 -0
- package/dist/doctor.test.js +246 -0
- package/package.json +1 -1
- package/bundle/server/static-ui/assets/index-ChnI0F02.css +0 -1
- package/bundle/server/static-ui/assets/index-CyB1573Q.js +0 -60
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
// packages/server/src/capacity/cursor-individual-credentials.ts
|
|
2
|
+
//
|
|
3
|
+
// NOT-250: isolated credential lookup for the experimental Cursor Individual
|
|
4
|
+
// dashboard adapter. NOT-267: the desktop Cursor login is the primary
|
|
5
|
+
// source, the Cursor Agent auth file the fallback.
|
|
6
|
+
//
|
|
7
|
+
// The dashboard endpoints this feature calls are undocumented: the only
|
|
8
|
+
// credential available is the existing local Cursor login, whose storage
|
|
9
|
+
// locations and shapes are themselves not a supported contract and may
|
|
10
|
+
// change without notice. All of that risk lives in this one module:
|
|
11
|
+
//
|
|
12
|
+
// - Resolution order: Cursor desktop `state.vscdb` first (read-only,
|
|
13
|
+
// single allowlisted key), then the Cursor Agent `auth.json` allowlisted
|
|
14
|
+
// token shapes. The first usable source wins; Agent auth unchanged.
|
|
15
|
+
// - Accepted JSON shapes are an explicit allowlist below — anything else
|
|
16
|
+
// reads `unparsable` (changed format), never a guess.
|
|
17
|
+
// - `cursorIndividualCredentialStatus()` (the diagnostics surface) returns
|
|
18
|
+
// presence/source/path/format ONLY — never secret material.
|
|
19
|
+
// - `loadCursorIndividualCredential()` additionally returns an in-memory
|
|
20
|
+
// `authHeader` for the dashboard HTTP call. The header value must never be
|
|
21
|
+
// persisted, returned to the browser, or logged — the adapter, repository,
|
|
22
|
+
// and route layers enforce that, and tests pin it.
|
|
23
|
+
//
|
|
24
|
+
// No silent opt-in or credential migration lives here: this module only
|
|
25
|
+
// *finds* credentials; whether they may be *used* is decided by the
|
|
26
|
+
// explicit `AGENT_DEALER_CURSOR_INDIVIDUAL_CAPACITY=experimental` opt-in in
|
|
27
|
+
// cursor-individual.ts, which short-circuits before touching this module.
|
|
28
|
+
// Nothing here writes Cursor's database, refreshes tokens, or touches the
|
|
29
|
+
// Keychain or auth files — all reads are read-only.
|
|
30
|
+
import Database from "better-sqlite3";
|
|
31
|
+
import fs from "node:fs";
|
|
32
|
+
import os from "node:os";
|
|
33
|
+
import path from "node:path";
|
|
34
|
+
/** Explicit credential-file override (tests point this at tmp fixtures). */
|
|
35
|
+
export const CURSOR_INDIVIDUAL_CREDENTIAL_FILE_ENV = "CURSOR_INDIVIDUAL_CREDENTIAL_FILE";
|
|
36
|
+
/** Home-directory override for tests (mirrors how OS home resolution works). */
|
|
37
|
+
export const CURSOR_INDIVIDUAL_HOME_ENV = "CURSOR_INDIVIDUAL_HOME";
|
|
38
|
+
/** Explicit desktop-state database override (tests point this at tmp fixtures). */
|
|
39
|
+
export const CURSOR_INDIVIDUAL_DESKTOP_STATE_FILE_ENV = "CURSOR_INDIVIDUAL_DESKTOP_STATE_FILE";
|
|
40
|
+
/**
|
|
41
|
+
* Community-observed local Cursor login file. The location is undocumented
|
|
42
|
+
* and may move — when it does, reads degrade to `absent`/`unparsable`, never
|
|
43
|
+
* to a scan of unrelated files. Exactly one default candidate is listed on
|
|
44
|
+
* purpose: this module must not go hunting through the home directory.
|
|
45
|
+
*/
|
|
46
|
+
export const CURSOR_INDIVIDUAL_DEFAULT_RELATIVE_PATH = path.join(".cursor", "auth.json");
|
|
47
|
+
/**
|
|
48
|
+
* Accepted top-level token keys. The credential format is undocumented, so
|
|
49
|
+
* both camelCase and snake_case spellings are tolerated — but ONLY these
|
|
50
|
+
* keys. An object with none of them is a changed format (`unparsable`).
|
|
51
|
+
*/
|
|
52
|
+
export const CURSOR_INDIVIDUAL_TOKEN_KEYS = [
|
|
53
|
+
"token",
|
|
54
|
+
"accessToken",
|
|
55
|
+
"access_token",
|
|
56
|
+
"sessionToken",
|
|
57
|
+
"session_token",
|
|
58
|
+
"authToken",
|
|
59
|
+
"auth_token",
|
|
60
|
+
"apiKey",
|
|
61
|
+
"api_key",
|
|
62
|
+
"jwt",
|
|
63
|
+
];
|
|
64
|
+
/**
|
|
65
|
+
* Accepted top-level WorkOS user-id keys. The dashboard's session cookie is
|
|
66
|
+
* `<userId>%3A%3A<token>` (see `cursorIndividualAuthHeader`) — a bare token
|
|
67
|
+
* is not enough. Same allowlist discipline as the token keys: an explicit
|
|
68
|
+
* field here wins; otherwise the id is derived from the token's own JWT
|
|
69
|
+
* `sub` claim (see `decodeJwtSubject`). Either source is normalized through
|
|
70
|
+
* `normalizeWorkosUserId` to strip a provider prefix.
|
|
71
|
+
*/
|
|
72
|
+
export const CURSOR_INDIVIDUAL_USER_ID_KEYS = [
|
|
73
|
+
"userId",
|
|
74
|
+
"user_id",
|
|
75
|
+
"workosUserId",
|
|
76
|
+
"workos_user_id",
|
|
77
|
+
"sub",
|
|
78
|
+
];
|
|
79
|
+
/** Nested objects tolerated one level deep (same key allowlists apply). */
|
|
80
|
+
const NESTED_KEYS = ["auth", "data", "user"];
|
|
81
|
+
function homeDir() {
|
|
82
|
+
const override = process.env[CURSOR_INDIVIDUAL_HOME_ENV]?.trim();
|
|
83
|
+
if (override)
|
|
84
|
+
return override;
|
|
85
|
+
return os.homedir();
|
|
86
|
+
}
|
|
87
|
+
/** Ordered candidate files: explicit override first, then the local login. */
|
|
88
|
+
export function cursorIndividualCredentialCandidates() {
|
|
89
|
+
const override = process.env[CURSOR_INDIVIDUAL_CREDENTIAL_FILE_ENV]?.trim();
|
|
90
|
+
if (override)
|
|
91
|
+
return [override];
|
|
92
|
+
return [path.join(homeDir(), CURSOR_INDIVIDUAL_DEFAULT_RELATIVE_PATH)];
|
|
93
|
+
}
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Cursor desktop login (NOT-267, primary source).
|
|
96
|
+
//
|
|
97
|
+
// Cursor desktop keeps its login in the standard VS Code global-storage
|
|
98
|
+
// SQLite database (`state.vscdb`), under the key
|
|
99
|
+
// `cursorAuth/accessToken`. Community tools (ai-usagebar and ports) read
|
|
100
|
+
// that same key first and fall back to a Cursor Agent `auth.json` — this
|
|
101
|
+
// module follows the same order.
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
/** The ONLY desktop-state key this module ever reads — never anything else. */
|
|
104
|
+
export const CURSOR_DESKTOP_ACCESS_TOKEN_KEY = "cursorAuth/accessToken";
|
|
105
|
+
/** Format label for a credential answered by the desktop state database. */
|
|
106
|
+
export const CURSOR_DESKTOP_FORMAT = "desktop:cursorAuth/accessToken";
|
|
107
|
+
/**
|
|
108
|
+
* Ordered desktop-state database candidates: exactly one default path per
|
|
109
|
+
* OS (plus the explicit file override when set). The module never hunts
|
|
110
|
+
* beyond these — an absent database degrades to the Agent auth fallback,
|
|
111
|
+
* never to a scan of unrelated files.
|
|
112
|
+
*
|
|
113
|
+
* - macOS: `~/Library/Application Support/Cursor/User/globalStorage/state.vscdb`
|
|
114
|
+
* - Windows: `%APPDATA%/Cursor/User/globalStorage/state.vscdb`
|
|
115
|
+
* - Linux (and anything else): `~/.config/Cursor/User/globalStorage/state.vscdb`
|
|
116
|
+
*/
|
|
117
|
+
export function cursorIndividualDesktopStateCandidates(opts = {}) {
|
|
118
|
+
const override = process.env[CURSOR_INDIVIDUAL_DESKTOP_STATE_FILE_ENV]?.trim();
|
|
119
|
+
if (override)
|
|
120
|
+
return [override];
|
|
121
|
+
const platform = opts.platform ?? process.platform;
|
|
122
|
+
const home = opts.home ?? homeDir();
|
|
123
|
+
if (platform === "win32") {
|
|
124
|
+
// An explicit `appData` opt always wins (even an explicit empty string,
|
|
125
|
+
// which reads as no candidate). A present-but-blank APPDATA is the same
|
|
126
|
+
// as unset — fall back to the roaming default instead of resolving to [].
|
|
127
|
+
const envAppData = process.env.APPDATA?.trim();
|
|
128
|
+
const appData = opts.appData ?? (envAppData ? envAppData : path.join(home, "AppData", "Roaming"));
|
|
129
|
+
if (!appData)
|
|
130
|
+
return [];
|
|
131
|
+
return [path.join(appData, "Cursor", "User", "globalStorage", "state.vscdb")];
|
|
132
|
+
}
|
|
133
|
+
if (platform === "darwin") {
|
|
134
|
+
return [
|
|
135
|
+
path.join(home, "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb"),
|
|
136
|
+
];
|
|
137
|
+
}
|
|
138
|
+
return [path.join(home, ".config", "Cursor", "User", "globalStorage", "state.vscdb")];
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Normalize one raw desktop-state value to a token. Cursor stores the
|
|
142
|
+
* access token as a plain-text JWT; a JSON-quoted wrapping is tolerated
|
|
143
|
+
* (same bytes, quoted), anything else is unusable — never a guess.
|
|
144
|
+
*/
|
|
145
|
+
export function normalizeDesktopTokenValue(value) {
|
|
146
|
+
if (typeof value !== "string")
|
|
147
|
+
return null;
|
|
148
|
+
const trimmed = value.trim();
|
|
149
|
+
if (!trimmed)
|
|
150
|
+
return null;
|
|
151
|
+
if (trimmed.startsWith('"')) {
|
|
152
|
+
try {
|
|
153
|
+
const inner = JSON.parse(trimmed);
|
|
154
|
+
return typeof inner === "string" && inner.trim() ? inner.trim() : null;
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return trimmed;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Open the desktop state database READ-ONLY and read ONLY the allowlisted
|
|
164
|
+
* access-token key. Never throws: a missing/locked database, a missing key,
|
|
165
|
+
* or an unusable value all read null. The database is never copied, never
|
|
166
|
+
* written, and no unrelated key is ever selected.
|
|
167
|
+
*/
|
|
168
|
+
export function defaultReadDesktopAccessToken(dbPath) {
|
|
169
|
+
let db = null;
|
|
170
|
+
try {
|
|
171
|
+
db = new Database(dbPath, { readonly: true, fileMustExist: true });
|
|
172
|
+
const row = db
|
|
173
|
+
.prepare("SELECT value FROM ItemTable WHERE key = ?")
|
|
174
|
+
.get(CURSOR_DESKTOP_ACCESS_TOKEN_KEY);
|
|
175
|
+
if (!row)
|
|
176
|
+
return null;
|
|
177
|
+
return normalizeDesktopTokenValue(row.value);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
try {
|
|
184
|
+
db?.close();
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
// Read-only handle cleanup is best-effort; the read verdict stands.
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Build a dashboard auth header from a desktop access token. The desktop
|
|
193
|
+
* store carries no separate user-id field, so the id is derived from the
|
|
194
|
+
* token's own JWT `sub` claim through the existing normalization — a token
|
|
195
|
+
* with no derivable id is unusable (null), never a Bearer-only guess.
|
|
196
|
+
*/
|
|
197
|
+
export function desktopTokenToAuthHeader(token) {
|
|
198
|
+
const rawUserId = decodeJwtSubject(token);
|
|
199
|
+
if (!rawUserId)
|
|
200
|
+
return null;
|
|
201
|
+
const userId = normalizeWorkosUserId(rawUserId);
|
|
202
|
+
if (!userId)
|
|
203
|
+
return null;
|
|
204
|
+
return cursorIndividualAuthHeader(userId, token);
|
|
205
|
+
}
|
|
206
|
+
function pickByKeys(obj, keys) {
|
|
207
|
+
for (const key of keys) {
|
|
208
|
+
const value = obj[key];
|
|
209
|
+
if (typeof value === "string" && value.trim())
|
|
210
|
+
return { key, value: value.trim() };
|
|
211
|
+
}
|
|
212
|
+
for (const nest of NESTED_KEYS) {
|
|
213
|
+
const inner = obj[nest];
|
|
214
|
+
if (inner && typeof inner === "object" && !Array.isArray(inner)) {
|
|
215
|
+
for (const key of keys) {
|
|
216
|
+
const value = inner[key];
|
|
217
|
+
if (typeof value === "string" && value.trim()) {
|
|
218
|
+
return { key: `${nest}.${key}`, value: value.trim() };
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
function pickToken(obj) {
|
|
226
|
+
const picked = pickByKeys(obj, CURSOR_INDIVIDUAL_TOKEN_KEYS);
|
|
227
|
+
return picked ? { key: picked.key, token: picked.value } : null;
|
|
228
|
+
}
|
|
229
|
+
function pickUserId(obj) {
|
|
230
|
+
return pickByKeys(obj, CURSOR_INDIVIDUAL_USER_ID_KEYS)?.value ?? null;
|
|
231
|
+
}
|
|
232
|
+
function base64UrlDecode(segment) {
|
|
233
|
+
try {
|
|
234
|
+
const padded = segment.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(segment.length / 4) * 4, "=");
|
|
235
|
+
return Buffer.from(padded, "base64").toString("utf8");
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* The WorkOS session id is normally the JWT's own `sub` claim — decoded
|
|
243
|
+
* locally (no network, no signature verification: this only *reads* an id
|
|
244
|
+
* already present in a credential the caller trusts) so a plain access-token
|
|
245
|
+
* file still yields a usable id without a separate stored field. WorkOS
|
|
246
|
+
* subjects are commonly provider-prefixed (e.g. `google-oauth2|user_abc`);
|
|
247
|
+
* the raw claim is returned as-is here — `normalizeWorkosUserId` strips the
|
|
248
|
+
* prefix, applied uniformly to both this and an explicit `userId` field so
|
|
249
|
+
* either source lands on the same bare id.
|
|
250
|
+
*/
|
|
251
|
+
export function decodeJwtSubject(token) {
|
|
252
|
+
const parts = token.split(".");
|
|
253
|
+
if (parts.length !== 3)
|
|
254
|
+
return null;
|
|
255
|
+
const json = base64UrlDecode(parts[1]);
|
|
256
|
+
if (json === null)
|
|
257
|
+
return null;
|
|
258
|
+
try {
|
|
259
|
+
const payload = JSON.parse(json);
|
|
260
|
+
if (!payload || typeof payload !== "object")
|
|
261
|
+
return null;
|
|
262
|
+
const sub = payload.sub;
|
|
263
|
+
return typeof sub === "string" && sub.trim() ? sub.trim() : null;
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Strips a WorkOS connection-type prefix (`google-oauth2|user_abc` →
|
|
271
|
+
* `user_abc`): the dashboard's own session cookie carries the bare id after
|
|
272
|
+
* the final `|`, never the provider-qualified subject.
|
|
273
|
+
*/
|
|
274
|
+
export function normalizeWorkosUserId(id) {
|
|
275
|
+
const at = id.lastIndexOf("|");
|
|
276
|
+
return at === -1 ? id : id.slice(at + 1);
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Dashboard auth scheme: a `WorkosCursorSessionToken` cookie of
|
|
280
|
+
* `<userId>%3A%3A<token>` (community-observed: cursor-pulse, oh-my-pi,
|
|
281
|
+
* vct-core) — the dashboard rejects a Bearer `Authorization` header, and the
|
|
282
|
+
* `::` delimiter itself is percent-encoded in the live cookie value, not
|
|
283
|
+
* sent literally. The scheme is part of the undocumented surface and may
|
|
284
|
+
* drift (drift reads `unparsable`/`forbidden` at the HTTP layer, never a
|
|
285
|
+
* credential guess here). Returns the `Cookie` header VALUE (the caller
|
|
286
|
+
* sends it under the `Cookie` header name).
|
|
287
|
+
*/
|
|
288
|
+
export function cursorIndividualAuthHeader(userId, token) {
|
|
289
|
+
return `WorkosCursorSessionToken=${userId}%3A%3A${token}`;
|
|
290
|
+
}
|
|
291
|
+
function defaultReadFile(filePath) {
|
|
292
|
+
try {
|
|
293
|
+
return fs.readFileSync(filePath, "utf8");
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Find a usable local Cursor credential, desktop first, Agent auth as the
|
|
301
|
+
* fallback. Never throws: unreadable files, invalid JSON, unknown shapes,
|
|
302
|
+
* and unusable desktop tokens all map to `absent`/`unparsable`.
|
|
303
|
+
* The raw token leaves this module only inside `authHeader`, held in memory
|
|
304
|
+
* for the single dashboard call the adapter makes with it.
|
|
305
|
+
*
|
|
306
|
+
* When `CURSOR_INDIVIDUAL_CREDENTIAL_FILE` pins an explicit file, ONLY that
|
|
307
|
+
* file is consulted (legacy single-source behavior for fixtures); otherwise
|
|
308
|
+
* the desktop state database is tried first and the Agent auth file second.
|
|
309
|
+
*/
|
|
310
|
+
export function loadCursorIndividualCredential(readFile = defaultReadFile, readDesktopToken = defaultReadDesktopAccessToken) {
|
|
311
|
+
const explicitFile = process.env[CURSOR_INDIVIDUAL_CREDENTIAL_FILE_ENV]?.trim();
|
|
312
|
+
// A present-but-unusable desktop login is remembered (not returned yet):
|
|
313
|
+
// the Agent fallback still gets its chance — any usable credential wins
|
|
314
|
+
// over a precise-but-unusable verdict. When nothing is usable, the
|
|
315
|
+
// desktop verdict is the most precise diagnostic, so it wins over the
|
|
316
|
+
// Agent one.
|
|
317
|
+
let desktopUnparsable = null;
|
|
318
|
+
if (!explicitFile) {
|
|
319
|
+
const desktop = loadDesktopCredential(readDesktopToken);
|
|
320
|
+
if (desktop?.found)
|
|
321
|
+
return desktop.found;
|
|
322
|
+
desktopUnparsable = desktop?.unparsable ?? null;
|
|
323
|
+
}
|
|
324
|
+
const agentUnparsable = (candidate, format) => desktopUnparsable ?? { status: "unparsable", source: null, path: candidate, format };
|
|
325
|
+
for (const candidate of cursorIndividualCredentialCandidates()) {
|
|
326
|
+
const raw = readFile(candidate);
|
|
327
|
+
if (raw === null)
|
|
328
|
+
continue;
|
|
329
|
+
let parsed;
|
|
330
|
+
try {
|
|
331
|
+
parsed = JSON.parse(raw);
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
return agentUnparsable(candidate, null);
|
|
335
|
+
}
|
|
336
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
337
|
+
return agentUnparsable(candidate, null);
|
|
338
|
+
}
|
|
339
|
+
const picked = pickToken(parsed);
|
|
340
|
+
if (!picked)
|
|
341
|
+
return agentUnparsable(candidate, null);
|
|
342
|
+
// The cookie needs a user id too — an explicit field wins; otherwise
|
|
343
|
+
// derive it from the token's own JWT `sub` claim. Neither present means
|
|
344
|
+
// this credential cannot build a usable session, same as any other
|
|
345
|
+
// format drift. Either source may be provider-prefixed
|
|
346
|
+
// (`google-oauth2|user_abc`) — normalize both the same way.
|
|
347
|
+
const rawUserId = pickUserId(parsed) ?? decodeJwtSubject(picked.token);
|
|
348
|
+
if (!rawUserId)
|
|
349
|
+
return agentUnparsable(candidate, picked.key);
|
|
350
|
+
// Re-check AFTER normalization: an id ending in `|` (e.g. a bare
|
|
351
|
+
// `"google-oauth2|"` with nothing after it) is non-empty here but
|
|
352
|
+
// normalizes to "" — that must degrade too, not build a cookie with an
|
|
353
|
+
// empty user id.
|
|
354
|
+
const userId = normalizeWorkosUserId(rawUserId);
|
|
355
|
+
if (!userId)
|
|
356
|
+
return agentUnparsable(candidate, picked.key);
|
|
357
|
+
return {
|
|
358
|
+
status: "found",
|
|
359
|
+
source: "agent",
|
|
360
|
+
path: candidate,
|
|
361
|
+
format: picked.key,
|
|
362
|
+
authHeader: cursorIndividualAuthHeader(userId, picked.token),
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
return desktopUnparsable ?? { status: "absent", source: null, path: null, format: null };
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Try the desktop state database. Returns null when no desktop database
|
|
369
|
+
* was readable at all (fall through to Agent auth); otherwise the desktop
|
|
370
|
+
* verdict — `found` when the access token yields a usable session, or an
|
|
371
|
+
* `unparsable` diagnostic when the database answered but the token cannot
|
|
372
|
+
* (missing key, empty value, no derivable user id). A present-but-unusable
|
|
373
|
+
* desktop login is format drift, not absence — but the Agent fallback still
|
|
374
|
+
* gets its chance via the caller, which prefers any usable credential.
|
|
375
|
+
*/
|
|
376
|
+
function loadDesktopCredential(readDesktopToken) {
|
|
377
|
+
let sawDatabase = false;
|
|
378
|
+
let unparsable = null;
|
|
379
|
+
for (const dbPath of cursorIndividualDesktopStateCandidates()) {
|
|
380
|
+
let token;
|
|
381
|
+
try {
|
|
382
|
+
token = readDesktopToken(dbPath);
|
|
383
|
+
}
|
|
384
|
+
catch {
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
if (token === null)
|
|
388
|
+
continue;
|
|
389
|
+
sawDatabase = true;
|
|
390
|
+
const authHeader = desktopTokenToAuthHeader(token);
|
|
391
|
+
if (authHeader) {
|
|
392
|
+
return {
|
|
393
|
+
found: {
|
|
394
|
+
status: "found",
|
|
395
|
+
source: "desktop",
|
|
396
|
+
path: dbPath,
|
|
397
|
+
format: CURSOR_DESKTOP_FORMAT,
|
|
398
|
+
authHeader,
|
|
399
|
+
},
|
|
400
|
+
unparsable: null,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
// Token present but no derivable user id — same verdict as an Agent
|
|
404
|
+
// token with no id: changed format, never a Bearer-only guess.
|
|
405
|
+
if (!unparsable) {
|
|
406
|
+
unparsable = { status: "unparsable", source: null, path: dbPath, format: CURSOR_DESKTOP_FORMAT };
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
if (!sawDatabase && !unparsable)
|
|
410
|
+
return null;
|
|
411
|
+
return { found: null, unparsable };
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Diagnostics-safe credential presence: delegates to the loader, then
|
|
415
|
+
* strips the in-memory secret. Safe to expose via logs and API responses.
|
|
416
|
+
*/
|
|
417
|
+
export function cursorIndividualCredentialStatus(readFile = defaultReadFile, readDesktopToken = defaultReadDesktopAccessToken) {
|
|
418
|
+
const credential = loadCursorIndividualCredential(readFile, readDesktopToken);
|
|
419
|
+
return {
|
|
420
|
+
present: credential.status === "found",
|
|
421
|
+
source: credential.source,
|
|
422
|
+
path: credential.path,
|
|
423
|
+
format: credential.format,
|
|
424
|
+
};
|
|
425
|
+
}
|