dsh-subscriptions 0.1.0

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 (150) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +129 -0
  3. package/cordis.patch.yml +12 -0
  4. package/lib/auth/claude-code-creds.d.ts +23 -0
  5. package/lib/auth/claude-code-creds.js +172 -0
  6. package/lib/auth/device-flow.d.ts +55 -0
  7. package/lib/auth/device-flow.js +177 -0
  8. package/lib/auth/jwt.d.ts +10 -0
  9. package/lib/auth/jwt.js +25 -0
  10. package/lib/auth/oauth-flow.d.ts +91 -0
  11. package/lib/auth/oauth-flow.js +227 -0
  12. package/lib/auth/pkce.d.ts +31 -0
  13. package/lib/auth/pkce.js +35 -0
  14. package/lib/auth/rpc.d.ts +209 -0
  15. package/lib/auth/rpc.js +386 -0
  16. package/lib/auth/store.d.ts +196 -0
  17. package/lib/auth/store.js +468 -0
  18. package/lib/client/CursorCard.d.ts +9 -0
  19. package/lib/client/CursorCard.js +113 -0
  20. package/lib/client/ExternalUsageCards.d.ts +9 -0
  21. package/lib/client/ExternalUsageCards.js +72 -0
  22. package/lib/client/ImageGallery.d.ts +54 -0
  23. package/lib/client/ImageGallery.js +112 -0
  24. package/lib/client/ImageGenerateToolview.d.ts +48 -0
  25. package/lib/client/ImageGenerateToolview.js +133 -0
  26. package/lib/client/ProviderAccountManager.d.ts +13 -0
  27. package/lib/client/ProviderAccountManager.js +147 -0
  28. package/lib/client/ProviderModelEditor.d.ts +39 -0
  29. package/lib/client/ProviderModelEditor.js +107 -0
  30. package/lib/client/SpeedSelect.d.ts +71 -0
  31. package/lib/client/SpeedSelect.js +177 -0
  32. package/lib/client/SubscriptionUsageBadge.d.ts +116 -0
  33. package/lib/client/SubscriptionUsageBadge.js +543 -0
  34. package/lib/client/SubscriptionsSection.d.ts +140 -0
  35. package/lib/client/SubscriptionsSection.js +544 -0
  36. package/lib/client/UsageBadgeDisplaySetting.d.ts +7 -0
  37. package/lib/client/UsageBadgeDisplaySetting.js +36 -0
  38. package/lib/client/VideoGenerateToolview.d.ts +42 -0
  39. package/lib/client/VideoGenerateToolview.js +175 -0
  40. package/lib/client/account-preferences.d.ts +24 -0
  41. package/lib/client/account-preferences.js +21 -0
  42. package/lib/client/fast-command.d.ts +2 -0
  43. package/lib/client/fast-command.js +4 -0
  44. package/lib/client/host-contract.d.ts +90 -0
  45. package/lib/client/host-contract.js +101 -0
  46. package/lib/client/host-icons.d.ts +24 -0
  47. package/lib/client/host-icons.js +21 -0
  48. package/lib/client/index.d.ts +27 -0
  49. package/lib/client/index.js +136 -0
  50. package/lib/client/locales.d.ts +297 -0
  51. package/lib/client/locales.js +295 -0
  52. package/lib/client/subscription-card-styles.d.ts +3 -0
  53. package/lib/client/subscription-card-styles.js +53 -0
  54. package/lib/client/subscriptions-rpc.d.ts +14 -0
  55. package/lib/client/subscriptions-rpc.js +34 -0
  56. package/lib/client/usage-badge-preferences.d.ts +11 -0
  57. package/lib/client/usage-badge-preferences.js +41 -0
  58. package/lib/client.js +4472 -0
  59. package/lib/client.js.map +1 -0
  60. package/lib/compat.d.ts +36 -0
  61. package/lib/compat.js +20 -0
  62. package/lib/http.d.ts +6 -0
  63. package/lib/http.js +15 -0
  64. package/lib/index.d.ts +80 -0
  65. package/lib/index.js +14018 -0
  66. package/lib/model-defaults.d.ts +23 -0
  67. package/lib/model-defaults.js +237 -0
  68. package/lib/provider-settings.d.ts +39 -0
  69. package/lib/provider-settings.js +151 -0
  70. package/lib/providers/account-preferences.d.ts +41 -0
  71. package/lib/providers/account-preferences.js +153 -0
  72. package/lib/providers/accounts.d.ts +112 -0
  73. package/lib/providers/accounts.js +142 -0
  74. package/lib/providers/antigravity-oauth-client.d.ts +7 -0
  75. package/lib/providers/antigravity-oauth-client.js +7 -0
  76. package/lib/providers/antigravity.d.ts +110 -0
  77. package/lib/providers/antigravity.js +516 -0
  78. package/lib/providers/catalog-store.d.ts +48 -0
  79. package/lib/providers/catalog-store.js +243 -0
  80. package/lib/providers/claude-cli-version.d.ts +15 -0
  81. package/lib/providers/claude-cli-version.js +24 -0
  82. package/lib/providers/claude.d.ts +181 -0
  83. package/lib/providers/claude.js +680 -0
  84. package/lib/providers/codex-client-version.d.ts +8 -0
  85. package/lib/providers/codex-client-version.js +17 -0
  86. package/lib/providers/codex-search.d.ts +29 -0
  87. package/lib/providers/codex-search.js +198 -0
  88. package/lib/providers/codex.d.ts +229 -0
  89. package/lib/providers/codex.js +821 -0
  90. package/lib/providers/common.d.ts +389 -0
  91. package/lib/providers/common.js +656 -0
  92. package/lib/providers/copilot.d.ts +344 -0
  93. package/lib/providers/copilot.js +888 -0
  94. package/lib/providers/cursor-adapter.d.ts +15 -0
  95. package/lib/providers/cursor-adapter.js +39 -0
  96. package/lib/providers/cursor-auth.d.ts +43 -0
  97. package/lib/providers/cursor-auth.js +169 -0
  98. package/lib/providers/cursor-model-settings.d.ts +15 -0
  99. package/lib/providers/cursor-model-settings.js +51 -0
  100. package/lib/providers/cursor-usage.d.ts +9 -0
  101. package/lib/providers/cursor-usage.js +112 -0
  102. package/lib/providers/external-usage-controller.d.ts +19 -0
  103. package/lib/providers/external-usage-controller.js +32 -0
  104. package/lib/providers/external-usage.d.ts +8 -0
  105. package/lib/providers/external-usage.js +82 -0
  106. package/lib/providers/grok.d.ts +205 -0
  107. package/lib/providers/grok.js +713 -0
  108. package/lib/providers/image-pool.d.ts +30 -0
  109. package/lib/providers/image-pool.js +89 -0
  110. package/lib/providers/npm-cli-version.d.ts +56 -0
  111. package/lib/providers/npm-cli-version.js +98 -0
  112. package/lib/providers/pool-family.d.ts +56 -0
  113. package/lib/providers/pool-family.js +45 -0
  114. package/lib/providers/pool-health.d.ts +74 -0
  115. package/lib/providers/pool-health.js +150 -0
  116. package/lib/providers/pool-usage.d.ts +86 -0
  117. package/lib/providers/pool-usage.js +223 -0
  118. package/lib/providers/pool.d.ts +108 -0
  119. package/lib/providers/pool.js +368 -0
  120. package/lib/providers/rate-limit.d.ts +192 -0
  121. package/lib/providers/rate-limit.js +338 -0
  122. package/lib/tools/image-generate.d.ts +128 -0
  123. package/lib/tools/image-generate.js +438 -0
  124. package/lib/tools/registration.d.ts +15 -0
  125. package/lib/tools/registration.js +24 -0
  126. package/lib/tools/video-generate.d.ts +89 -0
  127. package/lib/tools/video-generate.js +257 -0
  128. package/lib/tools/x-search.d.ts +58 -0
  129. package/lib/tools/x-search.js +197 -0
  130. package/lib/translate/anthropic.d.ts +161 -0
  131. package/lib/translate/anthropic.js +515 -0
  132. package/lib/translate/antigravity-schema.d.ts +4 -0
  133. package/lib/translate/antigravity-schema.js +145 -0
  134. package/lib/translate/antigravity-thinking.d.ts +5 -0
  135. package/lib/translate/antigravity-thinking.js +44 -0
  136. package/lib/translate/antigravity.d.ts +114 -0
  137. package/lib/translate/antigravity.js +366 -0
  138. package/lib/translate/chat-completions.d.ts +120 -0
  139. package/lib/translate/chat-completions.js +382 -0
  140. package/lib/translate/resolved.d.ts +80 -0
  141. package/lib/translate/resolved.js +118 -0
  142. package/lib/translate/responses.d.ts +179 -0
  143. package/lib/translate/responses.js +408 -0
  144. package/lib/translate/sse.d.ts +21 -0
  145. package/lib/translate/sse.js +56 -0
  146. package/package.json +114 -0
  147. package/vendor/cursor/LICENSE +21 -0
  148. package/vendor/cursor/index.d.ts +18 -0
  149. package/vendor/cursor/index.js +3574 -0
  150. package/vendor/cursor/proto.js +254 -0
