dsh-plugin-subscriptions 0.5.1 → 0.5.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.
Files changed (50) hide show
  1. package/README.md +42 -1
  2. package/README.zh.md +42 -1
  3. package/lib/auth/device-flow.d.ts +0 -9
  4. package/lib/auth/device-flow.js +2 -1
  5. package/lib/auth/rpc.d.ts +44 -13
  6. package/lib/auth/rpc.js +127 -9
  7. package/lib/auth/store.d.ts +75 -17
  8. package/lib/auth/store.js +148 -27
  9. package/lib/client/SubscriptionsSection.d.ts +26 -3
  10. package/lib/client/SubscriptionsSection.js +263 -67
  11. package/lib/client/index.js +11 -0
  12. package/lib/client/locales.d.ts +82 -10
  13. package/lib/client/locales.js +82 -10
  14. package/lib/client.js +837 -223
  15. package/lib/client.js.map +1 -1
  16. package/lib/http.d.ts +114 -0
  17. package/lib/http.js +402 -0
  18. package/lib/index.d.ts +21 -0
  19. package/lib/index.js +1938 -208
  20. package/lib/providers/accounts.d.ts +102 -0
  21. package/lib/providers/accounts.js +123 -0
  22. package/lib/providers/antigravity.d.ts +90 -0
  23. package/lib/providers/antigravity.js +392 -0
  24. package/lib/providers/claude.d.ts +22 -4
  25. package/lib/providers/claude.js +97 -16
  26. package/lib/providers/codex.d.ts +24 -3
  27. package/lib/providers/codex.js +121 -21
  28. package/lib/providers/common.d.ts +17 -0
  29. package/lib/providers/common.js +67 -3
  30. package/lib/providers/copilot.d.ts +23 -4
  31. package/lib/providers/copilot.js +99 -19
  32. package/lib/providers/grok.d.ts +24 -4
  33. package/lib/providers/grok.js +106 -19
  34. package/lib/providers/pool-family.d.ts +56 -0
  35. package/lib/providers/pool-family.js +45 -0
  36. package/lib/providers/pool-health.d.ts +74 -0
  37. package/lib/providers/pool-health.js +148 -0
  38. package/lib/providers/pool-usage.d.ts +57 -0
  39. package/lib/providers/pool-usage.js +130 -0
  40. package/lib/providers/pool.d.ts +107 -0
  41. package/lib/providers/pool.js +371 -0
  42. package/lib/tools/image-generate.d.ts +3 -3
  43. package/lib/tools/image-generate.js +4 -2
  44. package/lib/tools/video-generate.d.ts +2 -2
  45. package/lib/tools/video-generate.js +4 -2
  46. package/lib/tools/x-search.d.ts +2 -2
  47. package/lib/tools/x-search.js +4 -2
  48. package/lib/translate/antigravity.d.ts +110 -0
  49. package/lib/translate/antigravity.js +303 -0
  50. package/package.json +14 -9
package/lib/auth/store.js CHANGED
@@ -1,16 +1,52 @@
1
1
  /**
2
2
  * On-disk OAuth session store at `~/.dsh/plugins/subscriptions/auth.json`.
3
3
  *
4
- * The file is a JSON object keyed by provider id. Writes are atomic
5
- * (tmp file + rename) with mode 0600 because they carry bearer tokens.
6
- * Session shapes live here (not in the provider modules) because this file
7
- * owns the durable format.
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.
8
14
  */
