dsh-subscriptions 0.2.0 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +220 -180
- package/lib/auth/claude-code-creds.js +31 -9
- package/lib/auth/device-flow.js +16 -9
- package/lib/auth/oauth-flow.js +13 -10
- package/lib/auth/rpc.d.ts +8 -4
- package/lib/auth/rpc.js +89 -25
- package/lib/auth/store.js +80 -31
- package/lib/client/CursorCard.d.ts +1 -1
- package/lib/client/CursorCard.js +50 -16
- package/lib/client/ExternalUsageCards.d.ts +1 -1
- package/lib/client/ExternalUsageCards.js +37 -16
- package/lib/client/ImageGallery.d.ts +2 -2
- package/lib/client/ImageGallery.js +92 -33
- package/lib/client/ImageGenerateToolview.js +50 -14
- package/lib/client/OllamaCard.d.ts +1 -1
- package/lib/client/OllamaCard.js +26 -8
- package/lib/client/OllamaSettingsManager.js +113 -30
- package/lib/client/ProviderAccountManager.d.ts +1 -1
- package/lib/client/ProviderAccountManager.js +164 -49
- package/lib/client/ProviderModelEditor.js +137 -34
- package/lib/client/SpeedSelect.d.ts +62 -14
- package/lib/client/SpeedSelect.js +261 -49
- package/lib/client/SubscriptionUsageBadge.d.ts +4 -10
- package/lib/client/SubscriptionUsageBadge.js +300 -112
- package/lib/client/SubscriptionsSection.d.ts +2 -0
- package/lib/client/SubscriptionsSection.js +265 -85
- package/lib/client/UsageBadgeDisplaySetting.js +35 -12
- package/lib/client/UsageMeter.d.ts +1 -1
- package/lib/client/UsageMeter.js +62 -24
- package/lib/client/VideoGenerateToolview.js +69 -17
- package/lib/client/account-preferences.d.ts +8 -0
- package/lib/client/account-preferences.js +20 -5
- package/lib/client/host-contract.d.ts +11 -2
- package/lib/client/host-contract.js +20 -3
- package/lib/client/index.d.ts +5 -5
- package/lib/client/index.js +34 -12
- package/lib/client/locales.d.ts +22 -0
- package/lib/client/locales.js +73 -51
- package/lib/client/model-filter.d.ts +37 -0
- package/lib/client/model-filter.js +45 -0
- package/lib/client/ollama-draft.js +21 -12
- package/lib/client/reset-action.js +6 -4
- package/lib/client/reset-credits-view.d.ts +1 -1
- package/lib/client/reset-credits-view.js +66 -17
- package/lib/client/subscription-card-styles.js +94 -24
- package/lib/client/usage-color-preferences.js +4 -2
- package/lib/client/usage-pace.js +30 -12
- package/lib/client/usage-window-filter.d.ts +7 -0
- package/lib/client/usage-window-filter.js +12 -0
- package/lib/client.js +480 -136
- package/lib/client.js.map +1 -1
- package/lib/http.js +1 -1
- package/lib/index.d.ts +3 -3
- package/lib/index.js +167 -76
- package/lib/model-defaults.js +4 -2
- package/lib/provider-settings.d.ts +17 -0
- package/lib/provider-settings.js +79 -16
- package/lib/providers/account-preferences.js +51 -22
- package/lib/providers/accounts.js +15 -9
- package/lib/providers/antigravity-oauth-client.js +2 -2
- package/lib/providers/antigravity.js +141 -68
- package/lib/providers/catalog-store.js +120 -41
- package/lib/providers/claude-cli-version.js +3 -3
- package/lib/providers/claude.d.ts +9 -2
- package/lib/providers/claude.js +153 -90
- package/lib/providers/codex-client-version.js +3 -3
- package/lib/providers/codex-search.js +40 -13
- package/lib/providers/codex.js +196 -112
- package/lib/providers/common.js +71 -39
- package/lib/providers/copilot.js +143 -82
- package/lib/providers/cursor-adapter.js +42 -18
- package/lib/providers/cursor-auth.d.ts +1 -1
- package/lib/providers/cursor-auth.js +69 -24
- package/lib/providers/cursor-model-settings.js +14 -5
- package/lib/providers/cursor-usage.js +27 -13
- package/lib/providers/external-usage-controller.js +14 -5
- package/lib/providers/external-usage.js +59 -23
- package/lib/providers/grok.js +152 -89
- package/lib/providers/image-pool.js +29 -11
- package/lib/providers/minimax-usage.js +31 -11
- package/lib/providers/npm-cli-version.js +10 -3
- package/lib/providers/ollama-settings.js +24 -8
- package/lib/providers/ollama-web.js +29 -11
- package/lib/providers/ollama.js +129 -66
- package/lib/providers/pool-family.js +11 -3
- package/lib/providers/pool-health.js +28 -8
- package/lib/providers/pool-usage.js +16 -9
- package/lib/providers/pool.d.ts +8 -0
- package/lib/providers/pool.js +81 -42
- package/lib/providers/rate-limit.js +8 -2
- package/lib/providers/replay.js +40 -17
- package/lib/providers/reset-redemption.js +18 -6
- package/lib/tools/image-generate.js +123 -65
- package/lib/tools/registration.js +5 -2
- package/lib/tools/video-generate.js +51 -31
- package/lib/tools/x-search.js +50 -22
- package/lib/translate/anthropic.js +85 -37
- package/lib/translate/antigravity-schema.js +71 -28
- package/lib/translate/antigravity-thinking.js +22 -9
- package/lib/translate/antigravity.js +151 -52
- package/lib/translate/chat-completions.js +67 -23
- package/lib/translate/resolved.js +55 -21
- package/lib/translate/responses.js +69 -29
- package/lib/translate/sse.js +4 -1
- package/package.json +27 -4
package/lib/auth/device-flow.js
CHANGED
|
@@ -70,18 +70,24 @@ export class DeviceFlowManager {
|
|
|
70
70
|
const response = await fetchFn(spec.deviceCodeUrl, {
|
|
71
71
|
method: 'POST',
|
|
72
72
|
headers: {
|
|
73
|
-
|
|
73
|
+
accept: 'application/json',
|
|
74
74
|
'content-type': 'application/x-www-form-urlencoded',
|
|
75
75
|
},
|
|
76
|
-
body: new URLSearchParams({
|
|
76
|
+
body: new URLSearchParams({
|
|
77
|
+
client_id: spec.clientId,
|
|
78
|
+
scope: spec.scope,
|
|
79
|
+
}).toString(),
|
|
77
80
|
});
|
|
78
81
|
if (!response.ok) {
|
|
79
82
|
throw new Error(`${provider} device-code request failed (HTTP ${String(response.status)})`);
|
|
80
83
|
}
|
|
81
|
-
const wire = await parseProviderJson(response, 'device code request');
|
|
82
|
-
if (typeof wire.device_code !== 'string' ||
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
const wire = (await parseProviderJson(response, 'device code request'));
|
|
85
|
+
if (typeof wire.device_code !== 'string' ||
|
|
86
|
+
wire.device_code.length === 0 ||
|
|
87
|
+
typeof wire.user_code !== 'string' ||
|
|
88
|
+
wire.user_code.length === 0 ||
|
|
89
|
+
typeof wire.verification_uri !== 'string' ||
|
|
90
|
+
wire.verification_uri.length === 0) {
|
|
85
91
|
throw new Error(`${provider} device-code response is missing device_code/user_code/verification_uri`);
|
|
86
92
|
}
|
|
87
93
|
const intervalSec = typeof wire.interval === 'number' && wire.interval > 0
|
|
@@ -124,7 +130,7 @@ export class DeviceFlowManager {
|
|
|
124
130
|
const pollResponse = await fetchFn(spec.tokenUrl, {
|
|
125
131
|
method: 'POST',
|
|
126
132
|
headers: {
|
|
127
|
-
|
|
133
|
+
accept: 'application/json',
|
|
128
134
|
'content-type': 'application/x-www-form-urlencoded',
|
|
129
135
|
},
|
|
130
136
|
body: new URLSearchParams({
|
|
@@ -134,8 +140,9 @@ export class DeviceFlowManager {
|
|
|
134
140
|
}).toString(),
|
|
135
141
|
signal: controller.signal,
|
|
136
142
|
});
|
|
137
|
-
const result = await parseProviderJson(pollResponse, 'device token poll');
|
|
138
|
-
if (typeof result.access_token === 'string' &&
|
|
143
|
+
const result = (await parseProviderJson(pollResponse, 'device token poll'));
|
|
144
|
+
if (typeof result.access_token === 'string' &&
|
|
145
|
+
result.access_token.length > 0) {
|
|
139
146
|
settle(undefined, result.access_token);
|
|
140
147
|
return;
|
|
141
148
|
}
|
package/lib/auth/oauth-flow.js
CHANGED
|
@@ -9,9 +9,9 @@ import { createServer } from 'node:http';
|
|
|
9
9
|
import { createPkce, randomHex, randomToken } from './pkce.js';
|
|
10
10
|
/** Default attempt lifetime: three minutes for the user to complete login. */
|
|
11
11
|
export const DEFAULT_FLOW_TIMEOUT_MS = 180_000;
|
|
12
|
-
const SUCCESS_PAGE = '<!doctype html><html><head><meta charset="utf-8"><title>Login successful</title></head>'
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
const SUCCESS_PAGE = '<!doctype html><html><head><meta charset="utf-8"><title>Login successful</title></head>' +
|
|
13
|
+
'<body style="font-family:sans-serif"><h1>Login successful</h1>' +
|
|
14
|
+
'<p>You can close this tab and return to DeepSeek Harness.</p></body></html>';
|
|
15
15
|
function authorizationFailure(error) {
|
|
16
16
|
// Safe means this failure path carries no provider-controlled free text,
|
|
17
17
|
// not that no secret can exist elsewhere in the system.
|
|
@@ -40,13 +40,13 @@ function authorizationFailure(error) {
|
|
|
40
40
|
category = 'temporarily_unavailable';
|
|
41
41
|
break;
|
|
42
42
|
}
|
|
43
|
-
return 'authorization failed'
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
return ('authorization failed' +
|
|
44
|
+
(category === undefined ? '' : ` (${category})`) +
|
|
45
|
+
': [provider message omitted]');
|
|
46
46
|
}
|
|
47
47
|
function failurePage(detail) {
|
|
48
|
-
return '<!doctype html><html><head><meta charset="utf-8"><title>Login failed</title></head>'
|
|
49
|
-
|
|
48
|
+
return ('<!doctype html><html><head><meta charset="utf-8"><title>Login failed</title></head>' +
|
|
49
|
+
`<body style="font-family:sans-serif"><h1>Login failed</h1><p>${detail.replace(/[<>&]/g, '')}</p></body></html>`);
|
|
50
50
|
}
|
|
51
51
|
/**
|
|
52
52
|
* Loopback addresses one listen host covers. `localhost` resolves to ::1 or
|
|
@@ -69,7 +69,7 @@ function familyUnavailable(error) {
|
|
|
69
69
|
*/
|
|
70
70
|
async function listen(handler, spec) {
|
|
71
71
|
const hosts = listenHosts(spec.host);
|
|
72
|
-
const candidates = spec.ports.flatMap(port =>
|
|
72
|
+
const candidates = spec.ports.flatMap((port) => port === 0 ? [0, 0, 0] : [port]);
|
|
73
73
|
let lastError;
|
|
74
74
|
for (const candidate of candidates) {
|
|
75
75
|
const servers = [];
|
|
@@ -161,6 +161,8 @@ export class OAuthFlowManager {
|
|
|
161
161
|
rejectCode = reject;
|
|
162
162
|
});
|
|
163
163
|
let settled = false;
|
|
164
|
+
// Assigned after listen(); a callback that arrives first must see it absent.
|
|
165
|
+
// eslint-disable-next-line prefer-const -- the single write is after the listener exists
|
|
164
166
|
let timer;
|
|
165
167
|
let servers = [];
|
|
166
168
|
const handler = (request, response) => {
|
|
@@ -177,7 +179,8 @@ export class OAuthFlowManager {
|
|
|
177
179
|
response.end('state mismatch');
|
|
178
180
|
return;
|
|
179
181
|
}
|
|
180
|
-
if (url.searchParams.has('error_description') ||
|
|
182
|
+
if (url.searchParams.has('error_description') ||
|
|
183
|
+
url.searchParams.has('error')) {
|
|
181
184
|
const detail = authorizationFailure(url.searchParams.get('error'));
|
|
182
185
|
response.writeHead(200, { 'content-type': 'text/html' });
|
|
183
186
|
response.end(failurePage(detail));
|
package/lib/auth/rpc.d.ts
CHANGED
|
@@ -38,15 +38,15 @@ export interface VideoBytesResult {
|
|
|
38
38
|
export type SpeedTier = 'standard' | 'fast';
|
|
39
39
|
/** `speed` endpoint value: the session's choice plus the visibility list. */
|
|
40
40
|
export interface SpeedState {
|
|
41
|
-
/**
|
|
41
|
+
/** Explicit session speed, or the subagent's model default when supplied. */
|
|
42
42
|
tier: SpeedTier;
|
|
43
43
|
/** Codex model ids whose catalog advertises a fast tier. */
|
|
44
44
|
fastModels: string[];
|
|
45
45
|
}
|
|
46
|
-
/**
|
|
46
|
+
/** Session overrides plus persisted subagent model defaults. */
|
|
47
47
|
export interface SpeedController {
|
|
48
|
-
/**
|
|
49
|
-
speed(sessionId: string): Promise<SpeedState>;
|
|
48
|
+
/** Pass the current Codex model to resolve a subagent's default speed. */
|
|
49
|
+
speed(sessionId: string, model?: string): Promise<SpeedState>;
|
|
50
50
|
/** Set one session's speed tier. */
|
|
51
51
|
setSpeed(sessionId: string, tier: SpeedTier): Promise<void>;
|
|
52
52
|
}
|
|
@@ -65,6 +65,8 @@ export interface AccountStatus {
|
|
|
65
65
|
}
|
|
66
66
|
/** Login state of one provider, as rendered by the Settings page. */
|
|
67
67
|
export interface ProviderStatus {
|
|
68
|
+
/** Saved picker visibility, attached by the status RPC from local preferences. */
|
|
69
|
+
visibleModels?: string[];
|
|
68
70
|
/** Whether a login attempt is currently waiting for its code. */
|
|
69
71
|
busy: boolean;
|
|
70
72
|
/** Logged-in accounts, default first. */
|
|
@@ -110,6 +112,8 @@ export interface ProviderAccountCatalog {
|
|
|
110
112
|
}
|
|
111
113
|
/** Provider and account preference operations behind providerSettings/setProviderSettings. */
|
|
112
114
|
export interface ProviderSettingsController {
|
|
115
|
+
/** Local picker selection, without triggering model discovery. */
|
|
116
|
+
visibleModels?(provider: ProviderId): string[] | undefined;
|
|
113
117
|
get(provider: ProviderId, force: boolean): Promise<unknown>;
|
|
114
118
|
set(provider: ProviderId, settings: unknown): Promise<void>;
|
|
115
119
|
}
|
package/lib/auth/rpc.js
CHANGED
|
@@ -21,18 +21,44 @@ export const SUBSCRIPTIONS_AUTH_PREFIX = 'subscriptions-auth.';
|
|
|
21
21
|
* Kept in one place so the route table and the switch cannot drift apart.
|
|
22
22
|
*/
|
|
23
23
|
export const SUBSCRIPTIONS_AUTH_ENDPOINTS = [
|
|
24
|
-
'providerSettings',
|
|
25
|
-
'
|
|
26
|
-
'
|
|
27
|
-
'
|
|
28
|
-
'
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
-
'
|
|
32
|
-
'
|
|
24
|
+
'providerSettings',
|
|
25
|
+
'setProviderSettings',
|
|
26
|
+
'status',
|
|
27
|
+
'login',
|
|
28
|
+
'manual',
|
|
29
|
+
'cancel',
|
|
30
|
+
'logout',
|
|
31
|
+
'setDefault',
|
|
32
|
+
'usage',
|
|
33
|
+
'externalStatus',
|
|
34
|
+
'externalUsage',
|
|
35
|
+
'cursorStatus',
|
|
36
|
+
'cursorLogin',
|
|
37
|
+
'cursorCancel',
|
|
38
|
+
'cursorLogout',
|
|
39
|
+
'cursorUsage',
|
|
40
|
+
'cursorModels',
|
|
41
|
+
'cursorSettings',
|
|
42
|
+
'cursorSetSettings',
|
|
43
|
+
'ollamaSettings',
|
|
44
|
+
'ollamaSetSettings',
|
|
45
|
+
'ollamaModels',
|
|
46
|
+
'image',
|
|
47
|
+
'video',
|
|
48
|
+
'prepareReset',
|
|
49
|
+
'consumeReset',
|
|
50
|
+
'speed',
|
|
51
|
+
'setSpeed',
|
|
52
|
+
'modelDefaults',
|
|
53
|
+
'setModelDefault',
|
|
33
54
|
];
|
|
34
55
|
/** Media types the attachment store accepts (ImageMediaType). */
|
|
35
|
-
const IMAGE_MEDIA_TYPES = [
|
|
56
|
+
const IMAGE_MEDIA_TYPES = [
|
|
57
|
+
'image/png',
|
|
58
|
+
'image/jpeg',
|
|
59
|
+
'image/webp',
|
|
60
|
+
'image/gif',
|
|
61
|
+
];
|
|
36
62
|
/** Bare MP4 file names the `video` endpoint accepts (no path separators). */
|
|
37
63
|
const VIDEO_NAME_PATTERN = /^[\w.-]+\.mp4$/;
|
|
38
64
|
/** Payload carried no usable provider id — an RPC client bug, not a server failure. */
|
|
@@ -42,9 +68,16 @@ function readEnvelope(body) {
|
|
|
42
68
|
if (typeof body !== 'object' || body === null)
|
|
43
69
|
return undefined;
|
|
44
70
|
const record = body;
|
|
45
|
-
if (record.type !== 'client-request' ||
|
|
71
|
+
if (record.type !== 'client-request' ||
|
|
72
|
+
typeof record.rpcId !== 'string' ||
|
|
73
|
+
typeof record.method !== 'string')
|
|
46
74
|
return undefined;
|
|
47
|
-
return {
|
|
75
|
+
return {
|
|
76
|
+
type: 'client-request',
|
|
77
|
+
rpcId: record.rpcId,
|
|
78
|
+
method: record.method,
|
|
79
|
+
payload: record.payload,
|
|
80
|
+
};
|
|
48
81
|
}
|
|
49
82
|
function serverResponse(rpcId, result) {
|
|
50
83
|
return Response.json({ type: 'server-response', rpcId, result });
|
|
@@ -64,8 +97,14 @@ function fetchRouteFor(endpoint, handler) {
|
|
|
64
97
|
methods: ['POST'],
|
|
65
98
|
requestBody: 'buffered',
|
|
66
99
|
fetch: async (request) => {
|
|
67
|
-
if (request.headers
|
|
68
|
-
|
|
100
|
+
if (request.headers
|
|
101
|
+
.get('content-type')
|
|
102
|
+
?.split(';', 1)[0]
|
|
103
|
+
?.trim()
|
|
104
|
+
.toLowerCase() !== 'application/json') {
|
|
105
|
+
return new Response('content type must be application/json', {
|
|
106
|
+
status: 415,
|
|
107
|
+
});
|
|
69
108
|
}
|
|
70
109
|
let body;
|
|
71
110
|
try {
|
|
@@ -79,7 +118,11 @@ function fetchRouteFor(endpoint, handler) {
|
|
|
79
118
|
const rawId = body?.rpcId;
|
|
80
119
|
return serverResponse(typeof rawId === 'string' ? rawId : 'invalid-request', {
|
|
81
120
|
ok: false,
|
|
82
|
-
error: {
|
|
121
|
+
error: {
|
|
122
|
+
code: 'gateway/bad-request',
|
|
123
|
+
message: 'invalid client-request message',
|
|
124
|
+
details: { issues: [] },
|
|
125
|
+
},
|
|
83
126
|
});
|
|
84
127
|
}
|
|
85
128
|
if (envelope.method !== method) {
|
|
@@ -103,7 +146,10 @@ function failure(error) {
|
|
|
103
146
|
const message = error instanceof Error ? error.message : String(error);
|
|
104
147
|
if (error instanceof BadRequest) {
|
|
105
148
|
// The issues array is zod-shaped upstream; this channel validates by hand.
|
|
106
|
-
return {
|
|
149
|
+
return {
|
|
150
|
+
ok: false,
|
|
151
|
+
error: { code: 'bad-request', message, details: { issues: [] } },
|
|
152
|
+
};
|
|
107
153
|
}
|
|
108
154
|
return { ok: false, error: { code: 'internal', message, details: {} } };
|
|
109
155
|
}
|
|
@@ -111,7 +157,8 @@ function readProvider(payload) {
|
|
|
111
157
|
if (typeof payload !== 'object' || payload === null)
|
|
112
158
|
throw new BadRequest('payload must be an object');
|
|
113
159
|
const provider = payload.provider;
|
|
114
|
-
if (typeof provider !== 'string' ||
|
|
160
|
+
if (typeof provider !== 'string' ||
|
|
161
|
+
!PROVIDER_IDS.includes(provider)) {
|
|
115
162
|
throw new BadRequest(`payload.provider must be one of ${PROVIDER_IDS.join(', ')}`);
|
|
116
163
|
}
|
|
117
164
|
return provider;
|
|
@@ -183,7 +230,8 @@ function readImageRef(payload) {
|
|
|
183
230
|
throw new BadRequest('payload.attachmentId must be a non-empty string');
|
|
184
231
|
}
|
|
185
232
|
const mediaType = record.mediaType;
|
|
186
|
-
if (typeof mediaType !== 'string' ||
|
|
233
|
+
if (typeof mediaType !== 'string' ||
|
|
234
|
+
!IMAGE_MEDIA_TYPES.includes(mediaType)) {
|
|
187
235
|
throw new BadRequest(`payload.mediaType must be one of ${IMAGE_MEDIA_TYPES.join(', ')}`);
|
|
188
236
|
}
|
|
189
237
|
for (const field of ['bytes', 'width', 'height']) {
|
|
@@ -202,7 +250,7 @@ function readImageRef(payload) {
|
|
|
202
250
|
bytes: record.bytes,
|
|
203
251
|
width: record.width,
|
|
204
252
|
height: record.height,
|
|
205
|
-
...name === undefined ? {} : { name: name },
|
|
253
|
+
...(name === undefined ? {} : { name: name }),
|
|
206
254
|
};
|
|
207
255
|
}
|
|
208
256
|
/**
|
|
@@ -253,12 +301,26 @@ async function dispatch(controller, speed, modelDefaults, endpoint, payload, sig
|
|
|
253
301
|
// One provider's failure (a corrupt store entry, a broken flow) must not
|
|
254
302
|
// blind the whole page: it degrades to an error detail on that provider
|
|
255
303
|
// while the others still report their real status.
|
|
256
|
-
const entries = await Promise.all(PROVIDER_IDS.map(async (provider) => [
|
|
304
|
+
const entries = await Promise.all(PROVIDER_IDS.map(async (provider) => [
|
|
305
|
+
provider,
|
|
306
|
+
await controller.status(provider).catch((error) => ({
|
|
257
307
|
busy: false,
|
|
258
308
|
accounts: [],
|
|
259
309
|
detail: error instanceof Error ? error.message : String(error),
|
|
260
|
-
}))
|
|
261
|
-
|
|
310
|
+
})),
|
|
311
|
+
]));
|
|
312
|
+
return ok({
|
|
313
|
+
providers: Object.fromEntries(entries.map(([provider, status]) => {
|
|
314
|
+
const visibleModels = providerSettings?.visibleModels?.(provider);
|
|
315
|
+
return [
|
|
316
|
+
provider,
|
|
317
|
+
{
|
|
318
|
+
...status,
|
|
319
|
+
...(visibleModels === undefined ? {} : { visibleModels }),
|
|
320
|
+
},
|
|
321
|
+
];
|
|
322
|
+
})),
|
|
323
|
+
});
|
|
262
324
|
}
|
|
263
325
|
case 'login': {
|
|
264
326
|
const provider = readProvider(payload);
|
|
@@ -364,8 +426,8 @@ async function dispatch(controller, speed, modelDefaults, endpoint, payload, sig
|
|
|
364
426
|
}
|
|
365
427
|
}
|
|
366
428
|
return ok(await ollama.models({
|
|
367
|
-
...apiKey === undefined ? {} : { apiKey },
|
|
368
|
-
...baseURL === undefined ? {} : { baseURL },
|
|
429
|
+
...(apiKey === undefined ? {} : { apiKey }),
|
|
430
|
+
...(baseURL === undefined ? {} : { baseURL }),
|
|
369
431
|
}, signal));
|
|
370
432
|
}
|
|
371
433
|
case 'prepareReset':
|
|
@@ -382,7 +444,9 @@ async function dispatch(controller, speed, modelDefaults, endpoint, payload, sig
|
|
|
382
444
|
case 'video':
|
|
383
445
|
return ok(await controller.readVideo(readVideoName(payload), signal));
|
|
384
446
|
case 'speed':
|
|
385
|
-
return ok(await speed.speed(readSessionId(payload)
|
|
447
|
+
return ok(await speed.speed(readSessionId(payload), payload.model === undefined
|
|
448
|
+
? undefined
|
|
449
|
+
: readString(payload, 'model')));
|
|
386
450
|
case 'setSpeed':
|
|
387
451
|
await speed.setSpeed(readSessionId(payload), readSpeedTier(payload));
|
|
388
452
|
return ok({ ok: true });
|
package/lib/auth/store.js
CHANGED
|
@@ -18,7 +18,13 @@ import { chmod, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises'
|
|
|
18
18
|
import { dirname } from 'node:path';
|
|
19
19
|
import { dshHomePath } from '@deepseek-ai/dsh-home-paths';
|
|
20
20
|
/** Every provider route, in display order. */
|
|
21
|
-
export const PROVIDER_IDS = [
|
|
21
|
+
export const PROVIDER_IDS = [
|
|
22
|
+
'codex',
|
|
23
|
+
'claude',
|
|
24
|
+
'grok',
|
|
25
|
+
'copilot',
|
|
26
|
+
'antigravity',
|
|
27
|
+
];
|
|
22
28
|
/**
|
|
23
29
|
* The stable identity of one session's account: Codex keys on workspace AND
|
|
24
30
|
* user (email fallback, workspace-only for unidentified legacy sessions),
|
|
@@ -37,40 +43,53 @@ export function accountKeyOf(provider, session) {
|
|
|
37
43
|
switch (provider) {
|
|
38
44
|
case 'codex': {
|
|
39
45
|
const codex = session;
|
|
40
|
-
const payload = typeof codex.idToken === 'string'
|
|
46
|
+
const payload = typeof codex.idToken === 'string'
|
|
47
|
+
? decodeJwtPayload(codex.idToken)
|
|
48
|
+
: undefined;
|
|
41
49
|
const auth = claimObject(payload?.['https://api.openai.com/auth']);
|
|
42
50
|
const user = nonEmpty(auth?.chatgpt_user_id) ?? nonEmpty(auth?.user_id);
|
|
43
51
|
const profile = claimObject(payload?.['https://api.openai.com/profile']);
|
|
44
|
-
const email = nonEmpty(codex.emailAddress) ??
|
|
52
|
+
const email = nonEmpty(codex.emailAddress) ??
|
|
53
|
+
nonEmpty(payload?.email) ??
|
|
54
|
+
nonEmpty(profile?.email);
|
|
45
55
|
if (user === undefined && email === undefined)
|
|
46
56
|
return codex.accountId;
|
|
47
57
|
// JSON tuple encoding avoids separator collisions and distinguishes IDs from emails.
|
|
48
|
-
return JSON.stringify([
|
|
58
|
+
return JSON.stringify([
|
|
59
|
+
codex.accountId,
|
|
60
|
+
user === undefined ? 'email' : 'user',
|
|
61
|
+
user ?? email.toLowerCase(),
|
|
62
|
+
]);
|
|
49
63
|
}
|
|
50
64
|
case 'claude':
|
|
51
|
-
return session.emailAddress ??
|
|
65
|
+
return (session.emailAddress ??
|
|
66
|
+
tokenHash(session.refreshToken));
|
|
52
67
|
case 'grok':
|
|
53
68
|
return session.account ?? tokenHash(session.refreshToken);
|
|
54
69
|
case 'antigravity':
|
|
55
|
-
return session.account ??
|
|
70
|
+
return (session.account ??
|
|
71
|
+
tokenHash(session.refreshToken));
|
|
56
72
|
case 'copilot':
|
|
57
|
-
return session.account ?? tokenHash(session.refreshToken);
|
|
73
|
+
return (session.account ?? tokenHash(session.refreshToken));
|
|
58
74
|
}
|
|
59
75
|
}
|
|
60
76
|
function nonEmpty(value) {
|
|
61
|
-
return typeof value === 'string' && value.trim().length > 0
|
|
77
|
+
return typeof value === 'string' && value.trim().length > 0
|
|
78
|
+
? value.trim()
|
|
79
|
+
: undefined;
|
|
62
80
|
}
|
|
63
81
|
function claimObject(value) {
|
|
64
82
|
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
65
|
-
? value
|
|
83
|
+
? value
|
|
84
|
+
: undefined;
|
|
66
85
|
}
|
|
67
86
|
/** Resolve only recorded aliases, not an ambiguous workspace-wide match. */
|
|
68
87
|
function resolveAccount(entry, key) {
|
|
69
88
|
const seen = new Set();
|
|
70
|
-
while (!Object.hasOwn(entry.accounts, key)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
89
|
+
while (!Object.hasOwn(entry.accounts, key) &&
|
|
90
|
+
entry.aliases !== undefined &&
|
|
91
|
+
Object.hasOwn(entry.aliases, key) &&
|
|
92
|
+
!seen.has(key)) {
|
|
74
93
|
seen.add(key);
|
|
75
94
|
key = entry.aliases[key];
|
|
76
95
|
}
|
|
@@ -107,9 +126,13 @@ export async function resolveAccountKey(provider, account, path = authFilePath()
|
|
|
107
126
|
}
|
|
108
127
|
/** Migrate workspace-only keys once; retain collisions rather than discard credentials. */
|
|
109
128
|
function codexEmail(session) {
|
|
110
|
-
const payload = typeof session.idToken === 'string'
|
|
129
|
+
const payload = typeof session.idToken === 'string'
|
|
130
|
+
? decodeJwtPayload(session.idToken)
|
|
131
|
+
: undefined;
|
|
111
132
|
const profile = claimObject(payload?.['https://api.openai.com/profile']);
|
|
112
|
-
return (nonEmpty(session.emailAddress) ??
|
|
133
|
+
return (nonEmpty(session.emailAddress) ??
|
|
134
|
+
nonEmpty(payload?.email) ??
|
|
135
|
+
nonEmpty(profile?.email))?.toLowerCase();
|
|
113
136
|
}
|
|
114
137
|
function codexUserKey(session) {
|
|
115
138
|
const key = accountKeyOf('codex', session);
|
|
@@ -179,13 +202,16 @@ function assertSessionShape(provider, account, value) {
|
|
|
179
202
|
throw new Error(`subscriptions auth store: entry "${provider}/${account}" is not an object; fix or delete the store file`);
|
|
180
203
|
}
|
|
181
204
|
const entry = value;
|
|
182
|
-
if (typeof entry.accessToken !== 'string' ||
|
|
183
|
-
|
|
184
|
-
|
|
205
|
+
if (typeof entry.accessToken !== 'string' ||
|
|
206
|
+
entry.accessToken.length === 0 ||
|
|
207
|
+
typeof entry.refreshToken !== 'string' ||
|
|
208
|
+
entry.refreshToken.length === 0 ||
|
|
209
|
+
typeof entry.expiresAt !== 'number' ||
|
|
210
|
+
!Number.isFinite(entry.expiresAt)) {
|
|
185
211
|
throw new Error(`subscriptions auth store: entry "${provider}/${account}" is missing accessToken/refreshToken/expiresAt; fix or delete the store file`);
|
|
186
212
|
}
|
|
187
|
-
if (provider === 'antigravity'
|
|
188
|
-
|
|
213
|
+
if (provider === 'antigravity' &&
|
|
214
|
+
(typeof entry.projectId !== 'string' || entry.projectId.length === 0)) {
|
|
189
215
|
throw new Error('subscriptions auth store: entry "antigravity" is missing projectId; log out and complete Antigravity login again');
|
|
190
216
|
}
|
|
191
217
|
}
|
|
@@ -261,12 +287,19 @@ function parseStore(text, path) {
|
|
|
261
287
|
continue;
|
|
262
288
|
}
|
|
263
289
|
const session = record;
|
|
264
|
-
const key = provider === 'codex'
|
|
265
|
-
|
|
290
|
+
const key = provider === 'codex'
|
|
291
|
+
? session.accountId
|
|
292
|
+
: accountKeyOf(provider, session);
|
|
293
|
+
store[provider] = {
|
|
294
|
+
default: key,
|
|
295
|
+
accounts: { [key]: session },
|
|
296
|
+
};
|
|
266
297
|
continue;
|
|
267
298
|
}
|
|
268
299
|
const accounts = record.accounts;
|
|
269
|
-
if (typeof accounts !== 'object' ||
|
|
300
|
+
if (typeof accounts !== 'object' ||
|
|
301
|
+
accounts === null ||
|
|
302
|
+
Array.isArray(accounts)) {
|
|
270
303
|
console.warn(`subscriptions auth store: entry "${provider}" has no accounts map; skipped`);
|
|
271
304
|
continue;
|
|
272
305
|
}
|
|
@@ -288,7 +321,11 @@ function parseStore(text, path) {
|
|
|
288
321
|
const validDefault = record.default === undefined || record.default in kept
|
|
289
322
|
? record.default
|
|
290
323
|
: Object.keys(kept)[0];
|
|
291
|
-
store[provider] = {
|
|
324
|
+
store[provider] = {
|
|
325
|
+
...record,
|
|
326
|
+
default: validDefault,
|
|
327
|
+
accounts: kept,
|
|
328
|
+
};
|
|
292
329
|
}
|
|
293
330
|
if (store.codex !== undefined)
|
|
294
331
|
migrateCodex(store.codex);
|
|
@@ -299,9 +336,12 @@ function isValidSessionShape(value) {
|
|
|
299
336
|
if (typeof value !== 'object' || value === null)
|
|
300
337
|
return false;
|
|
301
338
|
const entry = value;
|
|
302
|
-
return typeof entry.accessToken === 'string' &&
|
|
303
|
-
|
|
304
|
-
|
|
339
|
+
return (typeof entry.accessToken === 'string' &&
|
|
340
|
+
entry.accessToken.length > 0 &&
|
|
341
|
+
typeof entry.refreshToken === 'string' &&
|
|
342
|
+
entry.refreshToken.length > 0 &&
|
|
343
|
+
typeof entry.expiresAt === 'number' &&
|
|
344
|
+
Number.isFinite(entry.expiresAt));
|
|
305
345
|
}
|
|
306
346
|
/** Persist the whole store atomically with owner-only permissions. */
|
|
307
347
|
async function writeStore(store, path) {
|
|
@@ -361,7 +401,10 @@ export async function listAccounts(provider, path = authFilePath()) {
|
|
|
361
401
|
const entry = (await loadStore(path))[provider];
|
|
362
402
|
if (entry === undefined)
|
|
363
403
|
return [];
|
|
364
|
-
const accounts = Object.entries(entry.accounts).map(([key, session]) => ({
|
|
404
|
+
const accounts = Object.entries(entry.accounts).map(([key, session]) => ({
|
|
405
|
+
key,
|
|
406
|
+
session,
|
|
407
|
+
}));
|
|
365
408
|
accounts.sort((a, b) => Number(b.key === entry.default) - Number(a.key === entry.default));
|
|
366
409
|
return accounts;
|
|
367
410
|
}
|
|
@@ -402,7 +445,8 @@ export async function saveAccountSession(provider, account, session, path = auth
|
|
|
402
445
|
if (entry !== undefined) {
|
|
403
446
|
account = resolveAccount(entry, account);
|
|
404
447
|
if (provider === 'codex') {
|
|
405
|
-
account =
|
|
448
|
+
account =
|
|
449
|
+
reconcileCodexIdentity(entry, session) ?? accountKeyOf('codex', session);
|
|
406
450
|
}
|
|
407
451
|
}
|
|
408
452
|
;
|
|
@@ -439,7 +483,9 @@ export async function deleteAccountSession(provider, account, path = authFilePat
|
|
|
439
483
|
;
|
|
440
484
|
store[provider] = {
|
|
441
485
|
...entry,
|
|
442
|
-
...entry.default === account
|
|
486
|
+
...(entry.default === account
|
|
487
|
+
? { default: Object.keys(accounts)[0] }
|
|
488
|
+
: { default: entry.default }),
|
|
443
489
|
accounts,
|
|
444
490
|
};
|
|
445
491
|
}
|
|
@@ -462,7 +508,10 @@ export async function setDefaultAccount(provider, account, path = authFilePath()
|
|
|
462
508
|
throw new Error(`no ${provider} account "${account}" is logged in`);
|
|
463
509
|
}
|
|
464
510
|
;
|
|
465
|
-
store[provider] = {
|
|
511
|
+
store[provider] = {
|
|
512
|
+
...entry,
|
|
513
|
+
default: account,
|
|
514
|
+
};
|
|
466
515
|
await writeStore(store, path);
|
|
467
516
|
});
|
|
468
517
|
}
|
|
@@ -2,7 +2,7 @@ import type { ConnectionHandle } from '@deepseek-ai/dsh-api-remotes/client';
|
|
|
2
2
|
import type { SubscriptionsSectionInjected } from './SubscriptionsSection.js';
|
|
3
3
|
type Translate = SubscriptionsSectionInjected['t'];
|
|
4
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 }: {
|
|
5
|
+
export declare function CursorCard({ rpc, t, }: {
|
|
6
6
|
rpc: ConnectionHandle['rpc'];
|
|
7
7
|
t: Translate;
|
|
8
8
|
}): import("react").JSX.Element;
|