@@ -0,0 +1,468 @@
1
+ /**
2
+ * On-disk OAuth session store at `~/.dsh/plugins/subscriptions/auth.json`.
3
+ *
4
+ * The file is a JSON object keyed by provider id, each entry holding that
5
+ * provider's ACCOUNTS: a map of account key → session plus the default
6
+ * account's key. Writes are atomic (tmp file + rename) with mode 0600
7
+ * because they carry bearer tokens. Session shapes live here (not in the
8
+ * provider modules) because this file owns the durable format.
9
+ *
10
+ * Backward compatibility: entries written by single-account versions hold
11
+ * the session fields directly (no `accounts` wrapper); reads migrate them
12
+ * in memory, and the next write persists the new shape — existing logins
13
+ * survive the upgrade untouched.
14
+ */
15
+ import { createHash } from 'node:crypto';
16
+ import { decodeJwtPayload } from './jwt.js';
17
+ import { chmod, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
18
+ import { dirname } from 'node:path';
19
+ import { dshHomePath } from '@deepseek-ai/dsh-home-paths';
20
+ /** Every provider route, in display order. */
21
+ export const PROVIDER_IDS = ['codex', 'claude', 'grok', 'copilot', 'antigravity'];
22
+ /**
23
+ * The stable identity of one session's account: Codex keys on workspace AND
24
+ * user (email fallback, workspace-only for unidentified legacy sessions),
25
+ * the others on their display identity, falling
26
+ * back to a refresh-token hash for sessions stored before identity fields
27
+ * existed. Logging the same account in again lands on the same key, so a
28
+ * re-login updates in place instead of duplicating. (The hash fallback can
29
+ * miss that dedup once for a legacy session re-logged with a now-known
30
+ * identity — the duplicate is visible on the Settings page and can simply
31
+ * be logged out.)
32
+ * @param provider - the provider route.
33
+ * @param session - the session to key.
34
+ * @returns the account map key.
35
+ */
36
+ export function accountKeyOf(provider, session) {
37
+ switch (provider) {
38
+ case 'codex': {
39
+ const codex = session;
40
+ const payload = typeof codex.idToken === 'string' ? decodeJwtPayload(codex.idToken) : undefined;
41
+ const auth = claimObject(payload?.['https://api.openai.com/auth']);
42
+ const user = nonEmpty(auth?.chatgpt_user_id) ?? nonEmpty(auth?.user_id);
43
+ const profile = claimObject(payload?.['https://api.openai.com/profile']);
44
+ const email = nonEmpty(codex.emailAddress) ?? nonEmpty(payload?.email) ?? nonEmpty(profile?.email);
45
+ if (user === undefined && email === undefined)
46
+ return codex.accountId;
47
+ // JSON tuple encoding avoids separator collisions and distinguishes IDs from emails.
48
+ return JSON.stringify([codex.accountId, user === undefined ? 'email' : 'user', user ?? email.toLowerCase()]);
49
+ }
50
+ case 'claude':
51
+ return session.emailAddress ?? tokenHash(session.refreshToken);
52
+ case 'grok':
53
+ return session.account ?? tokenHash(session.refreshToken);
54
+ case 'antigravity':
55
+ return session.account ?? tokenHash(session.refreshToken);
56
+ case 'copilot':
57
+ return session.account ?? tokenHash(session.refreshToken);
58
+ }
59
+ }
60
+ function nonEmpty(value) {
61
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
62
+ }
63
+ function claimObject(value) {
64
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
65
+ ? value : undefined;
66
+ }
67
+ /** Resolve only recorded aliases, not an ambiguous workspace-wide match. */
68
+ function resolveAccount(entry, key) {
69
+ const seen = new Set();
70
+ while (!Object.hasOwn(entry.accounts, key)
71
+ && entry.aliases !== undefined
72
+ && Object.hasOwn(entry.aliases, key)
73
+ && !seen.has(key)) {
74
+ seen.add(key);
75
+ key = entry.aliases[key];
76
+ }
77
+ return key;
78
+ }
79
+ /**
80
+ * Resolve a Codex reference that is neither a stored key nor a recorded alias:
81
+ * a config `account` may name the login email (as the other providers' keys
82
+ * do) or the bare workspace ID that keys stored before per-user keys. Only an
83
+ * unambiguous match resolves; two users sharing the workspace stay apart, and
84
+ * the reference is returned unchanged so the caller reports it as missing.
85
+ */
86
+ function resolveCodexReference(entry, reference) {
87
+ const wanted = reference.trim().toLowerCase();
88
+ if (wanted.length === 0)
89
+ return reference;
90
+ const matches = Object.entries(entry.accounts).filter(([, session]) => session.accountId === reference.trim() || codexEmail(session) === wanted);
91
+ return matches.length === 1 ? matches[0][0] : reference;
92
+ }
93
+ /**
94
+ * Resolve a persisted legacy account key to its canonical account identity.
95
+ * For Codex, a reference that matches no key or alias also resolves through
96
+ * a unique login email or workspace ID (see {@link resolveCodexReference}).
97
+ */
98
+ export async function resolveAccountKey(provider, account, path = authFilePath()) {
99
+ const entry = (await loadStore(path))[provider];
100
+ if (entry === undefined)
101
+ return account;
102
+ const key = resolveAccount(entry, account);
103
+ if (provider === 'codex' && !Object.hasOwn(entry.accounts, key)) {
104
+ return resolveCodexReference(entry, key);
105
+ }
106
+ return key;
107
+ }
108
+ /** Migrate workspace-only keys once; retain collisions rather than discard credentials. */
109
+ function codexEmail(session) {
110
+ const payload = typeof session.idToken === 'string' ? decodeJwtPayload(session.idToken) : undefined;
111
+ const profile = claimObject(payload?.['https://api.openai.com/profile']);
112
+ return (nonEmpty(session.emailAddress) ?? nonEmpty(payload?.email) ?? nonEmpty(profile?.email))?.toLowerCase();
113
+ }
114
+ function codexUserKey(session) {
115
+ const key = accountKeyOf('codex', session);
116
+ try {
117
+ const tuple = JSON.parse(key);
118
+ return Array.isArray(tuple) && tuple[1] === 'user' ? key : undefined;
119
+ }
120
+ catch {
121
+ return undefined;
122
+ }
123
+ }
124
+ /** Upgrade one unambiguous email fallback to the stable user key. */
125
+ function reconcileCodexIdentity(entry, session) {
126
+ const userKey = codexUserKey(session);
127
+ const email = codexEmail(session);
128
+ if (userKey === undefined || email === undefined)
129
+ return undefined;
130
+ const emailKey = JSON.stringify([session.accountId, 'email', email]);
131
+ const previous = entry.accounts[emailKey];
132
+ if (previous === undefined || codexEmail(previous) !== email)
133
+ return undefined;
134
+ // Never merge workspace-wide: the exact normalized workspace/email pair must match.
135
+ if (!Object.hasOwn(entry.accounts, userKey))
136
+ entry.accounts[userKey] = previous;
137
+ delete entry.accounts[emailKey];
138
+ entry.aliases = { ...entry.aliases, [emailKey]: userKey };
139
+ for (const [alias, target] of Object.entries(entry.aliases)) {
140
+ if (target === emailKey)
141
+ entry.aliases[alias] = userKey;
142
+ }
143
+ if (entry.default === emailKey)
144
+ entry.default = userKey;
145
+ return userKey;
146
+ }
147
+ function migrateCodex(entry) {
148
+ for (const [oldKey, session] of Object.entries(entry.accounts)) {
149
+ if (oldKey !== session.accountId)
150
+ continue;
151
+ const key = accountKeyOf('codex', session);
152
+ if (key === oldKey || Object.hasOwn(entry.accounts, key))
153
+ continue;
154
+ entry.accounts[key] = session;
155
+ delete entry.accounts[oldKey];
156
+ entry.aliases = { ...entry.aliases, [oldKey]: key };
157
+ if (entry.default === oldKey)
158
+ entry.default = key;
159
+ }
160
+ }
161
+ /** Short stable hash for sessions without an identity field. */
162
+ function tokenHash(refreshToken) {
163
+ return `token-${createHash('sha256').update(refreshToken).digest('hex').slice(0, 16)}`;
164
+ }
165
+ /**
166
+ * Absolute path of the auth store file.
167
+ * @returns `dshHomePath('plugins', 'subscriptions', 'auth.json')`.
168
+ */
169
+ export function authFilePath() {
170
+ return dshHomePath('plugins', 'subscriptions', 'auth.json');
171
+ }
172
+ /** Store location used before the plugin was renamed; migrated on first read. */
173
+ function legacyAuthFilePath() {
174
+ return dshHomePath('plugins', 'router', 'auth.json');
175
+ }
176
+ /** Check that one durable session carries the fields every session needs. */
177
+ function assertSessionShape(provider, account, value) {
178
+ if (typeof value !== 'object' || value === null) {
179
+ throw new Error(`subscriptions auth store: entry "${provider}/${account}" is not an object; fix or delete the store file`);
180
+ }
181
+ const entry = value;
182
+ if (typeof entry.accessToken !== 'string' || entry.accessToken.length === 0
183
+ || typeof entry.refreshToken !== 'string' || entry.refreshToken.length === 0
184
+ || typeof entry.expiresAt !== 'number' || !Number.isFinite(entry.expiresAt)) {
185
+ throw new Error(`subscriptions auth store: entry "${provider}/${account}" is missing accessToken/refreshToken/expiresAt; fix or delete the store file`);
186
+ }
187
+ if (provider === 'antigravity'
188
+ && (typeof entry.projectId !== 'string' || entry.projectId.length === 0)) {
189
+ throw new Error('subscriptions auth store: entry "antigravity" is missing projectId; log out and complete Antigravity login again');
190
+ }
191
+ }
192
+ /**
193
+ * Read the whole store. A missing file is an empty store; malformed JSON or a
194
+ * malformed entry throws, because silently discarding tokens would strand the
195
+ * user without a diagnosis. Single-account entries are migrated in memory;
196
+ * the next write persists the new shape.
197
+ * @param path - store file path; defaults to {@link authFilePath}.
198
+ * @returns the parsed session map.
199
+ */
200
+ export async function loadStore(path = authFilePath()) {
201
+ let text;
202
+ try {
203
+ text = await readFile(path, 'utf8');
204
+ }
205
+ catch (error) {
206
+ if (error.code !== 'ENOENT')
207
+ throw error;
208
+ // Migrate the pre-rename store once, preserving existing logins.
209
+ if (path !== authFilePath())
210
+ return {};
211
+ try {
212
+ text = await readFile(legacyAuthFilePath(), 'utf8');
213
+ }
214
+ catch (legacyError) {
215
+ if (legacyError.code === 'ENOENT')
216
+ return {};
217
+ throw legacyError;
218
+ }
219
+ const migrated = parseStore(text, legacyAuthFilePath());
220
+ await writeStore(migrated, path);
221
+ await rm(legacyAuthFilePath(), { force: true });
222
+ return migrated;
223
+ }
224
+ return parseStore(text, path);
225
+ }
226
+ /**
227
+ * Parse and migrate store JSON read from `path`. An ACCOUNT entry whose shape
228
+ * is invalid (empty or missing tokens — corruption seen in the wild from a
229
+ * broken keychain import) is SKIPPED instead of rejected: one bad entry must
230
+ * not blind every provider's status read, and a session without tokens is
231
+ * unusable by definition, so nothing of value is discarded. The next write
232
+ * persists the store without the skipped entry. Structural failures (invalid
233
+ * JSON, a non-object file) still throw — those say the file itself is broken.
234
+ */
235
+ function parseStore(text, path) {
236
+ let parsed;
237
+ try {
238
+ parsed = JSON.parse(text);
239
+ }
240
+ catch {
241
+ throw new Error(`subscriptions auth store at ${path} is not valid JSON; fix or delete the file`);
242
+ }
243
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
244
+ throw new Error(`subscriptions auth store at ${path} must be a JSON object keyed by provider; fix or delete the file`);
245
+ }
246
+ const raw = parsed;
247
+ const store = {};
248
+ for (const provider of PROVIDER_IDS) {
249
+ const entry = raw[provider];
250
+ if (entry === undefined)
251
+ continue;
252
+ if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
253
+ console.warn(`subscriptions auth store: entry "${provider}" is not an object; skipped`);
254
+ continue;
255
+ }
256
+ const record = entry;
257
+ if (typeof record.accessToken === 'string') {
258
+ // Single-account format: wrap the bare session, preserving every field.
259
+ if (!isValidSessionShape(record)) {
260
+ console.warn(`subscriptions auth store: legacy entry "${provider}" has no usable tokens; skipped`);
261
+ continue;
262
+ }
263
+ const session = record;
264
+ const key = provider === 'codex' ? session.accountId : accountKeyOf(provider, session);
265
+ store[provider] = { default: key, accounts: { [key]: session } };
266
+ continue;
267
+ }
268
+ const accounts = record.accounts;
269
+ if (typeof accounts !== 'object' || accounts === null || Array.isArray(accounts)) {
270
+ console.warn(`subscriptions auth store: entry "${provider}" has no accounts map; skipped`);
271
+ continue;
272
+ }
273
+ if (record.default !== undefined && typeof record.default !== 'string') {
274
+ console.warn(`subscriptions auth store: entry "${provider}" default is not a string; skipped`);
275
+ continue;
276
+ }
277
+ const kept = {};
278
+ for (const [account, session] of Object.entries(accounts)) {
279
+ if (isValidSessionShape(session)) {
280
+ kept[account] = session;
281
+ }
282
+ else {
283
+ console.warn(`subscriptions auth store: entry "${provider}/${account}" has no usable accessToken/refreshToken/expiresAt; skipped`);
284
+ }
285
+ }
286
+ if (Object.keys(kept).length === 0)
287
+ continue;
288
+ const validDefault = record.default === undefined || record.default in kept
289
+ ? record.default
290
+ : Object.keys(kept)[0];
291
+ store[provider] = { ...record, default: validDefault, accounts: kept };
292
+ }
293
+ if (store.codex !== undefined)
294
+ migrateCodex(store.codex);
295
+ return store;
296
+ }
297
+ /** Whether a value carries the fields every stored session needs (non-empty tokens). */
298
+ function isValidSessionShape(value) {
299
+ if (typeof value !== 'object' || value === null)
300
+ return false;
301
+ const entry = value;
302
+ return typeof entry.accessToken === 'string' && entry.accessToken.length > 0
303
+ && typeof entry.refreshToken === 'string' && entry.refreshToken.length > 0
304
+ && typeof entry.expiresAt === 'number' && Number.isFinite(entry.expiresAt);
305
+ }
306
+ /** Persist the whole store atomically with owner-only permissions. */
307
+ async function writeStore(store, path) {
308
+ await mkdir(dirname(path), { recursive: true });
309
+ const tmp = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
310
+ try {
311
+ await writeFile(tmp, JSON.stringify(store, null, 2), { mode: 0o600 });
312
+ // An existing destination keeps its old mode through rename on some
313
+ // filesystems; enforce 0600 on the source before the swap.
314
+ await chmod(tmp, 0o600);
315
+ await rename(tmp, path);
316
+ }
317
+ catch (error) {
318
+ await rm(tmp, { force: true });
319
+ throw error;
320
+ }
321
+ }
322
+ /**
323
+ * One write chain per store path. Every mutation is a read-modify-write of a
324
+ * single JSON file, and the plugin has several independent writers — a login,
325
+ * a logout, and one token refresh per provider account, each on its own
326
+ * schedule. Overlapping them unserialized costs whichever account read the
327
+ * store first its entry.
328
+ *
329
+ * A chain is dropped once nothing is queued behind it, so the map holds an
330
+ * entry only while writes are in flight.
331
+ */
332
+ const writeChains = new Map();
333
+ /**
334
+ * Run one read-modify-write of a store path after every write already queued
335
+ * for it. Callers join the chain synchronously, so call order is write order.
336
+ * @param path - the store file being mutated.
337
+ * @param action - the read-modify-write to run.
338
+ * @returns whatever `action` returns.
339
+ */
340
+ async function serialize(path, action) {
341
+ const previous = writeChains.get(path) ?? Promise.resolve();
342
+ // Both handlers: a failed write must not strand everything queued behind it.
343
+ const next = previous.then(action, action);
344
+ const tail = next.then(() => undefined, () => undefined);
345
+ writeChains.set(path, tail);
346
+ try {
347
+ return await next;
348
+ }
349
+ finally {
350
+ if (writeChains.get(path) === tail)
351
+ writeChains.delete(path);
352
+ }
353
+ }
354
+ /**
355
+ * List one provider's accounts, default first.
356
+ * @param provider - the provider route.
357
+ * @param path - store file path; defaults to {@link authFilePath}.
358
+ * @returns the account entries in stable order (empty when logged out).
359
+ */
360
+ export async function listAccounts(provider, path = authFilePath()) {
361
+ const entry = (await loadStore(path))[provider];
362
+ if (entry === undefined)
363
+ return [];
364
+ const accounts = Object.entries(entry.accounts).map(([key, session]) => ({ key, session }));
365
+ accounts.sort((a, b) => Number(b.key === entry.default) - Number(a.key === entry.default));
366
+ return accounts;
367
+ }
368
+ /**
369
+ * Read one account's session.
370
+ * @param provider - the provider route.
371
+ * @param account - the account key; defaults to the provider's default account.
372
+ * @param path - store file path; defaults to {@link authFilePath}.
373
+ * @returns the stored session, or `undefined` when absent.
374
+ */
375
+ export async function getAccountSession(provider, account, path = authFilePath()) {
376
+ const entry = (await loadStore(path))[provider];
377
+ if (entry === undefined)
378
+ return undefined;
379
+ const key = account ?? entry.default;
380
+ if (key === undefined)
381
+ return undefined;
382
+ return entry.accounts[resolveAccount(entry, key)];
383
+ }
384
+ /**
385
+ * Write one account's session, preserving the others. The first account of a
386
+ * provider becomes its default.
387
+ *
388
+ * The session is validated before it lands: a corrupt entry written here
389
+ * would fail every later read of the whole store (one bad entry breaks all
390
+ * providers' status), so the write path must be as strict as the read path.
391
+ * @param provider - the provider route.
392
+ * @param account - the account key (see {@link accountKeyOf}).
393
+ * @param session - the fresh session from a login or refresh.
394
+ * @param path - store file path; defaults to {@link authFilePath}.
395
+ * @throws when the session is missing accessToken/refreshToken/expiresAt.
396
+ */
397
+ export async function saveAccountSession(provider, account, session, path = authFilePath()) {
398
+ assertSessionShape(provider, account, session);
399
+ return serialize(path, async () => {
400
+ const store = await loadStore(path);
401
+ const entry = store[provider];
402
+ if (entry !== undefined) {
403
+ account = resolveAccount(entry, account);
404
+ if (provider === 'codex') {
405
+ account = reconcileCodexIdentity(entry, session) ?? accountKeyOf('codex', session);
406
+ }
407
+ }
408
+ ;
409
+ store[provider] = {
410
+ ...entry,
411
+ default: entry?.default ?? account,
412
+ accounts: { ...entry?.accounts, [account]: session },
413
+ };
414
+ await writeStore(store, path);
415
+ });
416
+ }
417
+ /**
418
+ * Delete one account's session (logout). Deleting the default moves the badge
419
+ * to the next remaining account.
420
+ * @param provider - the provider route.
421
+ * @param account - the account key.
422
+ * @param path - store file path; defaults to {@link authFilePath}.
423
+ */
424
+ export async function deleteAccountSession(provider, account, path = authFilePath()) {
425
+ return serialize(path, async () => {
426
+ const store = await loadStore(path);
427
+ const entry = store[provider];
428
+ if (entry === undefined)
429
+ return;
430
+ account = resolveAccount(entry, account);
431
+ if (!Object.hasOwn(entry.accounts, account))
432
+ return;
433
+ const accounts = { ...entry.accounts };
434
+ delete accounts[account];
435
+ if (Object.keys(accounts).length === 0) {
436
+ delete store[provider];
437
+ }
438
+ else {
439
+ ;
440
+ store[provider] = {
441
+ ...entry,
442
+ ...entry.default === account ? { default: Object.keys(accounts)[0] } : { default: entry.default },
443
+ accounts,
444
+ };
445
+ }
446
+ await writeStore(store, path);
447
+ });
448
+ }
449
+ /**
450
+ * Pin the account direct (non-pool) routes serve.
451
+ * @param provider - the provider route.
452
+ * @param account - the account key; must exist.
453
+ * @param path - store file path; defaults to {@link authFilePath}.
454
+ */
455
+ export async function setDefaultAccount(provider, account, path = authFilePath()) {
456
+ return serialize(path, async () => {
457
+ const store = await loadStore(path);
458
+ const entry = store[provider];
459
+ if (entry !== undefined)
460
+ account = resolveAccount(entry, account);
461
+ if (entry === undefined || !Object.hasOwn(entry.accounts, account)) {
462
+ throw new Error(`no ${provider} account "${account}" is logged in`);
463
+ }
464
+ ;
465
+ store[provider] = { ...entry, default: account };
466
+ await writeStore(store, path);
467
+ });
468
+ }
@@ -0,0 +1,9 @@
1
+ import type { ConnectionHandle } from '@deepseek-ai/dsh-api-remotes/client';
2
+ import type { SubscriptionsSectionInjected } from './SubscriptionsSection.js';
3
+ type Translate = SubscriptionsSectionInjected['t'];
4
+ /** Cursor's single connected account, rendered with the same card and Manage dialog as the other providers. */
5
+ export declare function CursorCard({ rpc, t }: {
6
+ rpc: ConnectionHandle['rpc'];
7
+ t: Translate;
8
+ }): import("react").JSX.Element;
9
+ export {};
@@ -0,0 +1,113 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useRef, useState } from 'react';
3
+ import { callSubscriptionsAuth } from './subscriptions-rpc.js';
4
+ import { ProviderAccountManager } from './ProviderAccountManager.js';
5
+ import { USAGE_BADGE_REFRESH_EVENT } from './usage-badge-preferences.js';
6
+ import { subscriptionCardStyles as styles } from './subscription-card-styles.js';
7
+ function message(error) {
8
+ return error instanceof Error ? error.message : String(error);
9
+ }
10
+ function barColor(percent) {
11
+ if (percent >= 95)
12
+ return 'var(--dsw-alias-state-error-primary)';
13
+ if (percent >= 80)
14
+ return 'var(--dsw-alias-state-warn-label)';
15
+ return 'var(--dsw-alias-state-success-primary)';
16
+ }
17
+ /** Cursor's single connected account, rendered with the same card and Manage dialog as the other providers. */
18
+ export function CursorCard({ rpc, t }) {
19
+ const [status, setStatus] = useState();
20
+ const [usage, setUsage] = useState();
21
+ const [authorizeUrl, setAuthorizeUrl] = useState();
22
+ const [error, setError] = useState();
23
+ const [loading, setLoading] = useState(false);
24
+ const [manage, setManage] = useState(false);
25
+ const authenticated = useRef();
26
+ const refreshStatus = useCallback(async () => {
27
+ try {
28
+ const value = await callSubscriptionsAuth(rpc, 'cursorStatus', {});
29
+ setStatus(value);
30
+ if (authenticated.current !== value.authenticated) {
31
+ authenticated.current = value.authenticated;
32
+ window.dispatchEvent(new Event(USAGE_BADGE_REFRESH_EVENT));
33
+ }
34
+ if (value.authenticated)
35
+ setAuthorizeUrl(undefined);
36
+ if (value.error)
37
+ setError(value.error);
38
+ }
39
+ catch (failure) {
40
+ setError(message(failure));
41
+ }
42
+ }, [rpc]);
43
+ const refreshUsage = useCallback(async () => {
44
+ setLoading(true);
45
+ try {
46
+ setUsage(await callSubscriptionsAuth(rpc, 'cursorUsage', {}));
47
+ setError(undefined);
48
+ window.dispatchEvent(new Event(USAGE_BADGE_REFRESH_EVENT));
49
+ }
50
+ catch (failure) {
51
+ setError(message(failure));
52
+ }
53
+ finally {
54
+ setLoading(false);
55
+ }
56
+ }, [rpc]);
57
+ useEffect(() => { void refreshStatus(); }, [refreshStatus]);
58
+ useEffect(() => {
59
+ if (status?.busy !== true)
60
+ return;
61
+ const timer = setInterval(() => { void refreshStatus(); }, 2000);
62
+ return () => clearInterval(timer);
63
+ }, [status?.busy, refreshStatus]);
64
+ useEffect(() => { if (status?.authenticated === true)
65
+ void refreshUsage(); }, [status?.authenticated, refreshUsage]);
66
+ const login = async () => {
67
+ setError(undefined);
68
+ try {
69
+ const value = await callSubscriptionsAuth(rpc, 'cursorLogin', {});
70
+ setAuthorizeUrl(value.authorizeUrl);
71
+ window.open(value.authorizeUrl, '_blank', 'noopener');
72
+ await refreshStatus();
73
+ }
74
+ catch (failure) {
75
+ setError(message(failure));
76
+ }
77
+ };
78
+ const cancel = async () => {
79
+ try {
80
+ await callSubscriptionsAuth(rpc, 'cursorCancel', {});
81
+ setAuthorizeUrl(undefined);
82
+ await refreshStatus();
83
+ }
84
+ catch (failure) {
85
+ setError(message(failure));
86
+ }
87
+ };
88
+ const logout = async () => {
89
+ if (!window.confirm(t('logoutAccountConfirm', { account: 'Cursor', provider: 'Cursor' })))
90
+ return;
91
+ try {
92
+ await callSubscriptionsAuth(rpc, 'cursorLogout', {});
93
+ setUsage(undefined);
94
+ await refreshStatus();
95
+ }
96
+ catch (failure) {
97
+ setError(message(failure));
98
+ }
99
+ };
100
+ const connected = status?.authenticated === true;
101
+ return _jsxs("div", { style: styles.card, children: [_jsxs("div", { style: styles.header, children: [_jsx("span", { style: { ...styles.dot, background: status?.busy ? 'var(--dsw-alias-state-warn-label)'
102
+ : connected ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-label-dimmed)' } }), _jsx("span", { style: styles.name, children: t('cursorTitle') })] }), _jsx("p", { style: styles.status, children: status === undefined ? t('checking') : status.busy ? t('loginInProgress')
103
+ : connected ? t('loggedInCount', { count: 1 }) : t('notLoggedIn') }), error !== undefined && _jsx("p", { style: styles.error, children: error }), connected && _jsxs("div", { style: styles.account, children: [_jsxs("div", { style: styles.accountHeader, children: [_jsx("span", { style: styles.defaultStar, title: t('defaultBadge'), children: "\u2605" }), _jsx("span", { style: styles.accountName, children: "Cursor" }), status.expiresAt !== undefined && _jsx("span", { style: styles.status, children: t('accountExpires', { date: new Date(status.expiresAt).toLocaleString() }) }), _jsx("button", { type: "button", style: { ...styles.button, marginLeft: 'auto', flexShrink: 0 }, onClick: () => { void logout(); }, children: t('logout') })] }), _jsxs("div", { style: styles.usage, children: [_jsxs("div", { style: styles.usageHeader, children: [_jsx("span", { style: styles.usageTitle, children: t('usageTitle') }), usage?.plan !== undefined && _jsx("span", { style: styles.status, children: t('usagePlan', { plan: usage.plan }) }), _jsx("button", { type: "button", style: styles.usageRefresh, disabled: loading, onClick: () => { void refreshUsage(); }, children: t('usageRefresh') })] }), loading && usage === undefined && _jsx("p", { style: styles.status, children: t('usageLoading') }), usage?.windows?.length === 0 && _jsx("p", { style: styles.status, children: t('usageEmpty') }), usage?.windows?.map((window, index) => {
104
+ const percent = Math.min(100, Math.max(0, window.usedPercent));
105
+ const label = window.scope === 'Included' ? t('cursorIncluded')
106
+ : window.scope === 'Cursor Models' ? t('cursorModels')
107
+ : window.scope === 'Other Models' ? t('cursorOtherModels')
108
+ : window.scope === 'Included requests' ? t('cursorIncludedRequests')
109
+ : window.scope ?? t('usageWindow');
110
+ return _jsxs("div", { style: styles.usageRow, children: [_jsxs("div", { style: styles.usageMeta, children: [_jsx("span", { children: label }), _jsxs("span", { children: [Math.round(percent), "%", window.resetsAt === undefined ? ''
111
+ : ` · ${t('usageResets', { date: new Date(window.resetsAt).toLocaleString() })}`] })] }), _jsx("div", { style: styles.usageTrack, children: _jsx("div", { style: { width: `${percent}%`, height: '100%', borderRadius: 3, background: barColor(percent) } }) })] }, index);
112
+ })] })] }), _jsxs("div", { style: styles.actions, children: [status?.authenticated === false && !status.busy && _jsx("button", { type: "button", style: styles.button, onClick: () => { void login(); }, children: t('login') }), _jsx("button", { type: "button", style: styles.button, "aria-haspopup": "dialog", onClick: () => setManage(true), children: t('accountsManage') }), status?.busy === true && _jsx("button", { type: "button", style: styles.button, onClick: () => { void cancel(); }, children: t('cancel') })] }), status?.busy === true && authorizeUrl !== undefined && _jsx("a", { href: authorizeUrl, target: "_blank", rel: "noopener noreferrer", children: t('cursorOpenLogin') }), manage && _jsx(ProviderAccountManager, { provider: "cursor-subscription", name: "Cursor", rpc: rpc, t: t, onClose: () => setManage(false) })] });
113
+ }
@@ -0,0 +1,9 @@
1
+ import type { ConnectionHandle } from '@deepseek-ai/dsh-api-remotes/client';
2
+ import type { SubscriptionsSectionInjected } from './SubscriptionsSection.js';
3
+ type Translate = SubscriptionsSectionInjected['t'];
4
+ /** Quota cards for API-key providers already available through DSH itself. */
5
+ export declare function ExternalUsageCards({ rpc, t }: {
6
+ rpc: ConnectionHandle['rpc'];
7
+ t: Translate;
8
+ }): import("react").JSX.Element;
9
+ export {};