15
+ import { createHash } from 'node:crypto';
9
16
  import { chmod, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
10
17
  import { dirname } from 'node:path';
11
18
  import { dshHomePath } from '@deepseek-ai/dsh-home-paths';
12
19
  /** Every provider route, in display order. */
13
20
  export const PROVIDER_IDS = ['codex', 'claude', 'grok', 'copilot'];
21
+ /**
22
+ * The stable identity of one session's account: codex keys on the always
23
+ * present `accountId` claim, the others on their display identity, falling
24
+ * back to a refresh-token hash for sessions stored before identity fields
25
+ * existed. Logging the same account in again lands on the same key, so a
26
+ * re-login updates in place instead of duplicating. (The hash fallback can
27
+ * miss that dedup once for a legacy session re-logged with a now-known
28
+ * identity — the duplicate is visible on the Settings page and can simply
29
+ * be logged out.)
30
+ * @param provider - the provider route.
31
+ * @param session - the session to key.
32
+ * @returns the account map key.
33
+ */
34
+ export function accountKeyOf(provider, session) {
35
+ switch (provider) {
36
+ case 'codex':
37
+ return session.accountId;
38
+ case 'claude':
39
+ return session.emailAddress ?? tokenHash(session.refreshToken);
40
+ case 'grok':
41
+ return session.account ?? tokenHash(session.refreshToken);
42
+ case 'copilot':
43
+ return session.account ?? tokenHash(session.refreshToken);
44
+ }
45
+ }
46
+ /** Short stable hash for sessions without an identity field. */
47
+ function tokenHash(refreshToken) {
48
+ return `token-${createHash('sha256').update(refreshToken).digest('hex').slice(0, 16)}`;
49
+ }
14
50
  /**
15
51
  * Absolute path of the auth store file.
16
52
  * @returns `dshHomePath('plugins', 'subscriptions', 'auth.json')`.
@@ -22,22 +58,23 @@ export function authFilePath() {
22
58
  function legacyAuthFilePath() {
23
59
  return dshHomePath('plugins', 'router', 'auth.json');
24
60
  }
25
- /** Check that one durable entry carries the fields every session needs. */
26
- function assertSessionShape(provider, value) {
61
+ /** Check that one durable session carries the fields every session needs. */
62
+ function assertSessionShape(provider, account, value) {
27
63
  if (typeof value !== 'object' || value === null) {
28
- throw new Error(`subscriptions auth store: entry "${provider}" is not an object; fix or delete the store file`);
64
+ throw new Error(`subscriptions auth store: entry "${provider}/${account}" is not an object; fix or delete the store file`);
29
65
  }
30
66
  const entry = value;
31
67
  if (typeof entry.accessToken !== 'string' || entry.accessToken.length === 0
32
68
  || typeof entry.refreshToken !== 'string' || entry.refreshToken.length === 0
33
69
  || typeof entry.expiresAt !== 'number' || !Number.isFinite(entry.expiresAt)) {
34
- throw new Error(`subscriptions auth store: entry "${provider}" is missing accessToken/refreshToken/expiresAt; fix or delete the store file`);
70
+ throw new Error(`subscriptions auth store: entry "${provider}/${account}" is missing accessToken/refreshToken/expiresAt; fix or delete the store file`);
35
71
  }
36
72
  }
37
73
  /**
38
74
  * Read the whole store. A missing file is an empty store; malformed JSON or a
39
75
  * malformed entry throws, because silently discarding tokens would strand the
40
- * user without a diagnosis.
76
+ * user without a diagnosis. Single-account entries are migrated in memory;
77
+ * the next write persists the new shape.
41
78
  * @param path - store file path; defaults to {@link authFilePath}.
42
79
  * @returns the parsed session map.
43
80
  */
@@ -67,7 +104,7 @@ export async function loadStore(path = authFilePath()) {
67
104
  }
68
105
  return parseStore(text, path);
69
106
  }
70
- /** Parse and validate store JSON read from `path`. */
107
+ /** Parse, validate, and migrate store JSON read from `path`. */
71
108
  function parseStore(text, path) {
72
109
  let parsed;
73
110
  try {
@@ -79,11 +116,36 @@ function parseStore(text, path) {
79
116
  if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
80
117
  throw new Error(`subscriptions auth store at ${path} must be a JSON object keyed by provider; fix or delete the file`);
81
118
  }
82
- const store = parsed;
119
+ const raw = parsed;
120
+ const store = {};
83
121
  for (const provider of PROVIDER_IDS) {
84
- const entry = store[provider];
85
- if (entry !== undefined)
86
- assertSessionShape(provider, entry);
122
+ const entry = raw[provider];
123
+ if (entry === undefined)
124
+ continue;
125
+ if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
126
+ throw new Error(`subscriptions auth store: entry "${provider}" is not an object; fix or delete the store file`);
127
+ }
128
+ const record = entry;
129
+ if (typeof record.accessToken === 'string') {
130
+ // Single-account format: wrap the bare session, preserving every field.
131
+ assertSessionShape(provider, '(legacy)', record);
132
+ const session = record;
133
+ const key = accountKeyOf(provider, session);
134
+ store[provider] = { default: key, accounts: { [key]: session } };
135
+ continue;
136
+ }
137
+ const accounts = record.accounts;
138
+ if (typeof accounts !== 'object' || accounts === null || Array.isArray(accounts)) {
139
+ throw new Error(`subscriptions auth store: entry "${provider}" has no accounts map; fix or delete the store file`);
140
+ }
141
+ if (record.default !== undefined && typeof record.default !== 'string') {
142
+ throw new Error(`subscriptions auth store: entry "${provider}" default is not a string; fix or delete the store file`);
143
+ }
144
+ for (const [account, session] of Object.entries(accounts)) {
145
+ assertSessionShape(provider, account, session);
146
+ }
147
+ ;
148
+ store[provider] = record;
87
149
  }
88
150
  return store;
89
151
  }
@@ -106,8 +168,8 @@ async function writeStore(store, path) {
106
168
  /**
107
169
  * One write chain per store path. Every mutation is a read-modify-write of a
108
170
  * single JSON file, and the plugin has several independent writers — a login,
109
- * a logout, and one token refresh per provider adapter, each on its own
110
- * schedule. Overlapping them unserialized costs whichever provider read the
171
+ * a logout, and one token refresh per provider account, each on its own
172
+ * schedule. Overlapping them unserialized costs whichever account read the
111
173
  * store first its entry.
112
174
  *
113
175
  * A chain is dropped once nothing is queued behind it, so the map holds an
@@ -136,38 +198,97 @@ async function serialize(path, action) {
136
198
  }
137
199
  }
138
200
  /**
139
- * Read one provider's session.
201
+ * List one provider's accounts, default first.
140
202
  * @param provider - the provider route.
141
203
  * @param path - store file path; defaults to {@link authFilePath}.
142
- * @returns the stored session, or `undefined` when logged out.
204
+ * @returns the account entries in stable order (empty when logged out).
143
205
  */
144
- export async function getSession(provider, path = authFilePath()) {
145
- return (await loadStore(path))[provider];
206
+ export async function listAccounts(provider, path = authFilePath()) {
207
+ const entry = (await loadStore(path))[provider];
208
+ if (entry === undefined)
209
+ return [];
210
+ const accounts = Object.entries(entry.accounts).map(([key, session]) => ({ key, session }));
211
+ accounts.sort((a, b) => Number(b.key === entry.default) - Number(a.key === entry.default));
212
+ return accounts;
146
213
  }
147
214
  /**
148
- * Write one provider's session, preserving the others.
215
+ * Read one account's session.
149
216
  * @param provider - the provider route.
217
+ * @param account - the account key; defaults to the provider's default account.
218
+ * @param path - store file path; defaults to {@link authFilePath}.
219
+ * @returns the stored session, or `undefined` when absent.
220
+ */
221
+ export async function getAccountSession(provider, account, path = authFilePath()) {
222
+ const entry = (await loadStore(path))[provider];
223
+ if (entry === undefined)
224
+ return undefined;
225
+ const key = account ?? entry.default;
226
+ if (key === undefined)
227
+ return undefined;
228
+ return entry.accounts[key];
229
+ }
230
+ /**
231
+ * Write one account's session, preserving the others. The first account of a
232
+ * provider becomes its default.
233
+ * @param provider - the provider route.
234
+ * @param account - the account key (see {@link accountKeyOf}).
150
235
  * @param session - the fresh session from a login or refresh.
151
236
  * @param path - store file path; defaults to {@link authFilePath}.
152
237
  */
153
- export async function saveSession(provider, session, path = authFilePath()) {
238
+ export async function saveAccountSession(provider, account, session, path = authFilePath()) {
154
239
  return serialize(path, async () => {
155
240
  const store = await loadStore(path);
156
- store[provider] = session;
241
+ const entry = store[provider];
242
+ store[provider] = {
243
+ default: entry?.default ?? account,
244
+ accounts: { ...entry?.accounts, [account]: session },
245
+ };
157
246
  await writeStore(store, path);
158
247
  });
159
248
  }
160
249
  /**
161
- * Delete one provider's session (logout).
250
+ * Delete one account's session (logout). Deleting the default moves the badge
251
+ * to the next remaining account.
162
252
  * @param provider - the provider route.
253
+ * @param account - the account key.
163
254
  * @param path - store file path; defaults to {@link authFilePath}.
164
255
  */
165
- export async function deleteSession(provider, path = authFilePath()) {
256
+ export async function deleteAccountSession(provider, account, path = authFilePath()) {
166
257
  return serialize(path, async () => {
167
258
  const store = await loadStore(path);
168
- if (store[provider] === undefined)
259
+ const entry = store[provider];
260
+ if (entry === undefined || !(account in entry.accounts))
169
261
  return;
170
- delete store[provider];
262
+ const accounts = { ...entry.accounts };
263
+ delete accounts[account];
264
+ if (Object.keys(accounts).length === 0) {
265
+ delete store[provider];
266
+ }
267
+ else {
268
+ ;
269
+ store[provider] = {
270
+ ...entry.default === account ? { default: Object.keys(accounts)[0] } : { default: entry.default },
271
+ accounts,
272
+ };
273
+ }
274
+ await writeStore(store, path);
275
+ });
276
+ }
277
+ /**
278
+ * Pin the account direct (non-pool) routes serve.
279
+ * @param provider - the provider route.
280
+ * @param account - the account key; must exist.
281
+ * @param path - store file path; defaults to {@link authFilePath}.
282
+ */
283
+ export async function setDefaultAccount(provider, account, path = authFilePath()) {
284
+ return serialize(path, async () => {
285
+ const store = await loadStore(path);
286
+ const entry = store[provider];
287
+ if (entry === undefined || !(account in entry.accounts)) {
288
+ throw new Error(`no ${provider} account "${account}" is logged in`);
289
+ }
290
+ ;
291
+ store[provider] = { ...entry, default: account };
171
292
  await writeStore(store, path);
172
293
  });
173
294
  }
@@ -2,12 +2,18 @@ import type { ConnectionHandle } from '@deepseek-ai/dsh-api-remotes/client';
2
2
  import type { SubscriptionsKey } from './locales.js';
3
3
  /** Subscription provider ids, fixed by the node half's OAuth adapters. */
4
4
  export type SubscriptionProvider = 'codex' | 'claude' | 'grok' | 'copilot';
5
+ /** One logged-in account as answered by the `status` endpoint. */
6
+ export interface AccountStatus {
7
+ key: string;
8
+ account?: string;
9
+ expiresAt?: number;
10
+ plan?: string;
11
+ isDefault: boolean;
12
+ }
5
13
  /** One provider's login state as answered by the `status` endpoint. */
6
14
  export interface ProviderStatus {
7
- loggedIn: boolean;
8
15
  busy: boolean;
9
- expiresAt?: number;
10
- account?: string;
16
+ accounts: AccountStatus[];
11
17
  detail?: string;
12
18
  }
13
19
  /** One rate-limit window as answered by the `usage` endpoint. */
@@ -23,6 +29,23 @@ export interface ProviderUsage {
23
29
  windows?: UsageWindow[];
24
30
  plan?: string;
25
31
  }
32
+ /** `proxyGet` endpoint value: the node half owns this shape (no secrets). */
33
+ export interface ProxyConfigView {
34
+ enabled: boolean;
35
+ url: string;
36
+ username?: string;
37
+ passwordSet: boolean;
38
+ bypass: string[];
39
+ error?: string;
40
+ }
41
+ /** `proxyTest` endpoint value. */
42
+ export interface ProxyTestResult {
43
+ ok: boolean;
44
+ viaProxy: boolean;
45
+ status?: number;
46
+ latencyMs?: number;
47
+ error?: string;
48
+ }
26
49
  /** Injected dependencies of {@link SubscriptionsSection} (slot `inject`). */
27
50
  export interface SubscriptionsSectionInjected {
28
51
  /** Generic logical-RPC caller over the Connection transport. */