dsh-plugin-subscriptions 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +93 -0
  2. package/README.zh.md +93 -0
  3. package/cordis.patch.yml +12 -0
  4. package/lib/auth/jwt.d.ts +10 -0
  5. package/lib/auth/jwt.js +25 -0
  6. package/lib/auth/oauth-flow.d.ts +91 -0
  7. package/lib/auth/oauth-flow.js +227 -0
  8. package/lib/auth/pkce.d.ts +31 -0
  9. package/lib/auth/pkce.js +35 -0
  10. package/lib/auth/rpc.d.ts +51 -0
  11. package/lib/auth/rpc.js +83 -0
  12. package/lib/auth/store.d.ts +90 -0
  13. package/lib/auth/store.js +137 -0
  14. package/lib/client/SubscriptionsSection.d.ts +30 -0
  15. package/lib/client/SubscriptionsSection.js +290 -0
  16. package/lib/client/index.d.ts +31 -0
  17. package/lib/client/index.js +35 -0
  18. package/lib/client/locales.d.ts +45 -0
  19. package/lib/client/locales.js +43 -0
  20. package/lib/client.js +546 -0
  21. package/lib/client.js.map +1 -0
  22. package/lib/index.d.ts +34 -0
  23. package/lib/index.js +2932 -0
  24. package/lib/providers/claude.d.ts +60 -0
  25. package/lib/providers/claude.js +243 -0
  26. package/lib/providers/codex.d.ts +96 -0
  27. package/lib/providers/codex.js +391 -0
  28. package/lib/providers/common.d.ts +185 -0
  29. package/lib/providers/common.js +302 -0
  30. package/lib/providers/grok.d.ts +90 -0
  31. package/lib/providers/grok.js +337 -0
  32. package/lib/tools/image-generate.d.ts +60 -0
  33. package/lib/tools/image-generate.js +142 -0
  34. package/lib/tools/x-search.d.ts +58 -0
  35. package/lib/tools/x-search.js +195 -0
  36. package/lib/translate/anthropic.d.ts +120 -0
  37. package/lib/translate/anthropic.js +370 -0
  38. package/lib/translate/resolved.d.ts +35 -0
  39. package/lib/translate/resolved.js +40 -0
  40. package/lib/translate/responses.d.ts +127 -0
  41. package/lib/translate/responses.js +352 -0
  42. package/lib/translate/sse.d.ts +21 -0
  43. package/lib/translate/sse.js +56 -0
  44. package/package.json +83 -0
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Claude Pro/Max subscription provider: OAuth against claude.ai /
3
+ * platform.claude.com with the Claude Code client id, and streaming against
4
+ * the Anthropic Messages API with the Claude Code identity headers.
5
+ */
6
+ import { LlmAdapter } from '@deepseek-ai/dsh-llm';
7
+ import type { GenerateOptions, LlmModelInfo, LlmProviderInfo, LlmResolvedModelInfo, StreamChunk } from '@deepseek-ai/dsh-llm';
8
+ import type { FlowSpec } from '../auth/oauth-flow.js';
9
+ import type { ClaudeSession } from '../auth/store.js';
10
+ import type { AttachmentStore } from '@deepseek-ai/dsh-attachment';
11
+ import { TokenManager } from './common.js';
12
+ import type { ModelEntry } from './common.js';
13
+ export declare const CLAUDE_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
14
+ export declare const CLAUDE_AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
15
+ export declare const CLAUDE_TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
16
+ export declare const CLAUDE_API_URL = "https://api.anthropic.com/v1/messages?beta=true";
17
+ export declare const CLAUDE_PROFILE_URL = "https://api.anthropic.com/api/oauth/profile";
18
+ /** Refresh when the access token has less than this much life left. */
19
+ export declare const CLAUDE_PREEMPT_MS: number;
20
+ /** Static claude flow facts for the OAuth flow engine. */
21
+ export declare const claudeFlow: FlowSpec;
22
+ /**
23
+ * Exchange an authorization code for a claude session (JSON grant).
24
+ * @param code - the authorization code from the callback.
25
+ * @param verifier - the PKCE verifier minted for the attempt.
26
+ * @param redirectUri - the attempt's redirect URI.
27
+ * @param state - the attempt's state (echoed to the token endpoint).
28
+ * @returns the session to store.
29
+ */
30
+ export declare function exchangeClaudeCode(code: string, verifier: string, redirectUri: string, state: string): Promise<ClaudeSession>;
31
+ /**
32
+ * Refresh a claude session (JSON grant echoing the issued scope).
33
+ * @param session - the stored session.
34
+ * @returns the fresh session to store.
35
+ */
36
+ export declare function refreshClaude(session: ClaudeSession): Promise<ClaudeSession>;
37
+ /**
38
+ * Whether a claude refresh failure means the login is permanently gone.
39
+ * @param error - the thrown refresh error.
40
+ * @returns true when re-login is the only fix.
41
+ */
42
+ export declare function isClaudePermanentRefreshError(error: unknown): boolean;
43
+ /** Constructor dependencies for {@link ClaudeAdapter}. */
44
+ export interface ClaudeAdapterOptions {
45
+ models: readonly ModelEntry[];
46
+ streamIdleTimeoutMs: number;
47
+ tokens: TokenManager<ClaudeSession>;
48
+ /** Resolve the attachment service per request; absent means image requests fail loudly. */
49
+ resolveAttachments?: () => AttachmentStore | undefined;
50
+ }
51
+ /** Claude wire adapter: one instance serves the `claude` provider route. */
52
+ export declare class ClaudeAdapter extends LlmAdapter {
53
+ private readonly options;
54
+ constructor(options: ClaudeAdapterOptions);
55
+ providerInfo(provider: string): LlmProviderInfo;
56
+ listModels(provider: string): Promise<readonly LlmModelInfo[]>;
57
+ resolveModel(provider: string, model: string): Promise<LlmResolvedModelInfo>;
58
+ stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
59
+ private request;
60
+ }
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Claude Pro/Max subscription provider: OAuth against claude.ai /
3
+ * platform.claude.com with the Claude Code client id, and streaming against
4
+ * the Anthropic Messages API with the Claude Code identity headers.
5
+ */
6
+ import { EMPTY_RESPONSE_CODE, LlmAdapter, LlmError } from '@deepseek-ai/dsh-llm';
7
+ import { resolveImages } from '../translate/resolved.js';
8
+ import { streamAnthropic, toAnthropicMessages, toAnthropicSystem, toAnthropicTools, } from '../translate/anthropic.js';
9
+ import { httpLlmError, idleWatchdog, mapFetchFailure, oauthEndpointError, OAuthEndpointError, TokenManager, } from './common.js';
10
+ export const CLAUDE_CLIENT_ID = '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
11
+ export const CLAUDE_AUTHORIZE_URL = 'https://claude.ai/oauth/authorize';
12
+ export const CLAUDE_TOKEN_URL = 'https://platform.claude.com/v1/oauth/token';
13
+ export const CLAUDE_API_URL = 'https://api.anthropic.com/v1/messages?beta=true';
14
+ export const CLAUDE_PROFILE_URL = 'https://api.anthropic.com/api/oauth/profile';
15
+ const CLAUDE_SCOPE = 'org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload';
16
+ const CLAUDE_CALLBACK_PATH = '/callback';
17
+ const CLAUDE_CONTEXT_WINDOW = 200_000;
18
+ const CLAUDE_DEFAULT_MAX_TOKENS = 32_000;
19
+ /** Refresh when the access token has less than this much life left. */
20
+ export const CLAUDE_PREEMPT_MS = 5 * 60_000;
21
+ /**
22
+ * The subscription endpoint only serves requests presenting as Claude Code,
23
+ * so these headers impersonate the CLI; the harness attribution user-agent
24
+ * cannot be sent here (one user-agent slot, and the CLI's wins).
25
+ */
26
+ const CLAUDE_CLI_USER_AGENT = 'claude-cli/2.1.97 (external, cli)';
27
+ const CLAUDE_BETA_FLAGS = 'claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27';
28
+ /** Static claude flow facts for the OAuth flow engine. */
29
+ export const claudeFlow = {
30
+ callbackPath: CLAUDE_CALLBACK_PATH,
31
+ // The redirect URI embeds the port, so it must be an ephemeral one.
32
+ listen: { host: 'localhost', ports: [0] },
33
+ buildAuthorizeUrl({ redirectUri, state, pkce }) {
34
+ const params = new URLSearchParams({
35
+ code: 'true',
36
+ client_id: CLAUDE_CLIENT_ID,
37
+ response_type: 'code',
38
+ redirect_uri: redirectUri,
39
+ scope: CLAUDE_SCOPE,
40
+ code_challenge: pkce.challenge,
41
+ code_challenge_method: 'S256',
42
+ state,
43
+ });
44
+ return `${CLAUDE_AUTHORIZE_URL}?${params.toString()}`;
45
+ },
46
+ };
47
+ /** Best-effort account profile; login must not fail when this does. */
48
+ async function fetchClaudeProfile(accessToken) {
49
+ try {
50
+ const response = await fetch(CLAUDE_PROFILE_URL, {
51
+ headers: { authorization: `Bearer ${accessToken}` },
52
+ });
53
+ if (!response.ok)
54
+ return {};
55
+ const profile = await response.json();
56
+ const account = typeof profile.account === 'object' && profile.account !== null
57
+ ? profile.account
58
+ : {};
59
+ const email = profile.emailAddress ?? profile.email ?? account.email_address ?? account.email;
60
+ const subscription = profile.subscriptionType ?? profile.subscription_type ?? account.subscription_type;
61
+ return {
62
+ ...typeof email === 'string' && email.length > 0 ? { emailAddress: email } : {},
63
+ ...typeof subscription === 'string' && subscription.length > 0 ? { subscriptionType: subscription } : {},
64
+ };
65
+ }
66
+ catch {
67
+ // Profile lookup is decorative; only the token exchange owns login success.
68
+ return {};
69
+ }
70
+ }
71
+ /** Build a session from a token response. */
72
+ async function claudeSession(tokens, fallbackRefreshToken, withProfile) {
73
+ if (typeof tokens.access_token !== 'string' || tokens.access_token.length === 0) {
74
+ throw new Error('claude token endpoint returned no access token');
75
+ }
76
+ const refreshToken = tokens.refresh_token ?? fallbackRefreshToken;
77
+ if (refreshToken === undefined)
78
+ throw new Error('claude token endpoint returned no refresh token');
79
+ if (typeof tokens.expires_in !== 'number' || tokens.expires_in <= 0) {
80
+ throw new Error('claude token endpoint returned no usable expiry');
81
+ }
82
+ const profile = withProfile ? await fetchClaudeProfile(tokens.access_token) : {};
83
+ return {
84
+ accessToken: tokens.access_token,
85
+ refreshToken,
86
+ expiresAt: Date.now() + tokens.expires_in * 1000,
87
+ scopes: tokens.scope ?? CLAUDE_SCOPE,
88
+ ...profile,
89
+ };
90
+ }
91
+ /**
92
+ * Exchange an authorization code for a claude session (JSON grant).
93
+ * @param code - the authorization code from the callback.
94
+ * @param verifier - the PKCE verifier minted for the attempt.
95
+ * @param redirectUri - the attempt's redirect URI.
96
+ * @param state - the attempt's state (echoed to the token endpoint).
97
+ * @returns the session to store.
98
+ */
99
+ export async function exchangeClaudeCode(code, verifier, redirectUri, state) {
100
+ const response = await fetch(CLAUDE_TOKEN_URL, {
101
+ method: 'POST',
102
+ headers: { 'content-type': 'application/json' },
103
+ body: JSON.stringify({
104
+ grant_type: 'authorization_code',
105
+ code,
106
+ redirect_uri: redirectUri,
107
+ client_id: CLAUDE_CLIENT_ID,
108
+ code_verifier: verifier,
109
+ state,
110
+ }),
111
+ });
112
+ if (!response.ok)
113
+ throw await oauthEndpointError(response, 'claude');
114
+ return claudeSession(await response.json(), undefined, true);
115
+ }
116
+ /**
117
+ * Refresh a claude session (JSON grant echoing the issued scope).
118
+ * @param session - the stored session.
119
+ * @returns the fresh session to store.
120
+ */
121
+ export async function refreshClaude(session) {
122
+ const response = await fetch(CLAUDE_TOKEN_URL, {
123
+ method: 'POST',
124
+ headers: { 'content-type': 'application/json' },
125
+ body: JSON.stringify({
126
+ grant_type: 'refresh_token',
127
+ refresh_token: session.refreshToken,
128
+ client_id: CLAUDE_CLIENT_ID,
129
+ scope: session.scopes,
130
+ }),
131
+ });
132
+ if (!response.ok)
133
+ throw await oauthEndpointError(response, 'claude');
134
+ const next = await claudeSession(await response.json(), session.refreshToken, false);
135
+ return {
136
+ ...next,
137
+ ...session.emailAddress === undefined ? {} : { emailAddress: session.emailAddress },
138
+ ...session.subscriptionType === undefined ? {} : { subscriptionType: session.subscriptionType },
139
+ };
140
+ }
141
+ /**
142
+ * Whether a claude refresh failure means the login is permanently gone.
143
+ * @param error - the thrown refresh error.
144
+ * @returns true when re-login is the only fix.
145
+ */
146
+ export function isClaudePermanentRefreshError(error) {
147
+ return error instanceof OAuthEndpointError
148
+ && (error.oauthCode === 'invalid_grant' || error.oauthCode === 'invalid_token');
149
+ }
150
+ /** The Claude 4.5 family accepts image input. */
151
+ const CLAUDE_MODALITIES = ['text', 'image'];
152
+ /** Claude wire adapter: one instance serves the `claude` provider route. */
153
+ export class ClaudeAdapter extends LlmAdapter {
154
+ options;
155
+ constructor(options) {
156
+ super();
157
+ this.options = options;
158
+ }
159
+ providerInfo(provider) {
160
+ return { id: provider, name: 'Claude (Subscription)' };
161
+ }
162
+ async listModels(provider) {
163
+ // Not logged in → empty catalog, so the web picker drops the provider.
164
+ // Claude has no subscription model-list endpoint, so the static catalog
165
+ // is the whole answer when logged in.
166
+ if (!await this.options.tokens.hasSession())
167
+ return [];
168
+ return this.options.models.map(model => ({
169
+ provider,
170
+ id: model.id,
171
+ name: model.name ?? model.id,
172
+ inputModalities: model.inputModalities ?? CLAUDE_MODALITIES,
173
+ }));
174
+ }
175
+ resolveModel(provider, model) {
176
+ const configured = this.options.models.find(entry => entry.id === model);
177
+ return Promise.resolve({
178
+ provider,
179
+ id: model,
180
+ name: configured?.name ?? model,
181
+ inputModalities: configured?.inputModalities ?? CLAUDE_MODALITIES,
182
+ context: { contextWindow: configured?.contextWindow ?? CLAUDE_CONTEXT_WINDOW },
183
+ defaultMaxTokens: configured?.maxTokens ?? CLAUDE_DEFAULT_MAX_TOKENS,
184
+ // No reasoning metadata: the subscription endpoint's thinking support is
185
+ // not exercised, so effort requests reject as unsupported.
186
+ });
187
+ }
188
+ async *stream(options) {
189
+ const watchdog = idleWatchdog(options.signal, this.options.streamIdleTimeoutMs);
190
+ try {
191
+ let session = await this.options.tokens.session();
192
+ let response = await this.request(options, session, watchdog.signal);
193
+ if (response.status === 401) {
194
+ // One forced refresh + retry on an unexpired-but-rejected token.
195
+ session = await this.options.tokens.session(true);
196
+ response = await this.request(options, session, watchdog.signal);
197
+ }
198
+ if (!response.ok)
199
+ throw await httpLlmError(response, 'claude API');
200
+ if (response.body === null) {
201
+ throw new LlmError('claude API returned no response body', EMPTY_RESPONSE_CODE);
202
+ }
203
+ yield* streamAnthropic(response.body, () => { watchdog.pulse(); });
204
+ }
205
+ catch (error) {
206
+ throw mapFetchFailure('claude API', error, watchdog, options.signal);
207
+ }
208
+ finally {
209
+ watchdog.stop();
210
+ }
211
+ }
212
+ async request(options, session, signal) {
213
+ const messages = await resolveImages(options.messages, this.options.resolveAttachments?.(), signal);
214
+ const body = {
215
+ model: options.model,
216
+ max_tokens: options.maxTokens
217
+ ?? this.options.models.find(entry => entry.id === options.model)?.maxTokens
218
+ ?? CLAUDE_DEFAULT_MAX_TOKENS,
219
+ system: toAnthropicSystem(options.system, messages),
220
+ messages: toAnthropicMessages(messages),
221
+ ...options.tools !== undefined && options.tools.length > 0
222
+ ? { tools: toAnthropicTools(options.tools) }
223
+ : {},
224
+ stream: true,
225
+ ...options.sessionId !== undefined ? { metadata: { user_id: String(options.sessionId) } } : {},
226
+ };
227
+ return fetch(CLAUDE_API_URL, {
228
+ method: 'POST',
229
+ headers: {
230
+ 'authorization': `Bearer ${session.accessToken}`,
231
+ 'anthropic-version': '2023-06-01',
232
+ 'anthropic-beta': CLAUDE_BETA_FLAGS,
233
+ 'user-agent': CLAUDE_CLI_USER_AGENT,
234
+ 'x-app': 'cli',
235
+ 'anthropic-dangerous-direct-browser-access': 'true',
236
+ 'accept': 'text/event-stream',
237
+ 'content-type': 'application/json',
238
+ },
239
+ body: JSON.stringify(body),
240
+ signal,
241
+ });
242
+ }
243
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * ChatGPT/Codex subscription provider: OAuth against auth.openai.com with the
3
+ * Codex CLI client id, and streaming against the ChatGPT backend Responses
4
+ * endpoint.
5
+ */
6
+ import { LlmAdapter } from '@deepseek-ai/dsh-llm';
7
+ import type { GenerateOptions, LlmModelInfo, LlmProviderInfo, LlmResolvedModelInfo, StreamChunk } from '@deepseek-ai/dsh-llm';
8
+ import type { FlowSpec } from '../auth/oauth-flow.js';
9
+ import type { CodexSession } from '../auth/store.js';
10
+ import type { AttachmentStore } from '@deepseek-ai/dsh-attachment';
11
+ import { TokenManager } from './common.js';
12
+ import type { DiscoveredModel, FetchFn, ModelEntry } from './common.js';
13
+ export declare const CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
14
+ export declare const CODEX_AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize";
15
+ export declare const CODEX_TOKEN_URL = "https://auth.openai.com/oauth/token";
16
+ export declare const CODEX_API_URL = "https://chatgpt.com/backend-api/codex/responses";
17
+ /** Refresh when the access token has less than this much life left. */
18
+ export declare const CODEX_PREEMPT_MS: number;
19
+ /** Static codex flow facts for the OAuth flow engine. */
20
+ export declare const codexFlow: FlowSpec;
21
+ /** User identity claims decoded from a codex id token. */
22
+ export interface CodexProfileClaims {
23
+ emailAddress?: string;
24
+ planType?: string;
25
+ }
26
+ /**
27
+ * Decode the user-identity claims of a codex id token (pure, cheap — no
28
+ * verification, same trust posture as {@link accountIdOf}). Claim paths
29
+ * mirror codex-rs `login/src/token_data.rs`: the email is the top-level
30
+ * `email` claim, falling back to `https://api.openai.com/profile`.email; the
31
+ * plan is `https://api.openai.com/auth`.chatgpt_plan_type.
32
+ * @param idToken - a stored or freshly issued id token, when present.
33
+ * @returns whichever claims the token carried; empty when undecodable.
34
+ */
35
+ export declare function codexProfileClaims(idToken: string | undefined): CodexProfileClaims;
36
+ /**
37
+ * Exchange an authorization code for a codex session (form-encoded grant).
38
+ * @param code - the authorization code from the callback.
39
+ * @param verifier - the PKCE verifier minted for the attempt.
40
+ * @param redirectUri - the attempt's redirect URI.
41
+ * @returns the session to store.
42
+ */
43
+ export declare function exchangeCodexCode(code: string, verifier: string, redirectUri: string): Promise<CodexSession>;
44
+ /**
45
+ * Refresh a codex session (JSON grant — unlike the code exchange).
46
+ * @param session - the stored session.
47
+ * @returns the fresh session to store.
48
+ */
49
+ export declare function refreshCodex(session: CodexSession): Promise<CodexSession>;
50
+ /**
51
+ * Whether a codex refresh failure means the login is permanently gone.
52
+ * @param error - the thrown refresh error.
53
+ * @returns true when re-login is the only fix.
54
+ */
55
+ export declare function isCodexPermanentRefreshError(error: unknown): boolean;
56
+ export declare const CODEX_MODELS_URL = "https://chatgpt.com/backend-api/codex/models";
57
+ /**
58
+ * Client version sent on the /models catalog request. The backend gates the
59
+ * visible model list by client version: versions below ~0.101 get an empty
60
+ * list, while current codex CLI releases get the full catalog — keep this in
61
+ * the range of current codex CLI releases.
62
+ */
63
+ export declare const CODEX_CLIENT_VERSION = "0.147.0";
64
+ /**
65
+ * Fetch the live codex model catalog with the session's auth headers.
66
+ * @param session - the stored session (used as-is; never refreshed here).
67
+ * @param fetchFn - fetch implementation (injectable for tests).
68
+ * @returns discovered models: hidden entries dropped, sorted by priority.
69
+ */
70
+ export declare function fetchCodexModels(session: CodexSession, fetchFn?: FetchFn): Promise<DiscoveredModel[]>;
71
+ /** Constructor dependencies for {@link CodexAdapter}. */
72
+ export interface CodexAdapterOptions {
73
+ models: readonly ModelEntry[];
74
+ streamIdleTimeoutMs: number;
75
+ tokens: TokenManager<CodexSession>;
76
+ /** Whether to fetch the live catalog when logged in (false when config `models` overrides). */
77
+ discovery: boolean;
78
+ /** Warning sink for discovery failures that fall back to the static catalog. */
79
+ onWarn?: (message: string) => void;
80
+ /** Fetch implementation for discovery (defaults to global fetch). */
81
+ fetchFn?: FetchFn;
82
+ /** Resolve the attachment service per request; absent means image requests fail loudly. */
83
+ resolveAttachments?: () => AttachmentStore | undefined;
84
+ }
85
+ /** Codex wire adapter: one instance serves the `codex` provider route. */
86
+ export declare class CodexAdapter extends LlmAdapter {
87
+ private readonly options;
88
+ private readonly catalog;
89
+ constructor(options: CodexAdapterOptions);
90
+ providerInfo(provider: string): LlmProviderInfo;
91
+ private staticModels;
92
+ listModels(provider: string): Promise<readonly LlmModelInfo[]>;
93
+ resolveModel(provider: string, model: string): Promise<LlmResolvedModelInfo>;
94
+ stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
95
+ private request;
96
+ }