dsh-plugin-subscriptions 0.4.0 → 0.4.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 +14 -6
- package/README.zh.md +14 -6
- package/lib/auth/claude-code-creds.d.ts +23 -0
- package/lib/auth/claude-code-creds.js +167 -0
- package/lib/auth/pkce.js +1 -1
- package/lib/client/SubscriptionsSection.js +6 -1
- package/lib/client.js +5 -1
- package/lib/client.js.map +1 -1
- package/lib/index.js +498 -77
- package/lib/providers/catalog-store.js +4 -0
- package/lib/providers/claude.d.ts +35 -3
- package/lib/providers/claude.js +181 -27
- package/lib/providers/common.d.ts +2 -0
- package/lib/tools/image-generate.d.ts +55 -19
- package/lib/tools/image-generate.js +130 -31
- package/package.json +6 -8
package/README.md
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
English | [中文](README.zh.md)
|
|
4
4
|
|
|
5
|
-
Use your **ChatGPT (Codex)**, **Claude**, and **Grok (X Premium)** subscriptions as LLM providers in [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) — no API keys.
|
|
5
|
+
Use your **ChatGPT (Codex)**, **Claude**, and **Grok (X Premium)** subscriptions as LLM providers in [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) — no API keys. Codex and Grok log in via OAuth in the dsh web UI (Settings → Subscriptions); Claude imports credentials directly from an existing Claude Code session (macOS Keychain or `~/.claude/.credentials.json`). Tokens live at `~/.dsh/plugins/subscriptions/auth.json` (mode 0600) and refresh automatically.
|
|
6
6
|
|
|
7
7
|
## Demo
|
|
8
8
|
|
|
9
|
-
Settings → **Subscriptions**: per-provider
|
|
9
|
+
Settings → **Subscriptions**: per-provider login/logout, no API keys. Claude imports credentials from Claude Code; Codex and Grok use OAuth (account address masked in the screenshot):
|
|
10
10
|
|
|
11
11
|

|
|
12
12
|
|
|
@@ -22,12 +22,20 @@ The `image_generate` tool renders its result inline in the conversation:
|
|
|
22
22
|
|
|
23
23
|

|
|
24
24
|
|
|
25
|
+
Its `provider` parameter picks the image backend — the same prompt through GPT (`gpt-image-2`, top) and Grok (`grok-imagine-image-2.0`, bottom):
|
|
26
|
+
|
|
27
|
+

|
|
28
|
+
|
|
29
|
+
The `video_generate` tool plays the generated clip inline:
|
|
30
|
+
|
|
31
|
+

