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.
- package/README.md +42 -1
- package/README.zh.md +42 -1
- package/lib/auth/device-flow.d.ts +0 -9
- package/lib/auth/device-flow.js +2 -1
- package/lib/auth/rpc.d.ts +44 -13
- package/lib/auth/rpc.js +127 -9
- package/lib/auth/store.d.ts +75 -17
- package/lib/auth/store.js +148 -27
- package/lib/client/SubscriptionsSection.d.ts +26 -3
- package/lib/client/SubscriptionsSection.js +263 -67
- package/lib/client/index.js +11 -0
- package/lib/client/locales.d.ts +82 -10
- package/lib/client/locales.js +82 -10
- package/lib/client.js +837 -223
- package/lib/client.js.map +1 -1
- package/lib/http.d.ts +114 -0
- package/lib/http.js +402 -0
- package/lib/index.d.ts +21 -0
- package/lib/index.js +1938 -208
- package/lib/providers/accounts.d.ts +102 -0
- package/lib/providers/accounts.js +123 -0
- package/lib/providers/antigravity.d.ts +90 -0
- package/lib/providers/antigravity.js +392 -0
- package/lib/providers/claude.d.ts +22 -4
- package/lib/providers/claude.js +97 -16
- package/lib/providers/codex.d.ts +24 -3
- package/lib/providers/codex.js +121 -21
- package/lib/providers/common.d.ts +17 -0
- package/lib/providers/common.js +67 -3
- package/lib/providers/copilot.d.ts +23 -4
- package/lib/providers/copilot.js +99 -19
- package/lib/providers/grok.d.ts +24 -4
- package/lib/providers/grok.js +106 -19
- package/lib/providers/pool-family.d.ts +56 -0
- package/lib/providers/pool-family.js +45 -0
- package/lib/providers/pool-health.d.ts +74 -0
- package/lib/providers/pool-health.js +148 -0
- package/lib/providers/pool-usage.d.ts +57 -0
- package/lib/providers/pool-usage.js +130 -0
- package/lib/providers/pool.d.ts +107 -0
- package/lib/providers/pool.js +371 -0
- package/lib/tools/image-generate.d.ts +3 -3
- package/lib/tools/image-generate.js +4 -2
- package/lib/tools/video-generate.d.ts +2 -2
- package/lib/tools/video-generate.js +4 -2
- package/lib/tools/x-search.d.ts +2 -2
- package/lib/tools/x-search.js +4 -2
- package/lib/translate/antigravity.d.ts +110 -0
- package/lib/translate/antigravity.js +303 -0
- 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
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
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
|
|
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
|
|
119
|
+
const raw = parsed;
|
|
120
|
+
const store = {};
|
|
83
121
|
for (const provider of PROVIDER_IDS) {
|
|
84
|
-
const entry =
|
|
85
|
-
if (entry
|
|
86
|
-
|
|
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
|
|
110
|
-
* schedule. Overlapping them unserialized costs whichever
|
|
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
|
-
*
|
|
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
|
|
204
|
+
* @returns the account entries in stable order (empty when logged out).
|
|
143
205
|
*/
|
|
144
|
-
export async function
|
|
145
|
-
|
|
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
|
-
*
|
|
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
|
|
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]
|
|
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
|
|
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
|
|
256
|
+
export async function deleteAccountSession(provider, account, path = authFilePath()) {
|
|
166
257
|
return serialize(path, async () => {
|
|
167
258
|
const store = await loadStore(path);
|
|
168
|
-
|
|
259
|
+
const entry = store[provider];
|
|
260
|
+
if (entry === undefined || !(account in entry.accounts))
|
|
169
261
|
return;
|
|
170
|
-
|
|
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
|
-
|
|
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. */
|