agent-dealer 1.2.1 → 1.2.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.
Files changed (43) hide show
  1. package/bundle/server/dist/adapters/linear-inbox.js +9 -5
  2. package/bundle/server/dist/capacity/adapter.js +2 -0
  3. package/bundle/server/dist/capacity/claude-events.js +18 -0
  4. package/bundle/server/dist/capacity/claude-events.test.js +7 -0
  5. package/bundle/server/dist/capacity/codex-app-server.js +100 -10
  6. package/bundle/server/dist/capacity/codex-app-server.test.js +195 -2
  7. package/bundle/server/dist/capacity/cursor-individual-credentials.js +238 -0
  8. package/bundle/server/dist/capacity/cursor-individual-credentials.test.js +185 -0
  9. package/bundle/server/dist/capacity/cursor-individual.js +970 -0
  10. package/bundle/server/dist/capacity/cursor-individual.test.js +815 -0
  11. package/bundle/server/dist/capacity/muse.js +94 -43
  12. package/bundle/server/dist/capacity/muse.test.js +95 -13
  13. package/bundle/server/dist/capacity/service.js +1 -0
  14. package/bundle/server/dist/db/index.js +30 -0
  15. package/bundle/server/dist/db/schema.sql +27 -0
  16. package/bundle/server/dist/index.js +2 -0
  17. package/bundle/server/dist/repository/cursor-individual-billing.js +84 -0
  18. package/bundle/server/dist/repository/repository-mappings.js +46 -0
  19. package/bundle/server/dist/repository/repository-mappings.test.js +83 -0
  20. package/bundle/server/dist/repository/runtime-capacity.js +8 -3
  21. package/bundle/server/dist/repository/runtime-capacity.test.js +4 -0
  22. package/bundle/server/dist/routes/cursor-individual-billing.test.js +80 -0
  23. package/bundle/server/dist/routes/index.js +32 -0
  24. package/bundle/server/dist/routes/intake-linear.test.js +55 -1
  25. package/bundle/server/dist/routes/repository-mappings.js +22 -0
  26. package/bundle/server/dist/routes/repository-mappings.test.js +110 -0
  27. package/bundle/server/dist/routes/runtime-capacity.test.js +72 -2
  28. package/bundle/server/package.json +2 -2
  29. package/bundle/server/static-ui/assets/index-0GDYfbaM.js +60 -0
  30. package/bundle/server/static-ui/assets/index-DUYzboWh.css +1 -0
  31. package/bundle/server/static-ui/index.html +2 -2
  32. package/bundle/shared/dist/index.d.ts +6 -6
  33. package/bundle/shared/dist/linear-intake.d.ts +71 -0
  34. package/bundle/shared/dist/linear-intake.js +90 -0
  35. package/bundle/shared/dist/linear-intake.test.js +80 -1
  36. package/bundle/shared/dist/runtime-capacity.d.ts +89 -0
  37. package/bundle/shared/dist/runtime-capacity.js +60 -0
  38. package/bundle/shared/dist/runtime-capacity.test.js +1 -0
  39. package/bundle/shared/package.json +1 -1
  40. package/dist/doctor.js +6 -0
  41. package/package.json +1 -1
  42. package/bundle/server/static-ui/assets/index-ChnI0F02.css +0 -1
  43. package/bundle/server/static-ui/assets/index-CyB1573Q.js +0 -60