|
|
32
|
+
|
|
25
33
|
## Providers
|
|
26
34
|
|
|
27
35
|
| Route | Subscription | Models |
|
|
28
36
|
|----------|-------------------|--------|
|
|
29
37
|
| `codex` | ChatGPT Plus/Pro | live catalog from `chatgpt.com/backend-api/codex/models` |
|
|
30
|
-
| `claude` | Claude Pro/Max |
|
|
38
|
+
| `claude` | Claude Pro/Max | all models available in your subscription (Opus, Sonnet, Haiku, Fable — static catalog, updated with the plugin) |
|
|
31
39
|
| `grok` | X Premium (xAI) | live catalog from `api.x.ai/v1/models` (chat models only); reasoning efforts from the Grok CLI catalog (`cli-chat-proxy.grok.com/v1/models`) |
|
|
32
40
|
|
|
33
41
|
Only logged-in providers appear in the session model picker; the lists above refresh on login/logout. Vision-capable models declare `['text', 'image']` input modalities, and image content is translated to each provider's wire format.
|
|
@@ -37,7 +45,7 @@ Logged-in cards also show **subscription usage** — per rate-limit window (5-ho
|
|
|
37
45
|
Also included, registered when the matching provider is enabled:
|
|
38
46
|
|
|
39
47
|
- **`x_search`** tool (Grok) — xAI's hosted X search, returning `{ answer, citations }`.
|
|
40
|
-
- **`image_generate`** tool (ChatGPT) — `gpt-image-2` via the Codex backend;
|
|
48
|
+
- **`image_generate`** tool (ChatGPT or Grok) — `gpt-image-2` via the Codex backend, or `grok-imagine-image-2.0` via `api.x.ai/v1/images/generations`. The `provider` argument picks the preferred provider (`gpt`, the default, or `grok`); when the preferred one is logged out the other serves as fallback. Images are saved under `~/.dsh/plugins/subscriptions/images/` and the paths returned. The `size`/`quality` arguments map onto Grok's `aspect_ratio`/`quality` on the Grok path.
|
|
41
49
|
- **`video_generate`** tool (Grok) — `grok-imagine-video-1.5` via `api.x.ai/v1/videos` (async submit + poll); MP4s are saved under `~/.dsh/plugins/subscriptions/videos/`, the path returned, and the clip plays inline in the conversation. Supports duration (1–15 s), aspect ratio, resolution, and image-to-video via `image_url`.
|
|
42
50
|
|
|
43
51
|
## Install
|
|
@@ -93,7 +101,7 @@ Either way, restart `dsh web` afterwards so the new version loads.
|
|
|
93
101
|
## Use
|
|
94
102
|
|
|
95
103
|
1. `dsh web`, open the printed URL.
|
|
96
|
-
2. Settings → **Subscriptions**: click **
|
|
104
|
+
2. Settings → **Subscriptions**: click **Connect** on a provider. For Claude, credentials are imported instantly from Claude Code (you must have run `claude` and logged in at least once). For Codex and Grok, authorize in the opened browser tab; if the browser flow can't complete (headless host), expand the manual fallback and paste the callback URL or code.
|
|
97
105
|
3. In any session, open the model picker (`/model`) and choose a model under **ChatGPT (Codex)** / **Claude (Subscription)** / **Grok (Subscription)**.
|
|
98
106
|
|
|
99
107
|
Not logged in? The provider stays out of the picker, and requests fail with `MISSING_CREDENTIAL` pointing at the Settings page; nothing else breaks.
|
|
@@ -126,7 +134,7 @@ After `pnpm build`, restart `dsh web` to pick up changes.
|
|
|
126
134
|
## Layout
|
|
127
135
|
|
|
128
136
|
- `src/index.ts` — plugin entry: config schema, adapter registration, auth-change re-announce, RPC wiring
|
|
129
|
-
- `src/auth/` — PKCE/JWT helpers, token store, OAuth flow engine (temp loopback callback server), `/subscriptions-auth` RPC channel
|
|
137
|
+
- `src/auth/` — PKCE/JWT helpers, token store, OAuth flow engine (temp loopback callback server), Claude Code credential reader (Keychain/file), `/subscriptions-auth` RPC channel
|
|
130
138
|
- `src/providers/` — per-provider OAuth constants/exchange/refresh + `LlmAdapter`s
|
|
131
139
|
- `src/translate/` — dsh `Message[]` ⟷ OpenAI Responses / Anthropic Messages wire formats, SSE → `StreamChunk`
|
|
132
140
|
- `src/tools/` — `x_search`, `image_generate`, and `video_generate`
|
package/README.zh.md
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) | 中文
|
|
4
4
|
|
|
5
|
-
把你的 **ChatGPT(Codex)**、**Claude**、**Grok(X Premium)** 订阅当作 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 的 LLM provider 使用 —— 不需要 API key
|
|
5
|
+
把你的 **ChatGPT(Codex)**、**Claude**、**Grok(X Premium)** 订阅当作 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 的 LLM provider 使用 —— 不需要 API key。Codex 和 Grok 通过 dsh web 界面 OAuth 登录(设置 → 订阅);Claude 直接从已有的 Claude Code 会话导入凭据(macOS Keychain 或 `~/.claude/.credentials.json`)。Token 保存在 `~/.dsh/plugins/subscriptions/auth.json`(权限 0600),过期自动刷新。
|
|
6
6
|
|
|
7
7
|
## 演示
|
|
8
8
|
|
|
9
|
-
设置 → **订阅**:每个 provider
|
|
9
|
+
设置 → **订阅**:每个 provider 的登录/退出,无需 API key。Claude 从 Claude Code 导入凭据;Codex 和 Grok 使用 OAuth(截图中账号已打码):
|
|
10
10
|
|
|
11
11
|

|
|
12
12
|
|
|
@@ -22,12 +22,20 @@
|
|
|
22
22
|
|
|
23
23
|

