dsh-subscriptions 0.1.5 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +212 -154
- 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 +73 -2
- package/lib/auth/rpc.js +124 -23
- 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 +13 -0
- package/lib/client/OllamaCard.js +94 -0
- package/lib/client/OllamaSettingsManager.d.ts +24 -0
- package/lib/client/OllamaSettingsManager.js +329 -0
- package/lib/client/ProviderAccountManager.d.ts +1 -1
- package/lib/client/ProviderAccountManager.js +164 -49
- package/lib/client/ProviderModelEditor.js +109 -26
- package/lib/client/SpeedSelect.d.ts +62 -14
- package/lib/client/SpeedSelect.js +257 -46
- package/lib/client/SubscriptionUsageBadge.d.ts +5 -11
- package/lib/client/SubscriptionUsageBadge.js +301 -112
- package/lib/client/SubscriptionsSection.d.ts +2 -0
- package/lib/client/SubscriptionsSection.js +259 -85
- package/lib/client/UsageBadgeDisplaySetting.js +35 -12
- package/lib/client/UsageMeter.d.ts +1 -1
- package/lib/client/UsageMeter.js +69 -27
- 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 +78 -0
- package/lib/client/locales.js +121 -34
- package/lib/client/ollama-draft.d.ts +229 -0
- package/lib/client/ollama-draft.js +255 -0
- 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.d.ts +7 -0
- package/lib/client/usage-pace.js +43 -10
- package/lib/client.js +1647 -266
- package/lib/client.js.map +1 -1
- package/lib/http.js +1 -1
- package/lib/index.d.ts +28 -3
- package/lib/index.js +9329 -8217
- package/lib/model-defaults.js +4 -2
- package/lib/provider-settings.d.ts +14 -1
- package/lib/provider-settings.js +64 -20
- 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 +23 -3
- package/lib/providers/claude.js +167 -92
- 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.d.ts +14 -0
- package/lib/providers/common.js +118 -38
- 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.d.ts +9 -2
- package/lib/providers/external-usage-controller.js +30 -7
- package/lib/providers/external-usage.d.ts +8 -0
- package/lib/providers/external-usage.js +140 -20
- 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.d.ts +90 -0
- package/lib/providers/ollama-settings.js +213 -0
- package/lib/providers/ollama-web.d.ts +46 -0
- package/lib/providers/ollama-web.js +231 -0
- package/lib/providers/ollama.d.ts +107 -0
- package/lib/providers/ollama.js +608 -0
- 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 +141 -50
- 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 +26 -4
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
|
@@ -23,7 +23,7 @@ export declare const SUBSCRIPTIONS_AUTH_PREFIX = "subscriptions-auth.";
|
|
|
23
23
|
* Every endpoint {@link dispatch} answers; each gets one exact Fetch route.
|
|
24
24
|
* Kept in one place so the route table and the switch cannot drift apart.
|
|
25
25
|
*/
|
|
26
|
-
export declare const SUBSCRIPTIONS_AUTH_ENDPOINTS: readonly ["providerSettings", "setProviderSettings", "status", "login", "manual", "cancel", "logout", "setDefault", "usage", "externalStatus", "externalUsage", "cursorStatus", "cursorLogin", "cursorCancel", "cursorLogout", "cursorUsage", "cursorModels", "cursorSettings", "cursorSetSettings", "image", "video", "prepareReset", "consumeReset", "speed", "setSpeed", "modelDefaults", "setModelDefault"];
|
|
26
|
+
export declare const SUBSCRIPTIONS_AUTH_ENDPOINTS: readonly ["providerSettings", "setProviderSettings", "status", "login", "manual", "cancel", "logout", "setDefault", "usage", "externalStatus", "externalUsage", "cursorStatus", "cursorLogin", "cursorCancel", "cursorLogout", "cursorUsage", "cursorModels", "cursorSettings", "cursorSetSettings", "ollamaSettings", "ollamaSetSettings", "ollamaModels", "image", "video", "prepareReset", "consumeReset", "speed", "setSpeed", "modelDefaults", "setModelDefault"];
|
|
27
27
|
/** Decoded image bytes returned by the `image` endpoint. */
|
|
28
28
|
export interface ImageBytesResult {
|
|
29
29
|
mediaType: string;
|
|
@@ -201,11 +201,82 @@ export interface CursorRpcController {
|
|
|
201
201
|
settings(force: boolean, signal: AbortSignal): Promise<unknown>;
|
|
202
202
|
setSettings(input: unknown): Promise<void>;
|
|
203
203
|
}
|
|
204
|
+
/** One model in the Ollama Cloud picker catalog, with its capability flags. */
|
|
205
|
+
export interface OllamaModelView {
|
|
206
|
+
id: string;
|
|
207
|
+
name: string;
|
|
208
|
+
/** The endpoint advertised the `vision` capability. */
|
|
209
|
+
vision: boolean;
|
|
210
|
+
/** The endpoint advertised the `thinking` capability. */
|
|
211
|
+
thinking: boolean;
|
|
212
|
+
contextWindow?: number;
|
|
213
|
+
/** Advertised reasoning levels, when the model reasons. */
|
|
214
|
+
efforts?: {
|
|
215
|
+
id: string;
|
|
216
|
+
name: string;
|
|
217
|
+
}[];
|
|
218
|
+
/** The level preselected for the model, when one is known. */
|
|
219
|
+
defaultEffort?: string;
|
|
220
|
+
}
|
|
221
|
+
/** One candidate returned by the cloud listing; adopting it stores `id`. */
|
|
222
|
+
export interface OllamaCloudCandidate {
|
|
223
|
+
/** Normalized wire id, e.g. `glm-5.3:cloud`. */
|
|
224
|
+
id: string;
|
|
225
|
+
/** The id exactly as the listing published it, e.g. `glm-5.3`. */
|
|
226
|
+
rawId: string;
|
|
227
|
+
/** Display basename for the checkbox row. */
|
|
228
|
+
name: string;
|
|
229
|
+
vision: boolean;
|
|
230
|
+
thinking: boolean;
|
|
231
|
+
contextWindow?: number;
|
|
232
|
+
efforts: string[];
|
|
233
|
+
defaultEffort?: string;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* What the Ollama card renders: the stored overrides, the effective values
|
|
237
|
+
* after config/default fallback, and the catalog the picker serves.
|
|
238
|
+
*/
|
|
239
|
+
export interface OllamaSettingsView {
|
|
240
|
+
provider: 'ollama-cloud';
|
|
241
|
+
/** The stored document; absent fields mean "no override". */
|
|
242
|
+
settings: unknown;
|
|
243
|
+
effective: {
|
|
244
|
+
baseURL: string;
|
|
245
|
+
apiKeyRef: string;
|
|
246
|
+
web: boolean;
|
|
247
|
+
};
|
|
248
|
+
/** The effective catalog, in picker order (empty hides the picker entry). */
|
|
249
|
+
models: OllamaModelView[];
|
|
250
|
+
/** A non-empty `config.ollama.models` wins over the store, so card edits are inert. */
|
|
251
|
+
configOverridesModels: boolean;
|
|
252
|
+
}
|
|
253
|
+
/** Ollama Cloud catalog and connection settings; the API key stays in DSH credentials. */
|
|
254
|
+
export interface OllamaRpcController {
|
|
255
|
+
settings(): Promise<OllamaSettingsView>;
|
|
256
|
+
/** Validate and persist the store document, then re-announce the route. */
|
|
257
|
+
setSettings(input: unknown): Promise<void>;
|
|
258
|
+
/**
|
|
259
|
+
* List the endpoint's Cloud models for the adoption checklist.
|
|
260
|
+
* @param input - an optional one-time API key and base URL override; the
|
|
261
|
+
* key is used for this listing only and never persisted.
|
|
262
|
+
* @param signal - caller cancellation from the RPC transport.
|
|
263
|
+
*/
|
|
264
|
+
models(input: {
|
|
265
|
+
apiKey?: string;
|
|
266
|
+
baseURL?: string;
|
|
267
|
+
}, signal: AbortSignal): Promise<{
|
|
268
|
+
models: OllamaCloudCandidate[];
|
|
269
|
+
}>;
|
|
270
|
+
}
|
|
204
271
|
/**
|
|
205
272
|
* Register the `/subscriptions-auth` RPC channel when a host connection exists.
|
|
206
273
|
* @param ctx - the plugin context (headless profiles have no `connection`).
|
|
207
274
|
* @param controller - the auth operations backing the endpoints.
|
|
208
275
|
* @param speed - the per-session speed-tier state backing the Speed toggle.
|
|
209
276
|
* @param modelDefaults - optional per-model default-effort state backing `modelDefaults`/`setModelDefault`.
|
|
277
|
+
* @param providerSettings - optional OAuth provider/account preferences.
|
|
278
|
+
* @param externalUsage - optional credential-backed quota readers.
|
|
279
|
+
* @param cursor - optional Cursor account and model-visibility actions.
|
|
280
|
+
* @param ollama - optional Ollama Cloud catalog and connection settings.
|
|
210
281
|
*/
|
|
211
|
-
export declare function registerAuthRpc(ctx: Context, controller: AuthController, speed: SpeedController, modelDefaults?: ModelDefaultsController | undefined, providerSettings?: ProviderSettingsController | undefined, externalUsage?: ExternalUsageRpcController | undefined, cursor?: CursorRpcController | undefined): void;
|
|
282
|
+
export declare function registerAuthRpc(ctx: Context, controller: AuthController, speed: SpeedController, modelDefaults?: ModelDefaultsController | undefined, providerSettings?: ProviderSettingsController | undefined, externalUsage?: ExternalUsageRpcController | undefined, cursor?: CursorRpcController | undefined, ollama?: OllamaRpcController | undefined): void;
|
package/lib/auth/rpc.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { AttachmentId } from '@deepseek-ai/dsh-attachment';
|
|
10
10
|
import { PROVIDER_IDS } from './store.js';
|
|
11
11
|
import { EXTERNAL_USAGE_SOURCES } from '../providers/external-usage-controller.js';
|
|
12
|
+
import { validateOllamaBaseURL } from '../providers/ollama-settings.js';
|
|
12
13
|
/**
|
|
13
14
|
* Endpoint-name prefix under the shared `/api` channel: endpoint `status`
|
|
14
15
|
* is served at `/api/subscriptions-auth.status`. The browser half calls
|
|
@@ -20,17 +21,44 @@ export const SUBSCRIPTIONS_AUTH_PREFIX = 'subscriptions-auth.';
|
|
|
20
21
|
* Kept in one place so the route table and the switch cannot drift apart.
|
|
21
22
|
*/
|
|
22
23
|
export const SUBSCRIPTIONS_AUTH_ENDPOINTS = [
|
|
23
|
-
'providerSettings',
|
|
24
|
-
'
|
|
25
|
-
'
|
|
26
|
-
'
|
|
27
|
-
'
|
|
28
|
-
'
|
|
29
|
-
'
|
|
30
|
-
'
|
|
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',
|
|
31
54
|
];
|
|
32
55
|
/** Media types the attachment store accepts (ImageMediaType). */
|
|
33
|
-
const IMAGE_MEDIA_TYPES = [
|
|
56
|
+
const IMAGE_MEDIA_TYPES = [
|
|
57
|
+
'image/png',
|
|
58
|
+
'image/jpeg',
|
|
59
|
+
'image/webp',
|
|
60
|
+
'image/gif',
|
|
61
|
+
];
|
|
34
62
|
/** Bare MP4 file names the `video` endpoint accepts (no path separators). */
|
|
35
63
|
const VIDEO_NAME_PATTERN = /^[\w.-]+\.mp4$/;
|
|
36
64
|
/** Payload carried no usable provider id — an RPC client bug, not a server failure. */
|
|
@@ -40,9 +68,16 @@ function readEnvelope(body) {
|
|
|
40
68
|
if (typeof body !== 'object' || body === null)
|
|
41
69
|
return undefined;
|
|
42
70
|
const record = body;
|
|
43
|
-
if (record.type !== 'client-request' ||
|
|
71
|
+
if (record.type !== 'client-request' ||
|
|
72
|
+
typeof record.rpcId !== 'string' ||
|
|
73
|
+
typeof record.method !== 'string')
|
|
44
74
|
return undefined;
|
|
45
|
-
return {
|
|
75
|
+
return {
|
|
76
|
+
type: 'client-request',
|
|
77
|
+
rpcId: record.rpcId,
|
|
78
|
+
method: record.method,
|
|
79
|
+
payload: record.payload,
|
|
80
|
+
};
|
|
46
81
|
}
|
|
47
82
|
function serverResponse(rpcId, result) {
|
|
48
83
|
return Response.json({ type: 'server-response', rpcId, result });
|
|
@@ -62,8 +97,14 @@ function fetchRouteFor(endpoint, handler) {
|
|
|
62
97
|
methods: ['POST'],
|
|
63
98
|
requestBody: 'buffered',
|
|
64
99
|
fetch: async (request) => {
|
|
65
|
-
if (request.headers
|
|
66
|
-
|
|
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
|
+
});
|
|
67
108
|
}
|
|
68
109
|
let body;
|
|
69
110
|
try {
|
|
@@ -77,7 +118,11 @@ function fetchRouteFor(endpoint, handler) {
|
|
|
77
118
|
const rawId = body?.rpcId;
|
|
78
119
|
return serverResponse(typeof rawId === 'string' ? rawId : 'invalid-request', {
|
|
79
120
|
ok: false,
|
|
80
|
-
error: {
|
|
121
|
+
error: {
|
|
122
|
+
code: 'gateway/bad-request',
|
|
123
|
+
message: 'invalid client-request message',
|
|
124
|
+
details: { issues: [] },
|
|
125
|
+
},
|
|
81
126
|
});
|
|
82
127
|
}
|
|
83
128
|
if (envelope.method !== method) {
|
|
@@ -101,7 +146,10 @@ function failure(error) {
|
|
|
101
146
|
const message = error instanceof Error ? error.message : String(error);
|
|
102
147
|
if (error instanceof BadRequest) {
|
|
103
148
|
// The issues array is zod-shaped upstream; this channel validates by hand.
|
|
104
|
-
return {
|
|
149
|
+
return {
|
|
150
|
+
ok: false,
|
|
151
|
+
error: { code: 'bad-request', message, details: { issues: [] } },
|
|
152
|
+
};
|
|
105
153
|
}
|
|
106
154
|
return { ok: false, error: { code: 'internal', message, details: {} } };
|
|
107
155
|
}
|
|
@@ -109,7 +157,8 @@ function readProvider(payload) {
|
|
|
109
157
|
if (typeof payload !== 'object' || payload === null)
|
|
110
158
|
throw new BadRequest('payload must be an object');
|
|
111
159
|
const provider = payload.provider;
|
|
112
|
-
if (typeof provider !== 'string' ||
|
|
160
|
+
if (typeof provider !== 'string' ||
|
|
161
|
+
!PROVIDER_IDS.includes(provider)) {
|
|
113
162
|
throw new BadRequest(`payload.provider must be one of ${PROVIDER_IDS.join(', ')}`);
|
|
114
163
|
}
|
|
115
164
|
return provider;
|
|
@@ -121,6 +170,18 @@ function readString(payload, field) {
|
|
|
121
170
|
}
|
|
122
171
|
return value;
|
|
123
172
|
}
|
|
173
|
+
/** Read a field that may be omitted, rejecting an empty or non-string value. */
|
|
174
|
+
function readOptionalString(payload, field) {
|
|
175
|
+
if (typeof payload !== 'object' || payload === null)
|
|
176
|
+
throw new BadRequest('payload must be an object');
|
|
177
|
+
const value = payload[field];
|
|
178
|
+
if (value === undefined)
|
|
179
|
+
return undefined;
|
|
180
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
181
|
+
throw new BadRequest(`payload.${field} must be a non-empty string when present`);
|
|
182
|
+
}
|
|
183
|
+
return value;
|
|
184
|
+
}
|
|
124
185
|
/** Validate the `setModelDefault` endpoint's payload. */
|
|
125
186
|
function readModelDefaultInput(payload) {
|
|
126
187
|
const provider = readProvider(payload);
|
|
@@ -169,7 +230,8 @@ function readImageRef(payload) {
|
|
|
169
230
|
throw new BadRequest('payload.attachmentId must be a non-empty string');
|
|
170
231
|
}
|
|
171
232
|
const mediaType = record.mediaType;
|
|
172
|
-
if (typeof mediaType !== 'string' ||
|
|
233
|
+
if (typeof mediaType !== 'string' ||
|
|
234
|
+
!IMAGE_MEDIA_TYPES.includes(mediaType)) {
|
|
173
235
|
throw new BadRequest(`payload.mediaType must be one of ${IMAGE_MEDIA_TYPES.join(', ')}`);
|
|
174
236
|
}
|
|
175
237
|
for (const field of ['bytes', 'width', 'height']) {
|
|
@@ -188,7 +250,7 @@ function readImageRef(payload) {
|
|
|
188
250
|
bytes: record.bytes,
|
|
189
251
|
width: record.width,
|
|
190
252
|
height: record.height,
|
|
191
|
-
...name === undefined ? {} : { name: name },
|
|
253
|
+
...(name === undefined ? {} : { name: name }),
|
|
192
254
|
};
|
|
193
255
|
}
|
|
194
256
|
/**
|
|
@@ -222,7 +284,7 @@ function readSessionId(payload) {
|
|
|
222
284
|
throw new BadRequest('payload must be an object');
|
|
223
285
|
return readString(payload, 'sessionId');
|
|
224
286
|
}
|
|
225
|
-
async function dispatch(controller, speed, modelDefaults, endpoint, payload, signal, providerSettings, externalUsage, cursor) {
|
|
287
|
+
async function dispatch(controller, speed, modelDefaults, endpoint, payload, signal, providerSettings, externalUsage, cursor, ollama) {
|
|
226
288
|
switch (endpoint) {
|
|
227
289
|
case 'providerSettings':
|
|
228
290
|
if (!providerSettings)
|
|
@@ -239,11 +301,14 @@ async function dispatch(controller, speed, modelDefaults, endpoint, payload, sig
|
|
|
239
301
|
// One provider's failure (a corrupt store entry, a broken flow) must not
|
|
240
302
|
// blind the whole page: it degrades to an error detail on that provider
|
|
241
303
|
// while the others still report their real status.
|
|
242
|
-
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) => ({
|
|
243
307
|
busy: false,
|
|
244
308
|
accounts: [],
|
|
245
309
|
detail: error instanceof Error ? error.message : String(error),
|
|
246
|
-
}))
|
|
310
|
+
})),
|
|
311
|
+
]));
|
|
247
312
|
return ok({ providers: Object.fromEntries(entries) });
|
|
248
313
|
}
|
|
249
314
|
case 'login': {
|
|
@@ -322,6 +387,38 @@ async function dispatch(controller, speed, modelDefaults, endpoint, payload, sig
|
|
|
322
387
|
throw new BadRequest('payload must be an object');
|
|
323
388
|
await cursor.setSettings(payload.settings);
|
|
324
389
|
return ok({ ok: true });
|
|
390
|
+
case 'ollamaSettings':
|
|
391
|
+
if (!ollama)
|
|
392
|
+
throw new BadRequest('Ollama settings are unavailable');
|
|
393
|
+
return ok(await ollama.settings());
|
|
394
|
+
case 'ollamaSetSettings':
|
|
395
|
+
if (!ollama)
|
|
396
|
+
throw new BadRequest('Ollama settings are unavailable');
|
|
397
|
+
if (payload === null || typeof payload !== 'object')
|
|
398
|
+
throw new BadRequest('payload must be an object');
|
|
399
|
+
await ollama.setSettings(payload.settings);
|
|
400
|
+
return ok({ ok: true });
|
|
401
|
+
case 'ollamaModels': {
|
|
402
|
+
if (!ollama)
|
|
403
|
+
throw new BadRequest('Ollama settings are unavailable');
|
|
404
|
+
const apiKey = readOptionalString(payload, 'apiKey');
|
|
405
|
+
const rawBaseURL = readOptionalString(payload, 'baseURL');
|
|
406
|
+
let baseURL;
|
|
407
|
+
if (rawBaseURL !== undefined) {
|
|
408
|
+
// The listing may run against a not-yet-saved draft address, so it
|
|
409
|
+
// passes the same URL check the store applies.
|
|
410
|
+
try {
|
|
411
|
+
baseURL = validateOllamaBaseURL(rawBaseURL);
|
|
412
|
+
}
|
|
413
|
+
catch (error) {
|
|
414
|
+
throw new BadRequest(error instanceof Error ? error.message : String(error));
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return ok(await ollama.models({
|
|
418
|
+
...(apiKey === undefined ? {} : { apiKey }),
|
|
419
|
+
...(baseURL === undefined ? {} : { baseURL }),
|
|
420
|
+
}, signal));
|
|
421
|
+
}
|
|
325
422
|
case 'prepareReset':
|
|
326
423
|
if (!controller.prepareReset)
|
|
327
424
|
throw new BadRequest('Reset redemption unavailable');
|
|
@@ -363,8 +460,12 @@ async function dispatch(controller, speed, modelDefaults, endpoint, payload, sig
|
|
|
363
460
|
* @param controller - the auth operations backing the endpoints.
|
|
364
461
|
* @param speed - the per-session speed-tier state backing the Speed toggle.
|
|
365
462
|
* @param modelDefaults - optional per-model default-effort state backing `modelDefaults`/`setModelDefault`.
|
|
463
|
+
* @param providerSettings - optional OAuth provider/account preferences.
|
|
464
|
+
* @param externalUsage - optional credential-backed quota readers.
|
|
465
|
+
* @param cursor - optional Cursor account and model-visibility actions.
|
|
466
|
+
* @param ollama - optional Ollama Cloud catalog and connection settings.
|
|
366
467
|
*/
|
|
367
|
-
export function registerAuthRpc(ctx, controller, speed, modelDefaults = undefined, providerSettings = undefined, externalUsage = undefined, cursor = undefined) {
|
|
468
|
+
export function registerAuthRpc(ctx, controller, speed, modelDefaults = undefined, providerSettings = undefined, externalUsage = undefined, cursor = undefined, ollama = undefined) {
|
|
368
469
|
// `connection` is not in this plugin's inject list (headless compositions
|
|
369
470
|
// lack it), so its startup order is unconstrained: defer registration until
|
|
370
471
|
// the service exists instead of probing once at apply time.
|
|
@@ -381,7 +482,7 @@ export function registerAuthRpc(ctx, controller, speed, modelDefaults = undefine
|
|
|
381
482
|
const connection = ctx.get('connection');
|
|
382
483
|
const handler = async (endpoint, payload, signal) => {
|
|
383
484
|
try {
|
|
384
|
-
return await dispatch(controller, speed, modelDefaults, endpoint, payload, signal, providerSettings, externalUsage, cursor);
|
|
485
|
+
return await dispatch(controller, speed, modelDefaults, endpoint, payload, signal, providerSettings, externalUsage, cursor, ollama);
|
|
385
486
|
}
|
|
386
487
|
catch (error) {
|
|
387
488
|
return failure(error);
|
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;
|