@@ -0,0 +1,238 @@
1
+ // packages/server/src/capacity/cursor-individual-credentials.ts
2
+ //
3
+ // NOT-250: isolated credential lookup for the experimental Cursor Individual
4
+ // dashboard adapter.
5
+ //
6
+ // The dashboard endpoints this feature calls are undocumented: the only
7
+ // credential available is the existing local Cursor login, whose file
8
+ // location and JSON shape are themselves not a supported contract and may
9
+ // change without notice. All of that risk lives in this one module:
10
+ //
11
+ // - Candidate files are tried in order; the first readable file wins.
12
+ // - Accepted JSON shapes are an explicit allowlist below — anything else
13
+ // reads `unparsable` (changed format), never a guess.
14
+ // - `cursorIndividualCredentialStatus()` (the diagnostics surface) returns
15
+ // presence/path/format ONLY — never secret material.
16
+ // - `loadCursorIndividualCredential()` additionally returns an in-memory
17
+ // `authHeader` for the dashboard HTTP call. The header value must never be
18
+ // persisted, returned to the browser, or logged — the adapter, repository,
19
+ // and route layers enforce that, and tests pin it.
20
+ //
21
+ // No silent opt-in or credential migration lives here: this module only
22
+ // *finds* credentials; whether they may be *used* is decided by the
23
+ // explicit `AGENT_DEALER_CURSOR_INDIVIDUAL_CAPACITY=experimental` opt-in in
24
+ // cursor-individual.ts, which short-circuits before touching this module.
25
+ import fs from "node:fs";
26
+ import os from "node:os";
27
+ import path from "node:path";
28
+ /** Explicit credential-file override (tests point this at tmp fixtures). */
29
+ export const CURSOR_INDIVIDUAL_CREDENTIAL_FILE_ENV = "CURSOR_INDIVIDUAL_CREDENTIAL_FILE";
30
+ /** Home-directory override for tests (mirrors how OS home resolution works). */
31
+ export const CURSOR_INDIVIDUAL_HOME_ENV = "CURSOR_INDIVIDUAL_HOME";
32
+ /**
33
+ * Community-observed local Cursor login file. The location is undocumented
34
+ * and may move — when it does, reads degrade to `absent`/`unparsable`, never
35
+ * to a scan of unrelated files. Exactly one default candidate is listed on
36
+ * purpose: this module must not go hunting through the home directory.
37
+ */
38
+ export const CURSOR_INDIVIDUAL_DEFAULT_RELATIVE_PATH = path.join(".cursor", "auth.json");
39
+ /**
40
+ * Accepted top-level token keys. The credential format is undocumented, so
41
+ * both camelCase and snake_case spellings are tolerated — but ONLY these
42
+ * keys. An object with none of them is a changed format (`unparsable`).
43
+ */
44
+ export const CURSOR_INDIVIDUAL_TOKEN_KEYS = [
45
+ "token",
46
+ "accessToken",
47
+ "access_token",
48
+ "sessionToken",
49
+ "session_token",
50
+ "authToken",
51
+ "auth_token",
52
+ "apiKey",
53
+ "api_key",
54
+ "jwt",
55
+ ];
56
+ /**
57
+ * Accepted top-level WorkOS user-id keys. The dashboard's session cookie is
58
+ * `<userId>%3A%3A<token>` (see `cursorIndividualAuthHeader`) — a bare token
59
+ * is not enough. Same allowlist discipline as the token keys: an explicit
60
+ * field here wins; otherwise the id is derived from the token's own JWT
61
+ * `sub` claim (see `decodeJwtSubject`). Either source is normalized through
62
+ * `normalizeWorkosUserId` to strip a provider prefix.
63
+ */
64
+ export const CURSOR_INDIVIDUAL_USER_ID_KEYS = [
65
+ "userId",
66
+ "user_id",
67
+ "workosUserId",
68
+ "workos_user_id",
69
+ "sub",
70
+ ];
71
+ /** Nested objects tolerated one level deep (same key allowlists apply). */
72
+ const NESTED_KEYS = ["auth", "data", "user"];
73
+ function homeDir() {
74
+ const override = process.env[CURSOR_INDIVIDUAL_HOME_ENV]?.trim();
75
+ if (override)
76
+ return override;
77
+ return os.homedir();
78
+ }
79
+ /** Ordered candidate files: explicit override first, then the local login. */
80
+ export function cursorIndividualCredentialCandidates() {
81
+ const override = process.env[CURSOR_INDIVIDUAL_CREDENTIAL_FILE_ENV]?.trim();
82
+ if (override)
83
+ return [override];
84
+ return [path.join(homeDir(), CURSOR_INDIVIDUAL_DEFAULT_RELATIVE_PATH)];
85
+ }
86
+ function pickByKeys(obj, keys) {
87
+ for (const key of keys) {
88
+ const value = obj[key];
89
+ if (typeof value === "string" && value.trim())
90
+ return { key, value: value.trim() };
91
+ }
92
+ for (const nest of NESTED_KEYS) {
93
+ const inner = obj[nest];
94
+ if (inner && typeof inner === "object" && !Array.isArray(inner)) {
95
+ for (const key of keys) {
96
+ const value = inner[key];
97
+ if (typeof value === "string" && value.trim()) {
98
+ return { key: `${nest}.${key}`, value: value.trim() };
99
+ }
100
+ }
101
+ }
102
+ }
103
+ return null;
104
+ }
105
+ function pickToken(obj) {
106
+ const picked = pickByKeys(obj, CURSOR_INDIVIDUAL_TOKEN_KEYS);
107
+ return picked ? { key: picked.key, token: picked.value } : null;
108
+ }
109
+ function pickUserId(obj) {
110
+ return pickByKeys(obj, CURSOR_INDIVIDUAL_USER_ID_KEYS)?.value ?? null;
111
+ }
112
+ function base64UrlDecode(segment) {
113
+ try {
114
+ const padded = segment.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(segment.length / 4) * 4, "=");
115
+ return Buffer.from(padded, "base64").toString("utf8");
116
+ }
117
+ catch {
118
+ return null;
119
+ }
120
+ }
121
+ /**
122
+ * The WorkOS session id is normally the JWT's own `sub` claim — decoded
123
+ * locally (no network, no signature verification: this only *reads* an id
124
+ * already present in a credential the caller trusts) so a plain access-token
125
+ * file still yields a usable id without a separate stored field. WorkOS
126
+ * subjects are commonly provider-prefixed (e.g. `google-oauth2|user_abc`);
127
+ * the raw claim is returned as-is here — `normalizeWorkosUserId` strips the
128
+ * prefix, applied uniformly to both this and an explicit `userId` field so
129
+ * either source lands on the same bare id.
130
+ */
131
+ export function decodeJwtSubject(token) {
132
+ const parts = token.split(".");
133
+ if (parts.length !== 3)
134
+ return null;
135
+ const json = base64UrlDecode(parts[1]);
136
+ if (json === null)
137
+ return null;
138
+ try {
139
+ const payload = JSON.parse(json);
140
+ if (!payload || typeof payload !== "object")
141
+ return null;
142
+ const sub = payload.sub;
143
+ return typeof sub === "string" && sub.trim() ? sub.trim() : null;
144
+ }
145
+ catch {
146
+ return null;
147
+ }
148
+ }
149
+ /**
150
+ * Strips a WorkOS connection-type prefix (`google-oauth2|user_abc` →
151
+ * `user_abc`): the dashboard's own session cookie carries the bare id after
152
+ * the final `|`, never the provider-qualified subject.
153
+ */
154
+ export function normalizeWorkosUserId(id) {
155
+ const at = id.lastIndexOf("|");
156
+ return at === -1 ? id : id.slice(at + 1);
157
+ }
158
+ /**
159
+ * Dashboard auth scheme: a `WorkosCursorSessionToken` cookie of
160
+ * `<userId>%3A%3A<token>` (community-observed: cursor-pulse, oh-my-pi,
161
+ * vct-core) — the dashboard rejects a Bearer `Authorization` header, and the
162
+ * `::` delimiter itself is percent-encoded in the live cookie value, not
163
+ * sent literally. The scheme is part of the undocumented surface and may
164
+ * drift (drift reads `unparsable`/`forbidden` at the HTTP layer, never a
165
+ * credential guess here). Returns the `Cookie` header VALUE (the caller
166
+ * sends it under the `Cookie` header name).
167
+ */
168
+ export function cursorIndividualAuthHeader(userId, token) {
169
+ return `WorkosCursorSessionToken=${userId}%3A%3A${token}`;
170
+ }
171
+ function defaultReadFile(filePath) {
172
+ try {
173
+ return fs.readFileSync(filePath, "utf8");
174
+ }
175
+ catch {
176
+ return null;
177
+ }
178
+ }
179
+ /**
180
+ * Find a usable local Cursor credential. Never throws: unreadable files,
181
+ * invalid JSON, and unknown shapes all map to `absent`/`unparsable`.
182
+ * The raw token leaves this module only inside `authHeader`, held in memory
183
+ * for the single dashboard call the adapter makes with it.
184
+ */
185
+ export function loadCursorIndividualCredential(readFile = defaultReadFile) {
186
+ for (const candidate of cursorIndividualCredentialCandidates()) {
187
+ const raw = readFile(candidate);
188
+ if (raw === null)
189
+ continue;
190
+ let parsed;
191
+ try {
192
+ parsed = JSON.parse(raw);
193
+ }
194
+ catch {
195
+ return { status: "unparsable", path: candidate, format: null };
196
+ }
197
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
198
+ return { status: "unparsable", path: candidate, format: null };
199
+ }
200
+ const picked = pickToken(parsed);
201
+ if (!picked)
202
+ return { status: "unparsable", path: candidate, format: null };
203
+ // The cookie needs a user id too — an explicit field wins; otherwise
204
+ // derive it from the token's own JWT `sub` claim. Neither present means
205
+ // this credential cannot build a usable session, same as any other
206
+ // format drift. Either source may be provider-prefixed
207
+ // (`google-oauth2|user_abc`) — normalize both the same way.
208
+ const rawUserId = pickUserId(parsed) ?? decodeJwtSubject(picked.token);
209
+ if (!rawUserId)
210
+ return { status: "unparsable", path: candidate, format: picked.key };
211
+ // Re-check AFTER normalization: an id ending in `|` (e.g. a bare
212
+ // `"google-oauth2|"` with nothing after it) is non-empty here but
213
+ // normalizes to "" — that must degrade too, not build a cookie with an
214
+ // empty user id.
215
+ const userId = normalizeWorkosUserId(rawUserId);
216
+ if (!userId)
217
+ return { status: "unparsable", path: candidate, format: picked.key };
218
+ return {
219
+ status: "found",
220
+ path: candidate,
221
+ format: picked.key,
222
+ authHeader: cursorIndividualAuthHeader(userId, picked.token),
223
+ };
224
+ }
225
+ return { status: "absent", path: null, format: null };
226
+ }
227
+ /**
228
+ * Diagnostics-safe credential presence: delegates to the loader, then
229
+ * strips the in-memory secret. Safe to expose via logs and API responses.
230
+ */
231
+ export function cursorIndividualCredentialStatus(readFile = defaultReadFile) {
232
+ const credential = loadCursorIndividualCredential(readFile);
233
+ return {
234
+ present: credential.status === "found",
235
+ path: credential.path,
236
+ format: credential.format,
237
+ };
238
+ }
@@ -0,0 +1,185 @@
1
+ // packages/server/src/capacity/cursor-individual-credentials.test.ts
2
+ //
3
+ // NOT-250: credential lookup is isolated behind an adapter, returns no
4
+ // secret on diagnostics surfaces, and degrades on absent/changed formats.
5
+ // Every case uses temporary fixture files (or an injected reader) — CI
6
+ // never touches the real local Cursor login.
7
+ import { test, beforeEach, afterEach } from "node:test";
8
+ import assert from "node:assert/strict";
9
+ import fs from "node:fs";
10
+ import os from "node:os";
11
+ import path from "node:path";
12
+ import { CURSOR_INDIVIDUAL_CREDENTIAL_FILE_ENV, CURSOR_INDIVIDUAL_HOME_ENV, cursorIndividualAuthHeader, cursorIndividualCredentialCandidates, cursorIndividualCredentialStatus, decodeJwtSubject, loadCursorIndividualCredential, normalizeWorkosUserId, } from "./cursor-individual-credentials.js";
13
+ const SECRET = "fixture-secret-token-abc123";
14
+ const USER_ID = "user_fixture_abc123";
15
+ /** header.payload.signature with { sub: USER_ID } — no signature verification, so any value works. */
16
+ function fixtureJwt(sub) {
17
+ const b64url = (obj) => Buffer.from(JSON.stringify(obj)).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
18
+ return `${b64url({ alg: "none" })}.${b64url({ sub })}.sig`;
19
+ }
20
+ let dir;
21
+ let savedFileEnv;
22
+ let savedHomeEnv;
23
+ beforeEach(() => {
24
+ dir = fs.mkdtempSync(path.join(os.tmpdir(), "dealer-cursor-indiv-creds-"));
25
+ savedFileEnv = process.env[CURSOR_INDIVIDUAL_CREDENTIAL_FILE_ENV];
26
+ savedHomeEnv = process.env[CURSOR_INDIVIDUAL_HOME_ENV];
27
+ });
28
+ afterEach(() => {
29
+ if (savedFileEnv === undefined)
30
+ delete process.env[CURSOR_INDIVIDUAL_CREDENTIAL_FILE_ENV];
31
+ else
32
+ process.env[CURSOR_INDIVIDUAL_CREDENTIAL_FILE_ENV] = savedFileEnv;
33
+ if (savedHomeEnv === undefined)
34
+ delete process.env[CURSOR_INDIVIDUAL_HOME_ENV];
35
+ else
36
+ process.env[CURSOR_INDIVIDUAL_HOME_ENV] = savedHomeEnv;
37
+ fs.rmSync(dir, { recursive: true, force: true });
38
+ });
39
+ function fixture(name, content) {
40
+ const file = path.join(dir, name);
41
+ fs.writeFileSync(file, content);
42
+ process.env[CURSOR_INDIVIDUAL_CREDENTIAL_FILE_ENV] = file;
43
+ return file;
44
+ }
45
+ test("absent credential file reads absent with no path", () => {
46
+ process.env[CURSOR_INDIVIDUAL_CREDENTIAL_FILE_ENV] = path.join(dir, "does-not-exist.json");
47
+ const credential = loadCursorIndividualCredential();
48
+ assert.equal(credential.status, "absent");
49
+ assert.equal(credential.path, null);
50
+ assert.equal(credential.format, null);
51
+ assert.equal(credential.authHeader, undefined);
52
+ const status = cursorIndividualCredentialStatus();
53
+ assert.deepEqual(status, { present: false, path: null, format: null });
54
+ });
55
+ test("empty home directory reads absent (no real login touched)", () => {
56
+ delete process.env[CURSOR_INDIVIDUAL_CREDENTIAL_FILE_ENV];
57
+ process.env[CURSOR_INDIVIDUAL_HOME_ENV] = dir;
58
+ assert.deepEqual(cursorIndividualCredentialCandidates(), [
59
+ path.join(dir, ".cursor", "auth.json"),
60
+ ]);
61
+ assert.equal(loadCursorIndividualCredential().status, "absent");
62
+ assert.equal(cursorIndividualCredentialStatus().present, false);
63
+ });
64
+ test("accepted token shapes load with an in-memory auth header", () => {
65
+ const cases = [
66
+ { body: { token: SECRET, userId: USER_ID }, format: "token" },
67
+ { body: { accessToken: SECRET, userId: USER_ID }, format: "accessToken" },
68
+ { body: { access_token: SECRET, user_id: USER_ID }, format: "access_token" },
69
+ { body: { sessionToken: SECRET, userId: USER_ID }, format: "sessionToken" },
70
+ { body: { apiKey: SECRET, userId: USER_ID }, format: "apiKey" },
71
+ { body: { auth: { accessToken: SECRET }, userId: USER_ID }, format: "auth.accessToken" },
72
+ ];
73
+ for (const { body, format } of cases) {
74
+ const file = fixture("auth.json", JSON.stringify(body));
75
+ const credential = loadCursorIndividualCredential();
76
+ assert.equal(credential.status, "found");
77
+ assert.equal(credential.path, file);
78
+ assert.equal(credential.format, format);
79
+ assert.equal(credential.authHeader, cursorIndividualAuthHeader(USER_ID, SECRET));
80
+ assert.ok(credential.authHeader.includes(SECRET), "header carries the token in memory only");
81
+ }
82
+ });
83
+ test("a user id is derived from the token's own JWT `sub` claim when no explicit field is present", () => {
84
+ const jwt = fixtureJwt(USER_ID);
85
+ assert.equal(decodeJwtSubject(jwt), USER_ID);
86
+ fixture("auth.json", JSON.stringify({ token: jwt }));
87
+ const credential = loadCursorIndividualCredential();
88
+ assert.equal(credential.status, "found");
89
+ assert.equal(credential.authHeader, cursorIndividualAuthHeader(USER_ID, jwt));
90
+ });
91
+ test("an explicit user-id field wins over the token's own JWT `sub` claim", () => {
92
+ const jwt = fixtureJwt("jwt-subject-should-lose");
93
+ fixture("auth.json", JSON.stringify({ token: jwt, userId: USER_ID }));
94
+ const credential = loadCursorIndividualCredential();
95
+ assert.equal(credential.authHeader, cursorIndividualAuthHeader(USER_ID, jwt));
96
+ });
97
+ test("a token with no derivable user id reads unparsable — never a Bearer-only guess", () => {
98
+ fixture("auth.json", JSON.stringify({ token: SECRET }));
99
+ const credential = loadCursorIndividualCredential();
100
+ assert.equal(credential.status, "unparsable");
101
+ assert.equal(credential.authHeader, undefined);
102
+ });
103
+ test("the auth header is the WorkosCursorSessionToken cookie with the :: delimiter percent-encoded", () => {
104
+ const header = cursorIndividualAuthHeader(USER_ID, SECRET);
105
+ assert.equal(header, `WorkosCursorSessionToken=${USER_ID}%3A%3A${SECRET}`);
106
+ // Never the raw, unencoded delimiter the dashboard rejects.
107
+ assert.ok(!header.includes(`${USER_ID}::${SECRET}`));
108
+ });
109
+ test("normalizeWorkosUserId strips a provider connection-type prefix", () => {
110
+ assert.equal(normalizeWorkosUserId("google-oauth2|user_abc"), "user_abc");
111
+ assert.equal(normalizeWorkosUserId("github|12345"), "12345");
112
+ // A bare id (no prefix) passes through unchanged.
113
+ assert.equal(normalizeWorkosUserId(USER_ID), USER_ID);
114
+ // Only the LAST `|` matters, in case a value itself contains one.
115
+ assert.equal(normalizeWorkosUserId("a|b|c"), "c");
116
+ });
117
+ test("a provider-prefixed JWT subject is normalized before it reaches the cookie", () => {
118
+ const jwt = fixtureJwt("google-oauth2|user_fixture");
119
+ fixture("auth.json", JSON.stringify({ token: jwt }));
120
+ const credential = loadCursorIndividualCredential();
121
+ assert.equal(credential.status, "found");
122
+ // The cookie carries the bare id, never the provider-qualified subject.
123
+ assert.equal(credential.authHeader, cursorIndividualAuthHeader("user_fixture", jwt));
124
+ assert.ok(!credential.authHeader.includes("google-oauth2"));
125
+ });
126
+ test("a provider-prefixed explicit userId field is normalized the same way", () => {
127
+ fixture("auth.json", JSON.stringify({ token: SECRET, userId: "github|user_fixture" }));
128
+ const credential = loadCursorIndividualCredential();
129
+ assert.equal(credential.authHeader, cursorIndividualAuthHeader("user_fixture", SECRET));
130
+ });
131
+ test("an id that normalizes to empty (nothing after the final |) reads unparsable, never an empty-id cookie", () => {
132
+ // Non-empty before normalization (passes the raw presence check), but
133
+ // normalizeWorkosUserId("github|") === "" — must still degrade, not build
134
+ // WorkosCursorSessionToken=%3A%3A<token>.
135
+ assert.equal(normalizeWorkosUserId("github|"), "");
136
+ // Explicit-field path.
137
+ fixture("auth.json", JSON.stringify({ token: SECRET, userId: "github|" }));
138
+ const fromField = loadCursorIndividualCredential();
139
+ assert.equal(fromField.status, "unparsable");
140
+ assert.equal(fromField.authHeader, undefined);
141
+ // JWT-`sub`-derived path.
142
+ const jwt = fixtureJwt("github|");
143
+ fixture("auth.json", JSON.stringify({ token: jwt }));
144
+ const fromJwt = loadCursorIndividualCredential();
145
+ assert.equal(fromJwt.status, "unparsable");
146
+ assert.equal(fromJwt.authHeader, undefined);
147
+ });
148
+ test("changed credential formats read unparsable, never a guess", () => {
149
+ const changed = [
150
+ { totallyNewShape: true, session: { id: "abc" } },
151
+ { token: "" },
152
+ { token: " " },
153
+ { token: 12345 },
154
+ { accessToken: null },
155
+ ["token-array"],
156
+ "just-a-string",
157
+ 42,
158
+ null,
159
+ ];
160
+ for (const body of changed) {
161
+ fixture("auth.json", JSON.stringify(body));
162
+ const credential = loadCursorIndividualCredential();
163
+ assert.equal(credential.status, "unparsable", `body ${JSON.stringify(body)} must not parse`);
164
+ assert.equal(credential.authHeader, undefined);
165
+ }
166
+ fixture("auth.json", "{ not valid json{{{");
167
+ assert.equal(loadCursorIndividualCredential().status, "unparsable");
168
+ });
169
+ test("diagnostics status never carries secret material", () => {
170
+ fixture("auth.json", JSON.stringify({ token: SECRET, userId: USER_ID }));
171
+ const status = cursorIndividualCredentialStatus();
172
+ assert.equal(status.present, true);
173
+ assert.equal(status.format, "token");
174
+ assert.ok(!JSON.stringify(status).includes(SECRET), "raw token must not appear in status output");
175
+ });
176
+ test("unreadable files degrade to absent without throwing", () => {
177
+ // The default reader swallows read errors (missing file, directory at the
178
+ // candidate path, permissions): none of them throw out of the loader.
179
+ process.env[CURSOR_INDIVIDUAL_CREDENTIAL_FILE_ENV] = dir;
180
+ assert.equal(loadCursorIndividualCredential().status, "absent");
181
+ process.env[CURSOR_INDIVIDUAL_CREDENTIAL_FILE_ENV] = path.join(dir, "missing.json");
182
+ assert.equal(loadCursorIndividualCredential().status, "absent");
183
+ const dirAsFile = loadCursorIndividualCredential((_p) => null);
184
+ assert.equal(dirAsFile.status, "absent");
185
+ });