|
|
24
24
|
|
|
25
|
+
`provider` 参数可选择生图后端——同一条提示词分别走 GPT(`gpt-image-2`,上)和 Grok(`grok-imagine-image-2.0`,下):
|
|
26
|
+
|
|
27
|
+

|
|
28
|
+
|
|
29
|
+
`video_generate` 工具生成的视频直接内联播放:
|
|
30
|
+
|
|
31
|
+

|
|
32
|
+
|
|
25
33
|
## Provider 一览
|
|
26
34
|
|
|
27
35
|
| 路由 | 订阅 | 模型 |
|
|
28
36
|
|----------|------------------|------|
|
|
29
37
|
| `codex` | ChatGPT Plus/Pro | 从 `chatgpt.com/backend-api/codex/models` 实时获取 |
|
|
30
|
-
| `claude` | Claude Pro/Max |
|
|
38
|
+
| `claude` | Claude Pro/Max | 订阅内所有可用模型(Opus、Sonnet、Haiku、Fable —— 静态目录,随插件更新) |
|
|
31
39
|
| `grok` | X Premium (xAI) | 从 `api.x.ai/v1/models` 实时获取(仅对话模型);推理等级来自 Grok CLI 目录(`cli-chat-proxy.grok.com/v1/models`) |
|
|
32
40
|
|
|
33
41
|
只有已登录的 provider 才会出现在会话模型选择器里;登录/退出后列表自动刷新。支持视觉的模型会声明 `['text', 'image']` 输入模态,图片内容会被翻译成各 provider 的 wire 格式。
|
|
@@ -37,7 +45,7 @@
|
|
|
37
45
|
随 provider 启用自动注册的工具:
|
|
38
46
|
|
|
39
47
|
- **`x_search`**(Grok)—— xAI 托管的 X 搜索,返回 `{ answer, citations }`。
|
|
40
|
-
- **`image_generate`**(ChatGPT)—— 经 Codex 后端调用 `gpt-image-2
|
|
48
|
+
- **`image_generate`**(ChatGPT 或 Grok)—— 经 Codex 后端调用 `gpt-image-2`,或经 `api.x.ai/v1/images/generations` 调用 `grok-imagine-image-2.0`。`provider` 参数指定首选提供方(`gpt` 为默认值,可选 `grok`);首选方未登录时自动回退到另一方。图片保存到 `~/.dsh/plugins/subscriptions/images/` 并返回路径。Grok 路径上 `size`/`quality` 参数会映射为 Grok 的 `aspect_ratio`/`quality`。
|
|
41
49
|
- **`video_generate`**(Grok)—— 经 `api.x.ai/v1/videos` 调用 `grok-imagine-video-1.5`(异步提交 + 轮询);MP4 保存到 `~/.dsh/plugins/subscriptions/videos/` 并返回路径,视频直接在对话里内联播放。支持时长(1–15 秒)、宽高比、分辨率,以及通过 `image_url` 做图生视频。
|
|
42
50
|
|
|
43
51
|
## 安装
|
|
@@ -93,7 +101,7 @@ GitHub 安装的:重新执行一遍 `add github:V1ki/dsh-plugin-subscriptions`
|
|
|
93
101
|
## 使用
|
|
94
102
|
|
|
95
103
|
1. `dsh web`,打开打印的 URL。
|
|
96
|
-
2. **设置 → 订阅**:点对应 provider
|
|
104
|
+
2. **设置 → 订阅**:点对应 provider 的「连接」。Claude 会即时从 Claude Code 导入凭据(需先运行过 `claude` 并登录)。Codex 和 Grok 在打开的标签页里授权;无浏览器环境下可展开手动兜底,粘贴回调 URL 或授权码。
|
|
97
105
|
3. 在任意会话里打开模型选择器(`/model`),选择 **ChatGPT (Codex)** / **Claude (Subscription)** / **Grok (Subscription)** 下的模型。
|
|
98
106
|
|
|
99
107
|
未登录时:该 provider 不出现在选择器里;直接请求会报 `MISSING_CREDENTIAL` 并提示去设置页登录,不影响其他功能。
|
|
@@ -126,7 +134,7 @@ pnpm test # 编译后跑 node --test 单测
|
|
|
126
134
|
## 目录结构
|
|
127
135
|
|
|
128
136
|
- `src/index.ts` —— 插件入口:配置 schema、adapter 注册、登录态变更通告、RPC 接线
|
|
129
|
-
- `src/auth/` —— PKCE/JWT 工具、token 存储、OAuth 流程引擎(临时本地回调服务)、`/subscriptions-auth` RPC 通道
|
|
137
|
+
- `src/auth/` —— PKCE/JWT 工具、token 存储、OAuth 流程引擎(临时本地回调服务)、Claude Code 凭据读取器(Keychain/文件)、`/subscriptions-auth` RPC 通道
|
|
130
138
|
- `src/providers/` —— 各 provider 的 OAuth 常量/换发/刷新 + `LlmAdapter` 实现
|
|
131
139
|
- `src/translate/` —— dsh `Message[]` 与 OpenAI Responses / Anthropic Messages 格式互转,SSE → `StreamChunk`
|
|
132
140
|
- `src/tools/` —— `x_search`、`image_generate` 与 `video_generate`
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ClaudeSession } from './store.js';
|
|
2
|
+
/** Read the current Claude Code session from its source of truth: macOS Keychain, falling back to the credentials file. */
|
|
3
|
+
export declare function readClaudeCodeCredentials(): ClaudeSession | undefined;
|
|
4
|
+
/**
|
|
5
|
+
* Write a refreshed session back to Claude Code's own credential store, so
|
|
6
|
+
* the `claude` CLI and any other consumer of the same account see the token
|
|
7
|
+
* we just rotated. A stale-blob mismatch (something else rotated it first)
|
|
8
|
+
* is a no-op — the caller already has that other rotation via readClaudeCodeCredentials.
|
|
9
|
+
* @param next - the freshly refreshed session to persist.
|
|
10
|
+
* @param expectedPriorAccessToken - the access token this refresh started from.
|
|
11
|
+
* @returns whether the write-back succeeded.
|
|
12
|
+
*/
|
|
13
|
+
export declare function writeBackClaudeCodeCredentials(next: ClaudeSession, expectedPriorAccessToken: string): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Refresh a Claude session, first checking whether Claude Code's own store
|
|
16
|
+
* already holds a fresher token (rotated by the `claude` CLI or another
|
|
17
|
+
* consumer) before hitting the OAuth endpoint ourselves — and writing our own
|
|
18
|
+
* refresh back to that store so every consumer of the account stays synced.
|
|
19
|
+
* @param session - the session TokenManager wants refreshed.
|
|
20
|
+
* @param doRefresh - the actual OAuth refresh-token grant (network call).
|
|
21
|
+
* @returns the freshest available session.
|
|
22
|
+
*/
|
|
23
|
+
export declare function refreshClaudeSynced(session: ClaudeSession, doRefresh: (session: ClaudeSession) => Promise<ClaudeSession>): Promise<ClaudeSession>;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
const PRIMARY_SERVICE = 'Claude Code-credentials';
|
|
6
|
+
const DEFAULT_SCOPES = 'user:profile user:inference user:sessions:claude_code user:mcp_servers';
|
|
7
|
+
function toSession(data) {
|
|
8
|
+
if (typeof data.accessToken !== 'string' || typeof data.refreshToken !== 'string' || typeof data.expiresAt !== 'number') {
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
const scopes = Array.isArray(data.scopes) ? data.scopes.join(' ') : typeof data.scopes === 'string' ? data.scopes : DEFAULT_SCOPES;
|
|
12
|
+
return {
|
|
13
|
+
accessToken: data.accessToken,
|
|
14
|
+
refreshToken: data.refreshToken,
|
|
15
|
+
expiresAt: Math.trunc(data.expiresAt),
|
|
16
|
+
scopes,
|
|
17
|
+
...typeof data.emailAddress === 'string' ? { emailAddress: data.emailAddress } : {},
|
|
18
|
+
...typeof data.subscriptionType === 'string' ? { subscriptionType: data.subscriptionType } : {},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function parseBlob(raw) {
|
|
22
|
+
let parsed;
|
|
23
|
+
try {
|
|
24
|
+
parsed = JSON.parse(raw);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
return toSession(parsed.claudeAiOauth ?? parsed);
|
|
30
|
+
}
|
|
31
|
+
function credentialsFilePath() {
|
|
32
|
+
return join(process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), '.claude'), '.credentials.json');
|
|
33
|
+
}
|
|
34
|
+
function readKeychainRaw() {
|
|
35
|
+
try {
|
|
36
|
+
return execFileSync('/usr/bin/security', ['find-generic-password', '-s', PRIMARY_SERVICE, '-w'], {
|
|
37
|
+
timeout: 3000,
|
|
38
|
+
encoding: 'utf8',
|
|
39
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
40
|
+
}).trim();
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function readFileRaw() {
|
|
47
|
+
try {
|
|
48
|
+
return readFileSync(credentialsFilePath(), 'utf8');
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** Read the current Claude Code session from its source of truth: macOS Keychain, falling back to the credentials file. */
|
|
55
|
+
export function readClaudeCodeCredentials() {
|
|
56
|
+
if (process.platform === 'darwin') {
|
|
57
|
+
const raw = readKeychainRaw();
|
|
58
|
+
const session = raw !== undefined ? parseBlob(raw) : undefined;
|
|
59
|
+
if (session)
|
|
60
|
+
return session;
|
|
61
|
+
}
|
|
62
|
+
const raw = readFileRaw();
|
|
63
|
+
return raw !== undefined ? parseBlob(raw) : undefined;
|
|
64
|
+
}
|
|
65
|
+
function blobMatches(raw, expectedAccessToken) {
|
|
66
|
+
return parseBlob(raw)?.accessToken === expectedAccessToken;
|
|
67
|
+
}
|
|
68
|
+
function getKeychainAccountName() {
|
|
69
|
+
try {
|
|
70
|
+
const output = execFileSync('/usr/bin/security', ['find-generic-password', '-s', PRIMARY_SERVICE], {
|
|
71
|
+
timeout: 2000,
|
|
72
|
+
encoding: 'utf8',
|
|
73
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
74
|
+
});
|
|
75
|
+
return /"acct"<blob>="([^"]*)"/.exec(output)?.[1];
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** Merge fresh tokens into an existing raw blob, preserving unrelated fields. */
|
|
82
|
+
function mergeIntoBlob(existingRaw, next) {
|
|
83
|
+
let parsed;
|
|
84
|
+
try {
|
|
85
|
+
parsed = JSON.parse(existingRaw);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
const target = (parsed.claudeAiOauth ?? parsed);
|
|
91
|
+
target.accessToken = next.accessToken;
|
|
92
|
+
target.refreshToken = next.refreshToken;
|
|
93
|
+
target.expiresAt = next.expiresAt;
|
|
94
|
+
return JSON.stringify(parsed);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Write a refreshed session back to Claude Code's own credential store, so
|
|
98
|
+
* the `claude` CLI and any other consumer of the same account see the token
|
|
99
|
+
* we just rotated. A stale-blob mismatch (something else rotated it first)
|
|
100
|
+
* is a no-op — the caller already has that other rotation via readClaudeCodeCredentials.
|
|
101
|
+
* @param next - the freshly refreshed session to persist.
|
|
102
|
+
* @param expectedPriorAccessToken - the access token this refresh started from.
|
|
103
|
+
* @returns whether the write-back succeeded.
|
|
104
|
+
*/
|
|
105
|
+
export function writeBackClaudeCodeCredentials(next, expectedPriorAccessToken) {
|
|
106
|
+
if (process.platform === 'darwin') {
|
|
107
|
+
const raw = readKeychainRaw();
|
|
108
|
+
if (raw === undefined || !blobMatches(raw, expectedPriorAccessToken))
|
|
109
|
+
return false;
|
|
110
|
+
const updated = mergeIntoBlob(raw, next);
|
|
111
|
+
if (updated === undefined)
|
|
112
|
+
return false;
|
|
113
|
+
const account = getKeychainAccountName() ?? PRIMARY_SERVICE;
|
|
114
|
+
try {
|
|
115
|
+
execFileSync('/usr/bin/security', ['add-generic-password', '-s', PRIMARY_SERVICE, '-a', account, '-w', updated, '-U'], {
|
|
116
|
+
timeout: 2000,
|
|
117
|
+
stdio: 'ignore',
|
|
118
|
+
});
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const path = credentialsFilePath();
|
|
126
|
+
let raw;
|
|
127
|
+
try {
|
|
128
|
+
raw = readFileSync(path, 'utf8');
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
if (!blobMatches(raw, expectedPriorAccessToken))
|
|
134
|
+
return false;
|
|
135
|
+
const updated = mergeIntoBlob(raw, next);
|
|
136
|
+
if (updated === undefined)
|
|
137
|
+
return false;
|
|
138
|
+
try {
|
|
139
|
+
const dir = dirname(path);
|
|
140
|
+
if (!existsSync(dir))
|
|
141
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
142
|
+
writeFileSync(path, updated, { encoding: 'utf8', mode: 0o600 });
|
|
143
|
+
chmodSync(path, 0o600);
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Refresh a Claude session, first checking whether Claude Code's own store
|
|
152
|
+
* already holds a fresher token (rotated by the `claude` CLI or another
|
|
153
|
+
* consumer) before hitting the OAuth endpoint ourselves — and writing our own
|
|
154
|
+
* refresh back to that store so every consumer of the account stays synced.
|
|
155
|
+
* @param session - the session TokenManager wants refreshed.
|
|
156
|
+
* @param doRefresh - the actual OAuth refresh-token grant (network call).
|
|
157
|
+
* @returns the freshest available session.
|
|
158
|
+
*/
|
|
159
|
+
export async function refreshClaudeSynced(session, doRefresh) {
|
|
160
|
+
const fromSource = readClaudeCodeCredentials();
|
|
161
|
+
const base = fromSource !== undefined && fromSource.accessToken !== session.accessToken ? fromSource : session;
|
|
162
|
+
if (base.expiresAt > Date.now() + 60_000)
|
|
163
|
+
return base;
|
|
164
|
+
const next = await doRefresh(base);
|
|
165
|
+
writeBackClaudeCodeCredentials(next, base.accessToken);
|
|
166
|
+
return next;
|
|
167
|
+
}
|
package/lib/auth/pkce.js
CHANGED
|
@@ -22,7 +22,7 @@ export function createPkce() {
|
|
|
22
22
|
* @param bytes - entropy length.
|
|
23
23
|
* @returns base64url-encoded random bytes.
|
|
24
24
|
*/
|
|
25
|
-
export function randomToken(bytes =
|
|
25
|
+
export function randomToken(bytes = 32) {
|
|
26
26
|
return base64url(randomBytes(bytes));
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
@@ -323,7 +323,12 @@ export function SubscriptionsSection(props) {
|
|
|
323
323
|
setProviderError(provider, undefined);
|
|
324
324
|
try {
|
|
325
325
|
const response = await callSubscriptionsAuth(rpc, 'login', { provider });
|
|
326
|
-
if (typeof response.authorizeUrl
|
|
326
|
+
if (typeof response.authorizeUrl === 'string' && response.authorizeUrl === '') {
|
|
327
|
+
// Instant login (e.g. imported from Claude Code credentials)
|
|
328
|
+
await refresh();
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
if (typeof response.authorizeUrl !== 'string') {
|
|
327
332
|
throw new SubscriptionsAuthError(t('loginMissingUrl'));
|
|
328
333
|
}
|
|
329
334
|
window.open(response.authorizeUrl, '_blank', 'noopener');
|
package/lib/client.js
CHANGED
|
@@ -518,7 +518,11 @@ function SubscriptionsSection(props) {
|
|
|
518
518
|
setProviderError(provider, void 0);
|
|
519
519
|
try {
|
|
520
520
|
const response = await callSubscriptionsAuth$1(rpc, "login", { provider });
|
|
521
|
-
if (typeof response.authorizeUrl
|
|
521
|
+
if (typeof response.authorizeUrl === "string" && response.authorizeUrl === "") {
|
|
522
|
+
await refresh();
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
if (typeof response.authorizeUrl !== "string") throw new SubscriptionsAuthError(t("loginMissingUrl"));
|
|
522
526
|
window.open(response.authorizeUrl, "_blank", "noopener");
|
|
523
527
|
if (!mountedRef.current) return;
|
|
524
528
|
setStatuses((prev) => ({
|