@pure01fx/dsh-openai-codex-auth 0.7.2 → 0.8.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.
- package/CHANGELOG.md +20 -0
- package/README.md +19 -8
- package/client.js +182 -4
- package/lib/index.d.ts +24 -2
- package/lib/index.js +262 -78
- package/lib/native-adapter.d.ts +2 -0
- package/lib/native-http.js +6 -0
- package/lib/native-websocket-socket.d.ts +2 -0
- package/lib/native-websocket-socket.js +12 -0
- package/lib/native-websocket.js +14 -3
- package/package.json +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.8.0
|
|
6
|
+
|
|
7
|
+
### Multi-account authentication
|
|
8
|
+
|
|
9
|
+
- Adds multiple managed ChatGPT OAuth accounts with a global current-account selector, per-account removal, optional email labels, and automatic activation of newly added accounts.
|
|
10
|
+
- Migrates legacy version-1 single-account credential documents atomically to version 2 while keeping DSH credential publication and rollback guarantees.
|
|
11
|
+
- Pins every request/recovery attempt to its starting account so a concurrent account switch cannot replay an authenticated request under another account.
|
|
12
|
+
- Extends the same-origin settings API and Web UI with an account list, explicit switching, and individual logout controls without exposing access or refresh tokens.
|
|
13
|
+
|
|
14
|
+
## 0.7.3
|
|
15
|
+
|
|
16
|
+
### Client model visibility
|
|
17
|
+
|
|
18
|
+
- Connects the settings-panel Codex visibility controls to DSH's shared model directory: opening either model selector now caches the loaded Codex catalog, updates an already-open settings section, and filters hidden models in the current browser.
|
|
19
|
+
|
|
20
|
+
### WebSocket reliability
|
|
21
|
+
|
|
22
|
+
- Keeps credential-backed WebSocket connection failures inside the bounded reconnect budget and falls back to HTTP/SSE, preventing turns from remaining on “Deep diving...” forever when a server can reach Codex over HTTPS but not WebSocket.
|
|
23
|
+
- Routes WebSocket v2 through the same opt-in `NODE_USE_ENV_PROXY=1`, `HTTPS_PROXY` / `HTTP_PROXY`, and `NO_PROXY` contract as Node fetch, using an HTTP CONNECT tunnel for WSS endpoints.
|
|
24
|
+
|
|
5
25
|
## 0.7.2
|
|
6
26
|
|
|
7
27
|
### Auxiliary-call compatibility
|
package/README.md
CHANGED
|
@@ -48,6 +48,14 @@ dsh web
|
|
|
48
48
|
|
|
49
49
|
设备码登录无需 OAuth callback,适合本机、SSH 隧道和 HTTPS 反向代理场景。
|
|
50
50
|
|
|
51
|
+
## 多账号切换
|
|
52
|
+
|
|
53
|
+
- 已登录后继续点击 **用设备码添加账号** 或 **用浏览器添加账号**,可追加 ChatGPT 账号;新登录的账号会自动成为当前账号。
|
|
54
|
+
- 设置页的 **已添加账号** 列表支持设为当前和单独移除。切换是全局的,只影响切换完成后的新 Codex 请求;已开始的请求继续使用启动时绑定的账号。
|
|
55
|
+
- 移除当前账号时,插件按账号列表顺序选择下一个账号;移除最后一个账号后会清除 `DSH_OPENAI_CODEX_TOKEN`。
|
|
56
|
+
- 原有单账号 `version: 1` 凭据会在首次读取时原子迁移为 `version: 2` 多账号文档,无需重新登录。
|
|
57
|
+
- 如果 `DSH_OPENAI_CODEX_TOKEN` 来自只读环境变量或其他外部 authority,插件会拒绝需要改写当前 token 的登录、切换和退出操作,避免界面与实际请求账号不一致。
|
|
58
|
+
|
|
51
59
|
## 两种登录方式
|
|
52
60
|
|
|
53
61
|
### 设备码登录(推荐)
|
|
@@ -82,6 +90,7 @@ http://localhost:1455/auth/callback
|
|
|
82
90
|
| --- | --- |
|
|
83
91
|
| 设备码登录 | 无本机 callback,支持远程/headless 使用 |
|
|
84
92
|
| 浏览器 OAuth fallback | PKCE + state、端到端 1455 探测、手动 code 回填和临时 callback listener |
|
|
93
|
+
| 多账号管理 | 添加多个 OAuth 账号、全局切换当前账号、单独移除,并自动迁移旧单账号凭据 |
|
|
85
94
|
| Codex 用量面板 | 展示短周期与周用量、剩余额度和重置时间;Native 模式直接接收 Codex 返回的套餐额度,`wham/usage` 仅作初始读取和 fallback |
|
|
86
95
|
| 当前路由状态 | 设置页显示 `openai-codex` 当前由本插件 Native Adapter、外部 Adapter 或无人持有;Native 模式同时显示 WebSocket v2 或 HTTP/SSE |
|
|
87
96
|
| 输入框额度圈 | 登录后显示在输入框右下角;悬停或键盘聚焦显示用量,点击圆圈强制刷新,不做常驻额度轮询 |
|
|
@@ -99,9 +108,9 @@ http://localhost:1455/auth/callback
|
|
|
99
108
|
|
|
100
109
|
登录成功后:
|
|
101
110
|
|
|
102
|
-
1.
|
|
103
|
-
2. access token 通过 DSH credentials 注入 `DSH_OPENAI_CODEX_TOKEN`。
|
|
104
|
-
3. `openai-codex`
|
|
111
|
+
1. 多账号凭据以 owner-only 权限原子写入 `$DSH_HOME/openai-codex-auth.json`;文档保存账号列表与全局当前账号指针。
|
|
112
|
+
2. 当前账号的 access token 通过 DSH credentials 注入 `DSH_OPENAI_CODEX_TOKEN`。
|
|
113
|
+
3. `openai-codex` 模型提供方在每个请求开始时读取当前凭据,并把该次尝试固定到同一个账号。
|
|
105
114
|
4. 输入框右下角显示 Codex 额度圈;悬停或键盘聚焦显示当前用量,点击才强制刷新。Native transport 优先消费 WebSocket `codex.rate_limits` 事件或 HTTP `x-codex-*` headers,未收到直接更新时才在 turn 结束后读取 `wham/usage`。
|
|
106
115
|
5. 设置页与额度圈只读取登录状态、账号 ID、过期时间和用量摘要,不接触 token。
|
|
107
116
|
|
|
@@ -111,13 +120,15 @@ http://localhost:1455/auth/callback
|
|
|
111
120
|
|
|
112
121
|
| 方法 | 路径 | 用途 |
|
|
113
122
|
| --- | --- | --- |
|
|
114
|
-
| GET | `/openai-codex/status` |
|
|
123
|
+
| GET | `/openai-codex/status` | 登录、账号列表、当前账号、flow、用量和 CSRF 状态 |
|
|
124
|
+
| POST | `/openai-codex/accounts/current` | 以 `{ "accountId": "…" }` 设定全局当前账号 |
|
|
125
|
+
| POST | `/openai-codex/accounts/logout` | 以 `{ "accountId": "…" }` 单独移除账号 |
|
|
115
126
|
| POST | `/openai-codex/device/start` | 创建或复用设备码 flow |
|
|
116
127
|
| POST | `/openai-codex/browser/prepare` | 创建浏览器 flow,返回授权 URL 和一次性 1455 探测 URL |
|
|
117
128
|
| POST | `/openai-codex/browser/complete` | 接收用户粘贴的 callback URL 或 authorization code |
|
|
118
129
|
| GET | `/openai-codex/browser/start` | 兼容入口:直接启动并跳转浏览器 OAuth |
|
|
119
130
|
| POST | `/openai-codex/cancel` | 取消当前登录、关闭临时 listener,保留旧凭据 |
|
|
120
|
-
| POST | `/openai-codex/logout` |
|
|
131
|
+
| POST | `/openai-codex/logout` | 兼容入口:取消 flow 并移除当前账号 |
|
|
121
132
|
|
|
122
133
|
1456 已完全退役。1455 平时关闭,仅浏览器 OAuth pending 时绑定 loopback;设备码登录完全不使用它。
|
|
123
134
|
|
|
@@ -189,7 +200,7 @@ $DSH_HOME/openai-codex-auth.json
|
|
|
189
200
|
|
|
190
201
|
上游 Native Codex Responses 请求没有输出 token 上限字段,因此普通会话若显式设置 `maxTokens` 仍会在 Provider I/O 前失败。DSH 的压缩与会话标题辅助调用会固定携带 `maxTokens`,并分别通过 `purpose: compaction` 与 `purpose: session-title` 标识;本适配器允许这两类 hint 通过,但不会把它序列化为 `max_output_tokens`,实际输出长度仍由 Native Codex 决定。
|
|
191
202
|
|
|
192
|
-
原生 route 默认使用 Responses WebSocket v2,在会话首个请求上执行 `generate: false` prewarm,并仅在请求历史严格延伸时发送 `previous_response_id`
|
|
203
|
+
原生 route 默认使用 Responses WebSocket v2,在会话首个请求上执行 `generate: false` prewarm,并仅在请求历史严格延伸时发送 `previous_response_id` 与增量后缀。连接重建会清除增量链;凭据解析完成前的网络失败不消耗普通 stream retry 预算,而是按 Codex 的网络恢复策略从 5 秒指数退避到 60 秒并持续等待成功或取消。凭据可用后,WebSocket 连接建立失败与连接已经建立后的首个 DSH chunk 前错误都使用默认 5 次 stream retry 与 200ms 起步的有界指数退避;安全重试耗尽或握手返回 HTTP 426 后,该 DSH 会话会确定性地回退到 HTTP/SSE,因此仅 WebSocket 不可达不会让界面无限停在 Deep diving。transport 层在任何 DSH chunk 已输出后仍不会直接重放原始 stream;它会把瞬态中断重新分类为只允许 durable failed-step 恢复的错误,标准 Agent Profile 中的 `dsh-llm-retry` 再按默认 5 次策略重建请求。该外层策略不会再次匹配 transport 已在首个 chunk 前耗尽的有限 stream 错误,避免两层预算相乘;失败 attempt 的原始事件可以留作非 surface 诊断,但不会组装成模型可见的持久 assistant message。未加载该恢复插件的直接 `ctx.llm.stream()` 调用在首个 chunk 后仍保持 single-attempt,避免重复输出。WebSocket `codex.rate_limits` 事件、握手/错误 metadata 以及 HTTP/SSE response headers 中的 `x-codex-*` quota 会被边界校验后直接写入 Host 用量缓存;额外 metered limit 不会覆盖默认 `codex` 套餐卡片。`response.completed.usage_metadata.amount` 会按原始高精度字符串保留,并作为当前账号的最近响应观测出现在 status JSON 中。
|
|
193
204
|
|
|
194
205
|
`<base>-fast` 只是公开选择别名:wire model 仍是 `<base>`,请求携带 `service_tier: priority`。若账号目录不声明 priority 能力,或请求前账号 authority 已改变,Fast 会直接失败,不会静默降级。Reasoning 选择在 adapter 边界按上游规则转换:`persistent` 在线上请求中写为 `disabled`;`ultra` 优先使用 Catalog 的 `multi_agent_reasoning_effort`,否则回退到 `max`、最高非 Ultra 档或 `medium`。
|
|
195
206
|
|
|
@@ -221,7 +232,7 @@ $DSH_HOME/openai-codex-auth.json
|
|
|
221
232
|
|
|
222
233
|
## 代理
|
|
223
234
|
|
|
224
|
-
|
|
235
|
+
插件的 HTTP/SSE 请求使用 Node.js 原生 `fetch`,WebSocket v2 使用 `ws`。设置 `NODE_USE_ENV_PROXY=1` 后,两条链路都会按 `HTTPS_PROXY` / `HTTP_PROXY` 选择 HTTP CONNECT 代理并遵守 `NO_PROXY`;若访问 OpenAI 需要代理,请在启动 DSH 前设置:
|
|
225
236
|
|
|
226
237
|
```sh
|
|
227
238
|
NODE_USE_ENV_PROXY=1 \
|
|
@@ -230,7 +241,7 @@ HTTPS_PROXY=http://127.0.0.1:7890 \
|
|
|
230
241
|
dsh web
|
|
231
242
|
```
|
|
232
243
|
|
|
233
|
-
可按环境补充 `NO_PROXY=localhost,127.0.0.1,::1
|
|
244
|
+
可按环境补充 `NO_PROXY=localhost,127.0.0.1,::1`。对于 `https://` / `wss://` Codex 端点应设置 `HTTPS_PROXY`;代理地址本身可以是 `http://`,WebSocket 会通过该代理向 `chatgpt.com:443` 建立 CONNECT 隧道。
|
|
234
245
|
|
|
235
246
|
## 常见问题
|
|
236
247
|
|
package/client.js
CHANGED
|
@@ -49,6 +49,121 @@ window.__ModuleLoader__.load({
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
function writeAvailableModels(models) {
|
|
53
|
+
try {
|
|
54
|
+
const next = JSON.stringify(models)
|
|
55
|
+
if (window.localStorage.getItem(AVAILABLE_MODELS_KEY) === next) return
|
|
56
|
+
window.localStorage.setItem(AVAILABLE_MODELS_KEY, next)
|
|
57
|
+
window.dispatchEvent(new CustomEvent(MODEL_CATALOG_EVENT, { detail: models }))
|
|
58
|
+
} catch {
|
|
59
|
+
// Browser-local catalog persistence is advisory and must never break model loading.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function codexModels(groups) {
|
|
64
|
+
const models = []
|
|
65
|
+
const seen = new Set()
|
|
66
|
+
for (const group of Array.isArray(groups) ? groups : []) {
|
|
67
|
+
if (!group || (group.id !== 'openai-codex' && group.id !== 'openai-codex-native')) continue
|
|
68
|
+
for (const model of Array.isArray(group.models) ? group.models : []) {
|
|
69
|
+
if (!model || typeof model.id !== 'string' || model.id.length === 0
|
|
70
|
+
|| typeof model.name !== 'string' || model.name.length === 0 || seen.has(model.id)) continue
|
|
71
|
+
seen.add(model.id)
|
|
72
|
+
models.push({ id: model.id, name: model.name })
|
|
73
|
+
if (models.length >= 512) return models
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return models
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function visibleModelGroups(groups) {
|
|
80
|
+
if (!Array.isArray(groups)) return []
|
|
81
|
+
const hidden = new Set(readModelVisibility().hidden)
|
|
82
|
+
if (hidden.size === 0) return groups
|
|
83
|
+
return groups.flatMap((group) => {
|
|
84
|
+
if (!group || (group.id !== 'openai-codex' && group.id !== 'openai-codex-native')) return [group]
|
|
85
|
+
const models = Array.isArray(group.models)
|
|
86
|
+
? group.models.filter((model) => model && !hidden.has(model.id)) : []
|
|
87
|
+
return [{ ...group, models }]
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function installModelVisibilityBridge(ctx) {
|
|
92
|
+
const resolver = ctx.modelDirectories
|
|
93
|
+
if (!resolver || typeof resolver.directoryFor !== 'function') return () => {}
|
|
94
|
+
const originalDirectoryFor = resolver.directoryFor
|
|
95
|
+
const tracked = new Map()
|
|
96
|
+
|
|
97
|
+
const render = (directory, rawGroups) => {
|
|
98
|
+
const available = codexModels(rawGroups)
|
|
99
|
+
if (available.length > 0) writeAvailableModels(available)
|
|
100
|
+
const groups = visibleModelGroups(rawGroups)
|
|
101
|
+
if (directory.store && typeof directory.store.update === 'function') {
|
|
102
|
+
directory.store.update((state) => { state.groups = groups })
|
|
103
|
+
}
|
|
104
|
+
return groups
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const track = (directory) => {
|
|
108
|
+
if (!directory || tracked.has(directory)
|
|
109
|
+
|| typeof directory.load !== 'function' || typeof directory.dispose !== 'function') return directory
|
|
110
|
+
const record = {
|
|
111
|
+
load: directory.load,
|
|
112
|
+
dispose: directory.dispose,
|
|
113
|
+
wrappedLoad: undefined,
|
|
114
|
+
wrappedDispose: undefined,
|
|
115
|
+
generation: 0,
|
|
116
|
+
rawGroups: directory.store && typeof directory.store.getSnapshot === 'function'
|
|
117
|
+
? directory.store.getSnapshot().groups : [],
|
|
118
|
+
}
|
|
119
|
+
record.wrappedLoad = async function (...args) {
|
|
120
|
+
const generation = ++record.generation
|
|
121
|
+
const value = await record.load.apply(this, args)
|
|
122
|
+
const rawGroups = Array.isArray(value && value.groups) ? value.groups : []
|
|
123
|
+
if (generation !== record.generation) {
|
|
124
|
+
const groups = visibleModelGroups(rawGroups)
|
|
125
|
+
return value && typeof value === 'object' ? { ...value, groups } : value
|
|
126
|
+
}
|
|
127
|
+
record.rawGroups = rawGroups
|
|
128
|
+
const groups = render(directory, record.rawGroups)
|
|
129
|
+
return value && typeof value === 'object' ? { ...value, groups } : value
|
|
130
|
+
}
|
|
131
|
+
record.wrappedDispose = function (...args) {
|
|
132
|
+
++record.generation
|
|
133
|
+
tracked.delete(directory)
|
|
134
|
+
return record.dispose.apply(this, args)
|
|
135
|
+
}
|
|
136
|
+
directory.load = record.wrappedLoad
|
|
137
|
+
directory.dispose = record.wrappedDispose
|
|
138
|
+
tracked.set(directory, record)
|
|
139
|
+
if (record.rawGroups.length > 0) render(directory, record.rawGroups)
|
|
140
|
+
return directory
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const bridgedDirectoryFor = function (...args) {
|
|
144
|
+
return track(originalDirectoryFor.apply(this, args))
|
|
145
|
+
}
|
|
146
|
+
resolver.directoryFor = bridgedDirectoryFor
|
|
147
|
+
if (resolver.live && resolver.live.directories && typeof resolver.live.directories.values === 'function') {
|
|
148
|
+
for (const directory of resolver.live.directories.values()) track(directory)
|
|
149
|
+
}
|
|
150
|
+
const refresh = () => {
|
|
151
|
+
for (const [directory, record] of tracked) render(directory, record.rawGroups)
|
|
152
|
+
}
|
|
153
|
+
window.addEventListener(MODEL_VISIBILITY_EVENT, refresh)
|
|
154
|
+
|
|
155
|
+
return () => {
|
|
156
|
+
window.removeEventListener(MODEL_VISIBILITY_EVENT, refresh)
|
|
157
|
+
if (resolver.directoryFor === bridgedDirectoryFor) resolver.directoryFor = originalDirectoryFor
|
|
158
|
+
for (const [directory, record] of tracked) {
|
|
159
|
+
++record.generation
|
|
160
|
+
if (directory.load === record.wrappedLoad) directory.load = record.load
|
|
161
|
+
if (directory.dispose === record.wrappedDispose) directory.dispose = record.dispose
|
|
162
|
+
}
|
|
163
|
+
tracked.clear()
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
52
167
|
const css = `
|
|
53
168
|
.codexSection{box-sizing:border-box;max-width:720px;color:var(--dsw-alias-label-primary);display:flex;flex-direction:column;gap:12px}
|
|
54
169
|
.codexSection *{box-sizing:border-box}.codexTitle{color:var(--dsw-alias-label-primary);margin:0;font-size:16px;font-weight:500;line-height:24px}
|
|
@@ -550,7 +665,38 @@ window.__ModuleLoader__.load({
|
|
|
550
665
|
}
|
|
551
666
|
}
|
|
552
667
|
|
|
668
|
+
const mutateAccount = async (path, accountId, action) => {
|
|
669
|
+
if (busy) return
|
|
670
|
+
setBusy(action + ':' + accountId)
|
|
671
|
+
setError('')
|
|
672
|
+
try {
|
|
673
|
+
await post(path, { accountId })
|
|
674
|
+
await load(false)
|
|
675
|
+
} catch (mutationError) {
|
|
676
|
+
setError(messageOf(mutationError))
|
|
677
|
+
} finally {
|
|
678
|
+
setBusy('')
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const setCurrentAccount = async (accountId) => {
|
|
683
|
+
await mutateAccount('/accounts/current', accountId, 'current')
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const removeAccount = async (accountId) => {
|
|
687
|
+
const account = accounts.find((candidate) => candidate.accountId === accountId)
|
|
688
|
+
const label = account && account.email ? account.email : shortAccount(accountId)
|
|
689
|
+
const isCurrent = account && (account.current === true || account.accountId === currentAccountId)
|
|
690
|
+
const isLast = accounts.length === 1
|
|
691
|
+
const consequence = isLast
|
|
692
|
+
? '移除后将退出 Codex,重新使用需要再次完成 OAuth 登录。'
|
|
693
|
+
: isCurrent ? '移除后会自动切换到列表中的下一个账号。' : '该账号的本地 OAuth 凭据会被删除。'
|
|
694
|
+
if (!window.confirm('确定移除 ' + label + '?\n\n' + consequence)) return
|
|
695
|
+
await mutateAccount('/accounts/logout', accountId, 'remove')
|
|
696
|
+
}
|
|
697
|
+
|
|
553
698
|
const logout = async () => {
|
|
699
|
+
if (busy) return
|
|
554
700
|
setBusy('logout')
|
|
555
701
|
probeGeneration.current += 1
|
|
556
702
|
try {
|
|
@@ -586,6 +732,10 @@ window.__ModuleLoader__.load({
|
|
|
586
732
|
const usage = status && status.usage
|
|
587
733
|
const loading = status === null && !error
|
|
588
734
|
const connected = Boolean(status && status.loggedIn)
|
|
735
|
+
const accounts = status && Array.isArray(status.accounts) ? status.accounts : []
|
|
736
|
+
const currentAccountId = status && status.currentAccountId
|
|
737
|
+
const currentAccount = accounts.find((account) => account.current === true || account.accountId === currentAccountId)
|
|
738
|
+
const currentAccountLabel = currentAccount && currentAccount.email ? currentAccount.email : shortAccount(status && status.accountId)
|
|
589
739
|
const pending = Boolean(status && status.loginPending) || watchLogin
|
|
590
740
|
const devicePending = Boolean(status && status.loginMethod === 'device' && status.device)
|
|
591
741
|
const browserPending = Boolean(status && status.loginMethod === 'browser' && status.browser)
|
|
@@ -616,7 +766,7 @@ window.__ModuleLoader__.load({
|
|
|
616
766
|
h('div', { className: 'codexLogo', 'aria-hidden': true }, 'OA'),
|
|
617
767
|
h('div', null,
|
|
618
768
|
h('h3', { className: 'codexName' }, 'OpenAI Codex 订阅'),
|
|
619
|
-
h('p', { className: 'codexMeta', title: connected ? status.accountId : '' }, loading ? '正在读取 OpenAI 登录状态' : connected ?
|
|
769
|
+
h('p', { className: 'codexMeta', title: connected ? status.accountId : '' }, loading ? '正在读取 OpenAI 登录状态' : connected ? currentAccountLabel : '尚未连接 ChatGPT 账号'),
|
|
620
770
|
),
|
|
621
771
|
),
|
|
622
772
|
h('span', { className: 'codexBadge ' + (loading || pending ? 'pending' : connected ? 'connected' : '') },
|
|
@@ -636,6 +786,31 @@ window.__ModuleLoader__.load({
|
|
|
636
786
|
h('p', { className: 'codexRouteDetail' }, route.detail),
|
|
637
787
|
)
|
|
638
788
|
: null,
|
|
789
|
+
!loading && accounts.length > 0
|
|
790
|
+
? h('div', { className: 'codexModelVisibility' },
|
|
791
|
+
h('div', { className: 'codexModelVisibilityHead' },
|
|
792
|
+
h('strong', null, '已添加账号'),
|
|
793
|
+
h('span', { className: 'codexMeta' }, accounts.length + ' 个'),
|
|
794
|
+
),
|
|
795
|
+
h('p', { className: 'codexModelVisibilityHint' }, '当前账号用于新的 Codex 请求;你可以切换或单独移除任意账号。'),
|
|
796
|
+
h('div', { className: 'codexModelVisibilityList' }, accounts.map((account) => {
|
|
797
|
+
const isCurrent = account.current === true || account.accountId === currentAccountId
|
|
798
|
+
const accountBusy = busy === 'current:' + account.accountId || busy === 'remove:' + account.accountId
|
|
799
|
+
return h('div', { className: 'codexModelVisibilityOption', key: account.accountId },
|
|
800
|
+
h('span', { title: account.accountId },
|
|
801
|
+
h('strong', null, account.email || shortAccount(account.accountId)),
|
|
802
|
+
account.email ? h('span', null, ' · ' + shortAccount(account.accountId)) : null,
|
|
803
|
+
h('span', null, isCurrent ? ' · 当前账号' : ''),
|
|
804
|
+
account.expiresAt ? h('span', null, ' · 到期 ' + new Date(account.expiresAt).toLocaleString()) : null,
|
|
805
|
+
),
|
|
806
|
+
h('div', { className: 'codexActions' },
|
|
807
|
+
!isCurrent ? h('button', { type: 'button', className: 'codexButton', disabled: Boolean(busy), onClick: () => { void setCurrentAccount(account.accountId) } }, accountBusy && busy.startsWith('current:') ? '切换中…' : '设为当前') : null,
|
|
808
|
+
h('button', { type: 'button', className: 'codexButton danger', disabled: Boolean(busy), onClick: () => { void removeAccount(account.accountId) } }, accountBusy && busy.startsWith('remove:') ? '移除中…' : '移除'),
|
|
809
|
+
),
|
|
810
|
+
)
|
|
811
|
+
})),
|
|
812
|
+
)
|
|
813
|
+
: null,
|
|
639
814
|
loading
|
|
640
815
|
? h('div', { 'aria-label': '加载中' }, h('div', { className: 'codexSkeleton' }), h('div', { className: 'codexSkeleton', style: { width: '72%' } }))
|
|
641
816
|
: connected
|
|
@@ -733,11 +908,11 @@ window.__ModuleLoader__.load({
|
|
|
733
908
|
: null,
|
|
734
909
|
h('div', { className: 'codexActions' },
|
|
735
910
|
h('button', { type: 'button', className: 'codexButton primary', disabled: Boolean(busy) || pending || loading, onClick: () => { void startDevice() } },
|
|
736
|
-
busy === 'device' ? '正在申请设备码…' : connected ? '
|
|
911
|
+
busy === 'device' ? '正在申请设备码…' : connected ? '用设备码添加账号' : '使用设备码登录'),
|
|
737
912
|
h('button', {
|
|
738
913
|
type: 'button', className: 'codexButton', disabled: Boolean(busy) || pending || loading || !browserAvailable,
|
|
739
914
|
title: browserAvailable ? '展开 callback 检测,不会自动弹出窗口' : '仅支持 HTTP 127.0.0.1/localhost 入口', onClick: () => { void startBrowser() },
|
|
740
|
-
}, busy === 'browser' ? '正在准备浏览器登录…' : connected ? '
|
|
915
|
+
}, busy === 'browser' ? '正在准备浏览器登录…' : connected ? '用浏览器添加账号' : '本机浏览器 OAuth'),
|
|
741
916
|
pending && !devicePending && !browserPending ? h('button', { type: 'button', className: 'codexButton danger', disabled: Boolean(busy), onClick: () => { void cancel() } }, busy === 'cancel' ? '取消中…' : '取消登录') : null,
|
|
742
917
|
connected ? h('button', { type: 'button', className: 'codexButton', disabled: Boolean(busy), onClick: refresh }, busy === 'refresh' ? '刷新中…' : '刷新用量') : null,
|
|
743
918
|
connected || (status && status.credentialError) ? h('button', { type: 'button', className: 'codexButton danger', disabled: Boolean(busy), onClick: () => { void logout() } }, busy === 'logout' ? '退出中…' : '退出登录') : null,
|
|
@@ -748,8 +923,11 @@ window.__ModuleLoader__.load({
|
|
|
748
923
|
)
|
|
749
924
|
}
|
|
750
925
|
|
|
751
|
-
const inject = ['slots']
|
|
926
|
+
const inject = ['slots', 'modelDirectories']
|
|
752
927
|
function apply(ctx) {
|
|
928
|
+
ctx.inject(['modelDirectories'], (scope) => {
|
|
929
|
+
scope.effect(() => installModelVisibilityBridge(scope), 'openai-codex-auth: model visibility bridge')
|
|
930
|
+
})
|
|
753
931
|
ctx.slots.inject('settings.section', () => ctx.slots.register({
|
|
754
932
|
name: 'settings.section',
|
|
755
933
|
id: 'openai-codex',
|
package/lib/index.d.ts
CHANGED
|
@@ -7,12 +7,23 @@ import { CODEX_PROVIDER } from './native-adapter.js';
|
|
|
7
7
|
export { CODEX_PROVIDER, NATIVE_CODEX_PROVIDER } from './native-adapter.js';
|
|
8
8
|
export { normalizeUsage } from './usage.js';
|
|
9
9
|
export { CODEX_CLIENT_VERSION, TRACKED_CODEX_COMMIT, TRACKED_CODEX_RELEASE, TRACKED_CODEX_REPOSITORY, } from './upstream.js';
|
|
10
|
-
/** Persisted OAuth credential. */
|
|
10
|
+
/** Persisted OAuth credential for one ChatGPT account. */
|
|
11
11
|
export interface OpenAICodexCredential {
|
|
12
12
|
access: string;
|
|
13
13
|
refresh: string;
|
|
14
14
|
expires: number;
|
|
15
15
|
accountId: string;
|
|
16
|
+
email?: string;
|
|
17
|
+
}
|
|
18
|
+
/** Current multi-account credential document. */
|
|
19
|
+
export interface OpenAICodexCredentialDocument {
|
|
20
|
+
version: 2;
|
|
21
|
+
currentAccountId: string | null;
|
|
22
|
+
accounts: OpenAICodexCredential[];
|
|
23
|
+
}
|
|
24
|
+
interface ParsedCredentialDocument {
|
|
25
|
+
document: OpenAICodexCredentialDocument;
|
|
26
|
+
migrated: boolean;
|
|
16
27
|
}
|
|
17
28
|
/** Plugin configuration. */
|
|
18
29
|
export interface Config {
|
|
@@ -73,6 +84,8 @@ interface TokenResponse {
|
|
|
73
84
|
id_token?: unknown;
|
|
74
85
|
}
|
|
75
86
|
declare function parseTokenResponse(value: TokenResponse | null, previous?: OpenAICodexCredential): OpenAICodexCredential;
|
|
87
|
+
declare function parseCredentialDocument(text: string, filename: string): ParsedCredentialDocument;
|
|
88
|
+
declare function currentCredential(document: OpenAICodexCredentialDocument | undefined): OpenAICodexCredential | undefined;
|
|
76
89
|
declare function parseAuthority(authority: string | undefined): URL | undefined;
|
|
77
90
|
declare function isLoopbackHostname(hostname: string): boolean;
|
|
78
91
|
declare function isTrustedHost(authority: string | undefined, trustedHosts: readonly string[]): boolean;
|
|
@@ -85,6 +98,8 @@ declare function pollDeviceAuthorization(device: DeviceAuthorization, signal?: A
|
|
|
85
98
|
declare function resolveCodexRouteStatus(config: NativeRouteConfig, llm: RouteRuntime | undefined): CodexRouteStatus;
|
|
86
99
|
export declare const internals: {
|
|
87
100
|
parseTokenResponse: typeof parseTokenResponse;
|
|
101
|
+
parseCredentialDocument: typeof parseCredentialDocument;
|
|
102
|
+
currentCredential: typeof currentCredential;
|
|
88
103
|
parseAuthority: typeof parseAuthority;
|
|
89
104
|
isLoopbackHostname: typeof isLoopbackHostname;
|
|
90
105
|
isTrustedHost: typeof isTrustedHost;
|
|
@@ -138,9 +153,11 @@ export declare class OpenAICodexAuth extends Service {
|
|
|
138
153
|
private restorePublishedCredential;
|
|
139
154
|
private publicationChangedError;
|
|
140
155
|
private failAfterPublicationRollback;
|
|
156
|
+
private commitDocument;
|
|
157
|
+
private upsertCurrentCredential;
|
|
141
158
|
private commitCredential;
|
|
142
159
|
private resolveManagedCredentialLocked;
|
|
143
|
-
/** Return
|
|
160
|
+
/** Return the current managed bearer token, refreshing and migrating it when needed. */
|
|
144
161
|
bearerToken(signal?: AbortSignal): Promise<string | undefined>;
|
|
145
162
|
private externalNativeCredential;
|
|
146
163
|
private resolveNativeCredential;
|
|
@@ -154,6 +171,8 @@ export declare class OpenAICodexAuth extends Service {
|
|
|
154
171
|
private beginBrowserLogin;
|
|
155
172
|
private createBrowserLogin;
|
|
156
173
|
private cancelLogin;
|
|
174
|
+
private resetCurrentAccountState;
|
|
175
|
+
private setCurrentAccount;
|
|
157
176
|
private logout;
|
|
158
177
|
private status;
|
|
159
178
|
private fetchUsage;
|
|
@@ -169,6 +188,9 @@ export declare class OpenAICodexAuth extends Service {
|
|
|
169
188
|
private handleBrowserComplete;
|
|
170
189
|
private handleCallback;
|
|
171
190
|
private handleCancel;
|
|
191
|
+
private accountIdBody;
|
|
192
|
+
private handleSetCurrentAccount;
|
|
193
|
+
private handleAccountLogout;
|
|
172
194
|
private handleLogout;
|
|
173
195
|
}
|
|
174
196
|
export default OpenAICodexAuth;
|
package/lib/index.js
CHANGED
|
@@ -46,6 +46,8 @@ class LoginConflictError extends Error {
|
|
|
46
46
|
}
|
|
47
47
|
class CredentialNotWritableError extends Error {
|
|
48
48
|
}
|
|
49
|
+
class AccountNotFoundError extends Error {
|
|
50
|
+
}
|
|
49
51
|
const PERMANENT_REFRESH_CODES = new Set([
|
|
50
52
|
'refresh_token_expired',
|
|
51
53
|
'refresh_token_reused',
|
|
@@ -124,28 +126,70 @@ function parseTokenResponse(value, previous) {
|
|
|
124
126
|
: chatGptAccountId(idToken, 'ID token');
|
|
125
127
|
if (resolvedAccountId === undefined)
|
|
126
128
|
throw new Error('OpenAI token response has no ID token');
|
|
127
|
-
|
|
129
|
+
const emailClaim = idToken === undefined ? undefined : jwtPayload(idToken, 'ID token').email;
|
|
130
|
+
const email = typeof emailClaim === 'string' && emailClaim.trim() !== ''
|
|
131
|
+
? emailClaim.trim()
|
|
132
|
+
: previous?.email;
|
|
133
|
+
return {
|
|
134
|
+
access: value.access_token,
|
|
135
|
+
refresh,
|
|
136
|
+
expires,
|
|
137
|
+
accountId: resolvedAccountId,
|
|
138
|
+
...email === undefined ? {} : { email },
|
|
139
|
+
};
|
|
128
140
|
}
|
|
129
141
|
function isPermanentRefreshError(error) {
|
|
130
142
|
return error instanceof OAuthEndpointError
|
|
131
143
|
&& (error.status === 401
|
|
132
144
|
|| (error.oauthCode !== undefined && PERMANENT_REFRESH_CODES.has(error.oauthCode.toLowerCase())));
|
|
133
145
|
}
|
|
134
|
-
function
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
if (value.version !== 1 || credential === undefined
|
|
146
|
+
function validateCredential(value, filename) {
|
|
147
|
+
const credential = value;
|
|
148
|
+
if (credential === null || typeof credential !== 'object'
|
|
138
149
|
|| typeof credential.access !== 'string' || credential.access.length === 0
|
|
139
150
|
|| typeof credential.refresh !== 'string' || credential.refresh.length === 0
|
|
140
151
|
|| typeof credential.expires !== 'number' || !Number.isFinite(credential.expires)
|
|
141
|
-
|| typeof credential.accountId !== 'string' || credential.accountId.length === 0
|
|
152
|
+
|| typeof credential.accountId !== 'string' || credential.accountId.length === 0
|
|
153
|
+
|| credential.accountId.length > 512
|
|
154
|
+
|| (credential.email !== undefined && (typeof credential.email !== 'string' || credential.email.length > 512))) {
|
|
142
155
|
throw new Error(`openai-codex-auth: invalid credential document ${filename}`);
|
|
143
156
|
}
|
|
144
|
-
return
|
|
157
|
+
return {
|
|
158
|
+
access: credential.access,
|
|
159
|
+
refresh: credential.refresh,
|
|
160
|
+
expires: credential.expires,
|
|
161
|
+
accountId: credential.accountId,
|
|
162
|
+
...credential.email === undefined ? {} : { email: credential.email },
|
|
163
|
+
};
|
|
145
164
|
}
|
|
146
|
-
|
|
165
|
+
function parseCredentialDocument(text, filename) {
|
|
166
|
+
const value = JSON.parse(text);
|
|
167
|
+
if (value.version === 1) {
|
|
168
|
+
const credential = validateCredential(value.credential, filename);
|
|
169
|
+
return {
|
|
170
|
+
document: { version: 2, currentAccountId: credential.accountId, accounts: [credential] },
|
|
171
|
+
migrated: true,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
if (value.version !== 2 || !Array.isArray(value.accounts)
|
|
175
|
+
|| (value.currentAccountId !== null && typeof value.currentAccountId !== 'string')) {
|
|
176
|
+
throw new Error(`openai-codex-auth: invalid credential document ${filename}`);
|
|
177
|
+
}
|
|
178
|
+
const accounts = value.accounts.map(account => validateCredential(account, filename));
|
|
179
|
+
const ids = new Set(accounts.map(account => account.accountId));
|
|
180
|
+
if (ids.size !== accounts.length
|
|
181
|
+
|| (accounts.length === 0 && value.currentAccountId !== null)
|
|
182
|
+
|| (accounts.length > 0 && (value.currentAccountId === null || !ids.has(value.currentAccountId)))) {
|
|
183
|
+
throw new Error(`openai-codex-auth: invalid credential document ${filename}`);
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
document: { version: 2, currentAccountId: value.currentAccountId, accounts },
|
|
187
|
+
migrated: false,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
async function readCredentialDocument(filename) {
|
|
147
191
|
try {
|
|
148
|
-
return
|
|
192
|
+
return parseCredentialDocument(await readFile(filename, 'utf8'), filename);
|
|
149
193
|
}
|
|
150
194
|
catch (error) {
|
|
151
195
|
if (error.code === 'ENOENT')
|
|
@@ -153,6 +197,11 @@ async function readCredential(filename) {
|
|
|
153
197
|
throw error;
|
|
154
198
|
}
|
|
155
199
|
}
|
|
200
|
+
function currentCredential(document) {
|
|
201
|
+
if (document?.currentAccountId === null || document === undefined)
|
|
202
|
+
return undefined;
|
|
203
|
+
return document.accounts.find(account => account.accountId === document.currentAccountId);
|
|
204
|
+
}
|
|
156
205
|
async function responseText(response) {
|
|
157
206
|
return (await response.text().catch(() => '')).slice(0, MAX_ERROR_BODY_LENGTH);
|
|
158
207
|
}
|
|
@@ -573,6 +622,8 @@ function resolveCodexRouteStatus(config, llm) {
|
|
|
573
622
|
}
|
|
574
623
|
export const internals = {
|
|
575
624
|
parseTokenResponse,
|
|
625
|
+
parseCredentialDocument,
|
|
626
|
+
currentCredential,
|
|
576
627
|
parseAuthority,
|
|
577
628
|
isLoopbackHostname,
|
|
578
629
|
isTrustedHost,
|
|
@@ -703,6 +754,8 @@ export class OpenAICodexAuth extends Service {
|
|
|
703
754
|
register('/openai-codex/browser/prepare', (req, res) => this.handleBrowserPrepare(req, res));
|
|
704
755
|
register('/openai-codex/browser/complete', (req, res) => this.handleBrowserComplete(req, res));
|
|
705
756
|
register('/openai-codex/cancel', (req, res) => this.handleCancel(req, res));
|
|
757
|
+
register('/openai-codex/accounts/current', (req, res) => this.handleSetCurrentAccount(req, res));
|
|
758
|
+
register('/openai-codex/accounts/logout', (req, res) => this.handleAccountLogout(req, res));
|
|
706
759
|
register('/openai-codex/logout', (req, res) => this.handleLogout(req, res));
|
|
707
760
|
}
|
|
708
761
|
catch (error) {
|
|
@@ -802,7 +855,7 @@ export class OpenAICodexAuth extends Service {
|
|
|
802
855
|
}
|
|
803
856
|
async performUsageRefresh(generation) {
|
|
804
857
|
try {
|
|
805
|
-
const credential = await
|
|
858
|
+
const credential = await withFileLock(this.filename, () => this.resolveManagedCredentialLocked());
|
|
806
859
|
if (credential === undefined) {
|
|
807
860
|
if (this.usageGeneration === generation) {
|
|
808
861
|
this.usageCache = undefined;
|
|
@@ -900,43 +953,72 @@ export class OpenAICodexAuth extends Service {
|
|
|
900
953
|
}
|
|
901
954
|
throw failure;
|
|
902
955
|
}
|
|
903
|
-
async
|
|
956
|
+
async commitDocument(document, signal) {
|
|
957
|
+
const nextToken = currentCredential(document)?.access;
|
|
904
958
|
const previous = await this.ctx.credentials.resolve(TOKEN_REF);
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
}
|
|
908
|
-
catch (error) {
|
|
909
|
-
let current;
|
|
959
|
+
throwIfCancelled(signal);
|
|
960
|
+
if (previous?.value !== nextToken) {
|
|
910
961
|
try {
|
|
911
|
-
|
|
962
|
+
if (nextToken === undefined)
|
|
963
|
+
await this.ctx.credentials.unset(TOKEN_REF);
|
|
964
|
+
else
|
|
965
|
+
await this.ctx.credentials.set(TOKEN_REF, nextToken);
|
|
912
966
|
}
|
|
913
|
-
catch {
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
967
|
+
catch (error) {
|
|
968
|
+
let current;
|
|
969
|
+
try {
|
|
970
|
+
current = await this.ctx.credentials.resolve(TOKEN_REF);
|
|
971
|
+
}
|
|
972
|
+
catch {
|
|
973
|
+
return this.failAfterPublicationRollback(previous, nextToken, error);
|
|
974
|
+
}
|
|
975
|
+
if (current?.value === nextToken) {
|
|
976
|
+
return this.failAfterPublicationRollback(previous, nextToken, error);
|
|
977
|
+
}
|
|
978
|
+
if (current?.value !== previous?.value)
|
|
979
|
+
throw this.publicationChangedError(error);
|
|
980
|
+
throw error;
|
|
918
981
|
}
|
|
919
|
-
if (current?.value !== previous?.value)
|
|
920
|
-
throw this.publicationChangedError(error);
|
|
921
|
-
throw error;
|
|
922
982
|
}
|
|
923
983
|
try {
|
|
924
|
-
await this.write(
|
|
984
|
+
await this.write(document);
|
|
925
985
|
}
|
|
926
986
|
catch (error) {
|
|
927
|
-
|
|
987
|
+
if (previous?.value === nextToken)
|
|
988
|
+
throw error;
|
|
989
|
+
return this.failAfterPublicationRollback(previous, nextToken, error);
|
|
928
990
|
}
|
|
929
991
|
}
|
|
992
|
+
upsertCurrentCredential(document, credential, replacedAccountId = credential.accountId) {
|
|
993
|
+
const existingIndex = document.accounts.findIndex(account => account.accountId === replacedAccountId);
|
|
994
|
+
const accounts = document.accounts.filter(account => account.accountId !== credential.accountId);
|
|
995
|
+
const filteredIndex = accounts.findIndex(account => account.accountId === replacedAccountId);
|
|
996
|
+
if (filteredIndex >= 0)
|
|
997
|
+
accounts.splice(filteredIndex, 1);
|
|
998
|
+
const insertion = existingIndex < 0 ? accounts.length : Math.min(existingIndex, accounts.length);
|
|
999
|
+
accounts.splice(insertion, 0, credential);
|
|
1000
|
+
return { version: 2, currentAccountId: credential.accountId, accounts };
|
|
1001
|
+
}
|
|
1002
|
+
async commitCredential(credential) {
|
|
1003
|
+
const parsed = await readCredentialDocument(this.filename);
|
|
1004
|
+
const document = parsed?.document ?? { version: 2, currentAccountId: null, accounts: [] };
|
|
1005
|
+
await this.commitDocument(this.upsertCurrentCredential(document, credential));
|
|
1006
|
+
}
|
|
930
1007
|
async resolveManagedCredentialLocked(signal) {
|
|
931
1008
|
throwIfCancelled(signal);
|
|
932
|
-
const
|
|
1009
|
+
const parsed = await readCredentialDocument(this.filename);
|
|
1010
|
+
const document = parsed?.document;
|
|
1011
|
+
const current = currentCredential(document);
|
|
933
1012
|
throwIfCancelled(signal);
|
|
934
1013
|
if (current === undefined) {
|
|
935
1014
|
this.setCredentialAccount(undefined);
|
|
936
1015
|
return undefined;
|
|
937
1016
|
}
|
|
938
1017
|
if (current.expires > Date.now() + TOKEN_REFRESH_PREEMPT_MS) {
|
|
939
|
-
|
|
1018
|
+
if (parsed?.migrated)
|
|
1019
|
+
await this.commitDocument(document, signal);
|
|
1020
|
+
else
|
|
1021
|
+
await this.publishCredentialToken(current.access, signal);
|
|
940
1022
|
this.setCredentialAccount(current.accountId);
|
|
941
1023
|
return current;
|
|
942
1024
|
}
|
|
@@ -948,18 +1030,24 @@ export class OpenAICodexAuth extends Service {
|
|
|
948
1030
|
catch (error) {
|
|
949
1031
|
throwIfCancelled(signal);
|
|
950
1032
|
if (!isPermanentRefreshError(error) && current.expires > Date.now()) {
|
|
951
|
-
|
|
1033
|
+
if (parsed?.migrated)
|
|
1034
|
+
await this.commitDocument(document, signal);
|
|
1035
|
+
else
|
|
1036
|
+
await this.publishCredentialToken(current.access, signal);
|
|
952
1037
|
this.setCredentialAccount(current.accountId);
|
|
953
1038
|
return current;
|
|
954
1039
|
}
|
|
955
1040
|
throw error;
|
|
956
1041
|
}
|
|
957
1042
|
throwIfCancelled(signal);
|
|
958
|
-
|
|
1043
|
+
if (next.accountId !== current.accountId) {
|
|
1044
|
+
throw new Error('OpenAI refresh changed the ChatGPT account identity');
|
|
1045
|
+
}
|
|
1046
|
+
await this.commitDocument(this.upsertCurrentCredential(document, next, current.accountId), signal);
|
|
959
1047
|
this.setCredentialAccount(next.accountId);
|
|
960
1048
|
return next;
|
|
961
1049
|
}
|
|
962
|
-
/** Return
|
|
1050
|
+
/** Return the current managed bearer token, refreshing and migrating it when needed. */
|
|
963
1051
|
async bearerToken(signal) {
|
|
964
1052
|
throwIfCancelled(signal);
|
|
965
1053
|
return withFileLock(this.filename, async () => {
|
|
@@ -1027,7 +1115,9 @@ export class OpenAICodexAuth extends Service {
|
|
|
1027
1115
|
throwIfCancelled(signal);
|
|
1028
1116
|
return await withFileLock(this.filename, async () => {
|
|
1029
1117
|
throwIfCancelled(signal);
|
|
1030
|
-
const
|
|
1118
|
+
const parsed = await readCredentialDocument(this.filename);
|
|
1119
|
+
const document = parsed?.document;
|
|
1120
|
+
const current = currentCredential(document);
|
|
1031
1121
|
throwIfCancelled(signal);
|
|
1032
1122
|
if (current === undefined) {
|
|
1033
1123
|
const external = await this.ctx.credentials.resolve(TOKEN_REF);
|
|
@@ -1036,11 +1126,15 @@ export class OpenAICodexAuth extends Service {
|
|
|
1036
1126
|
return false;
|
|
1037
1127
|
const credential = this.externalNativeCredential(external.value);
|
|
1038
1128
|
throwIfCancelled(signal);
|
|
1129
|
+
if (credential.accountId !== previous.accountId)
|
|
1130
|
+
return false;
|
|
1039
1131
|
this.setCredentialAccount(credential.accountId);
|
|
1040
|
-
return credential.accessToken !== previous.accessToken
|
|
1041
|
-
|| credential.accountId !== previous.accountId;
|
|
1132
|
+
return credential.accessToken !== previous.accessToken;
|
|
1042
1133
|
}
|
|
1043
|
-
|
|
1134
|
+
// A request that started under A must never recover by replaying under B.
|
|
1135
|
+
if (current.accountId !== previous.accountId)
|
|
1136
|
+
return false;
|
|
1137
|
+
if (current.access !== previous.accessToken) {
|
|
1044
1138
|
await this.publishCredentialToken(current.access, signal);
|
|
1045
1139
|
throwIfCancelled(signal);
|
|
1046
1140
|
this.setCredentialAccount(current.accountId);
|
|
@@ -1050,10 +1144,13 @@ export class OpenAICodexAuth extends Service {
|
|
|
1050
1144
|
throwIfCancelled(signal);
|
|
1051
1145
|
const next = await refreshToken(current, signal);
|
|
1052
1146
|
throwIfCancelled(signal);
|
|
1053
|
-
|
|
1147
|
+
if (next.accountId !== previous.accountId) {
|
|
1148
|
+
throw new Error('OpenAI refresh changed the ChatGPT account identity');
|
|
1149
|
+
}
|
|
1150
|
+
await this.commitDocument(this.upsertCurrentCredential(document, next, current.accountId), signal);
|
|
1054
1151
|
throwIfCancelled(signal);
|
|
1055
1152
|
this.setCredentialAccount(next.accountId);
|
|
1056
|
-
return next.access !== previous.accessToken
|
|
1153
|
+
return next.access !== previous.accessToken;
|
|
1057
1154
|
});
|
|
1058
1155
|
}
|
|
1059
1156
|
catch (error) {
|
|
@@ -1296,53 +1393,86 @@ export class OpenAICodexAuth extends Service {
|
|
|
1296
1393
|
if (clearError)
|
|
1297
1394
|
this.lastLoginError = undefined;
|
|
1298
1395
|
}
|
|
1299
|
-
|
|
1300
|
-
|
|
1396
|
+
resetCurrentAccountState(accountId) {
|
|
1397
|
+
this.setCredentialAccount(accountId);
|
|
1398
|
+
this.usageGeneration += 1;
|
|
1399
|
+
if (this.usageRefresh !== undefined)
|
|
1400
|
+
this.usageRefresh.queued = accountId !== undefined;
|
|
1401
|
+
this.usageCache = undefined;
|
|
1402
|
+
this.usageAccountId = undefined;
|
|
1403
|
+
this.directUsageAccountId = undefined;
|
|
1404
|
+
this.usageHasDirectDefault = false;
|
|
1405
|
+
this.usageError = undefined;
|
|
1406
|
+
this.lastLoginError = undefined;
|
|
1407
|
+
}
|
|
1408
|
+
async setCurrentAccount(accountId) {
|
|
1409
|
+
await withFileLock(this.filename, async () => {
|
|
1410
|
+
const document = (await readCredentialDocument(this.filename))?.document;
|
|
1411
|
+
if (document === undefined || !document.accounts.some(account => account.accountId === accountId)) {
|
|
1412
|
+
throw new AccountNotFoundError('OpenAI Codex account was not found');
|
|
1413
|
+
}
|
|
1414
|
+
if (document.currentAccountId !== accountId) {
|
|
1415
|
+
await this.assertCredentialWritable();
|
|
1416
|
+
await this.commitDocument({ ...document, currentAccountId: accountId });
|
|
1417
|
+
}
|
|
1418
|
+
this.resetCurrentAccountState(accountId);
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
async logout(accountId) {
|
|
1422
|
+
if (accountId === undefined)
|
|
1423
|
+
await this.cancelLogin(true);
|
|
1424
|
+
let nextAccountId;
|
|
1425
|
+
let removedCurrent = false;
|
|
1301
1426
|
await withFileLock(this.filename, async () => {
|
|
1302
|
-
|
|
1427
|
+
let parsed;
|
|
1303
1428
|
try {
|
|
1304
|
-
await this.
|
|
1429
|
+
parsed = await readCredentialDocument(this.filename);
|
|
1305
1430
|
}
|
|
1306
1431
|
catch (error) {
|
|
1307
|
-
|
|
1432
|
+
if (accountId !== undefined)
|
|
1433
|
+
throw error;
|
|
1434
|
+
const previous = await this.ctx.credentials.resolve(TOKEN_REF);
|
|
1308
1435
|
try {
|
|
1309
|
-
|
|
1436
|
+
if (previous !== undefined)
|
|
1437
|
+
await this.ctx.credentials.unset(TOKEN_REF);
|
|
1438
|
+
await unlink(this.filename);
|
|
1310
1439
|
}
|
|
1311
|
-
catch {
|
|
1312
|
-
return this.failAfterPublicationRollback(previous, undefined,
|
|
1440
|
+
catch (failure) {
|
|
1441
|
+
return this.failAfterPublicationRollback(previous, undefined, failure);
|
|
1313
1442
|
}
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
if (current.value !== previous?.value)
|
|
1318
|
-
throw this.publicationChangedError(error);
|
|
1319
|
-
throw error;
|
|
1320
|
-
}
|
|
1321
|
-
try {
|
|
1322
|
-
await unlink(this.filename);
|
|
1443
|
+
removedCurrent = true;
|
|
1444
|
+
this.resetCurrentAccountState(undefined);
|
|
1445
|
+
return;
|
|
1323
1446
|
}
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1447
|
+
const document = parsed?.document;
|
|
1448
|
+
const target = accountId ?? document?.currentAccountId ?? undefined;
|
|
1449
|
+
if (document === undefined || target === undefined
|
|
1450
|
+
|| !document.accounts.some(account => account.accountId === target)) {
|
|
1451
|
+
if (accountId !== undefined)
|
|
1452
|
+
throw new AccountNotFoundError('OpenAI Codex account was not found');
|
|
1453
|
+
return;
|
|
1328
1454
|
}
|
|
1455
|
+
removedCurrent = document.currentAccountId === target;
|
|
1456
|
+
const targetIndex = document.accounts.findIndex(account => account.accountId === target);
|
|
1457
|
+
const accounts = document.accounts.filter(account => account.accountId !== target);
|
|
1458
|
+
const currentAccountId = removedCurrent
|
|
1459
|
+
? accounts[targetIndex]?.accountId ?? accounts[0]?.accountId ?? null
|
|
1460
|
+
: document.currentAccountId;
|
|
1461
|
+
if (removedCurrent)
|
|
1462
|
+
await this.assertCredentialWritable();
|
|
1463
|
+
await this.commitDocument({ version: 2, currentAccountId, accounts });
|
|
1464
|
+
nextAccountId = currentAccountId ?? undefined;
|
|
1465
|
+
if (removedCurrent)
|
|
1466
|
+
this.resetCurrentAccountState(nextAccountId);
|
|
1329
1467
|
});
|
|
1330
|
-
this.setCredentialAccount(undefined);
|
|
1331
|
-
this.usageGeneration += 1;
|
|
1332
|
-
if (this.usageRefresh !== undefined)
|
|
1333
|
-
this.usageRefresh.queued = false;
|
|
1334
|
-
this.usageCache = undefined;
|
|
1335
|
-
this.usageAccountId = undefined;
|
|
1336
|
-
this.directUsageAccountId = undefined;
|
|
1337
|
-
this.usageHasDirectDefault = false;
|
|
1338
|
-
this.usageError = undefined;
|
|
1339
|
-
this.lastLoginError = undefined;
|
|
1340
1468
|
}
|
|
1341
1469
|
async status(refresh, callbackUrl) {
|
|
1470
|
+
let document;
|
|
1342
1471
|
let credential;
|
|
1343
1472
|
let credentialError;
|
|
1344
1473
|
try {
|
|
1345
|
-
|
|
1474
|
+
document = (await readCredentialDocument(this.filename))?.document;
|
|
1475
|
+
credential = currentCredential(document);
|
|
1346
1476
|
}
|
|
1347
1477
|
catch (error) {
|
|
1348
1478
|
credentialError = messageOf(error);
|
|
@@ -1350,7 +1480,8 @@ export class OpenAICodexAuth extends Service {
|
|
|
1350
1480
|
if (credential !== undefined) {
|
|
1351
1481
|
try {
|
|
1352
1482
|
await this.bearerToken();
|
|
1353
|
-
|
|
1483
|
+
document = (await readCredentialDocument(this.filename))?.document ?? document;
|
|
1484
|
+
credential = currentCredential(document) ?? credential;
|
|
1354
1485
|
}
|
|
1355
1486
|
catch (error) {
|
|
1356
1487
|
this.usageError = messageOf(error);
|
|
@@ -1379,6 +1510,13 @@ export class OpenAICodexAuth extends Service {
|
|
|
1379
1510
|
return {
|
|
1380
1511
|
route: resolveCodexRouteStatus(this.routeConfig, this.ctx.get('llm')),
|
|
1381
1512
|
loggedIn: credential !== undefined,
|
|
1513
|
+
currentAccountId: document?.currentAccountId ?? null,
|
|
1514
|
+
accounts: (document?.accounts ?? []).map(account => ({
|
|
1515
|
+
accountId: account.accountId,
|
|
1516
|
+
...account.email === undefined ? {} : { email: account.email },
|
|
1517
|
+
expiresAt: account.expires,
|
|
1518
|
+
current: account.accountId === document?.currentAccountId,
|
|
1519
|
+
})),
|
|
1382
1520
|
loginPending: startingMethod !== undefined || flow !== undefined,
|
|
1383
1521
|
...startingMethod !== undefined ? { loginMethod: startingMethod } : flow === undefined ? {} : { loginMethod: flow.kind },
|
|
1384
1522
|
...this.lastLoginError === undefined ? {} : { loginError: this.lastLoginError },
|
|
@@ -1388,6 +1526,7 @@ export class OpenAICodexAuth extends Service {
|
|
|
1388
1526
|
...browser === undefined ? {} : { browser },
|
|
1389
1527
|
...credential === undefined ? {} : {
|
|
1390
1528
|
accountId: credential.accountId,
|
|
1529
|
+
...credential.email === undefined ? {} : { email: credential.email },
|
|
1391
1530
|
expiresAt: credential.expires,
|
|
1392
1531
|
...this.usageCache === undefined || this.usageAccountId !== credential.accountId
|
|
1393
1532
|
? {} : { usage: this.usageCache },
|
|
@@ -1402,14 +1541,11 @@ export class OpenAICodexAuth extends Service {
|
|
|
1402
1541
|
};
|
|
1403
1542
|
}
|
|
1404
1543
|
async fetchUsage(credential) {
|
|
1405
|
-
const access = await this.bearerToken();
|
|
1406
|
-
if (access === undefined)
|
|
1407
|
-
throw new Error('OpenAI login is missing');
|
|
1408
1544
|
const response = await fetch(USAGE_URL, {
|
|
1409
1545
|
redirect: 'error',
|
|
1410
1546
|
headers: {
|
|
1411
1547
|
accept: 'application/json',
|
|
1412
|
-
authorization: `Bearer ${access}`,
|
|
1548
|
+
authorization: `Bearer ${credential.access}`,
|
|
1413
1549
|
'chatgpt-account-id': credential.accountId,
|
|
1414
1550
|
'user-agent': 'dsh-openai-codex-auth/0.5.0',
|
|
1415
1551
|
},
|
|
@@ -1418,8 +1554,8 @@ export class OpenAICodexAuth extends Service {
|
|
|
1418
1554
|
throw new Error(`Codex usage request failed (HTTP ${response.status})`);
|
|
1419
1555
|
return normalizeUsage(await response.json());
|
|
1420
1556
|
}
|
|
1421
|
-
write(
|
|
1422
|
-
return writeFileAtomic(this.filename, `${JSON.stringify(
|
|
1557
|
+
write(document) {
|
|
1558
|
+
return writeFileAtomic(this.filename, `${JSON.stringify(document, null, 2)}\n`, {
|
|
1423
1559
|
mode: 0o600, dirMode: 0o700,
|
|
1424
1560
|
});
|
|
1425
1561
|
}
|
|
@@ -1642,6 +1778,54 @@ export class OpenAICodexAuth extends Service {
|
|
|
1642
1778
|
this.sendJson(res, 500, { error: messageOf(error) });
|
|
1643
1779
|
}
|
|
1644
1780
|
}
|
|
1781
|
+
async accountIdBody(req) {
|
|
1782
|
+
const body = await readJsonBody(req);
|
|
1783
|
+
if (Object.keys(body).length !== 1 || typeof body.accountId !== 'string'
|
|
1784
|
+
|| body.accountId.length === 0 || body.accountId.length > 512) {
|
|
1785
|
+
throw new Error('The accountId field must identify one OpenAI Codex account.');
|
|
1786
|
+
}
|
|
1787
|
+
return body.accountId;
|
|
1788
|
+
}
|
|
1789
|
+
async handleSetCurrentAccount(req, res) {
|
|
1790
|
+
if (!this.trustedManagementRequest(req, res))
|
|
1791
|
+
return;
|
|
1792
|
+
if (req.method !== 'POST') {
|
|
1793
|
+
this.sendJson(res, 405, { error: 'POST only' }, { allow: 'POST' });
|
|
1794
|
+
return;
|
|
1795
|
+
}
|
|
1796
|
+
if (!this.requireCsrf(req, res))
|
|
1797
|
+
return;
|
|
1798
|
+
try {
|
|
1799
|
+
await this.setCurrentAccount(await this.accountIdBody(req));
|
|
1800
|
+
this.sendJson(res, 200, { ok: true });
|
|
1801
|
+
}
|
|
1802
|
+
catch (error) {
|
|
1803
|
+
const status = error instanceof AccountNotFoundError
|
|
1804
|
+
? 404
|
|
1805
|
+
: error instanceof CredentialNotWritableError ? 409 : 400;
|
|
1806
|
+
this.sendJson(res, status, { error: messageOf(error) });
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
async handleAccountLogout(req, res) {
|
|
1810
|
+
if (!this.trustedManagementRequest(req, res))
|
|
1811
|
+
return;
|
|
1812
|
+
if (req.method !== 'POST') {
|
|
1813
|
+
this.sendJson(res, 405, { error: 'POST only' }, { allow: 'POST' });
|
|
1814
|
+
return;
|
|
1815
|
+
}
|
|
1816
|
+
if (!this.requireCsrf(req, res))
|
|
1817
|
+
return;
|
|
1818
|
+
try {
|
|
1819
|
+
await this.logout(await this.accountIdBody(req));
|
|
1820
|
+
this.sendJson(res, 200, { ok: true });
|
|
1821
|
+
}
|
|
1822
|
+
catch (error) {
|
|
1823
|
+
const status = error instanceof AccountNotFoundError
|
|
1824
|
+
? 404
|
|
1825
|
+
: error instanceof CredentialNotWritableError ? 409 : 400;
|
|
1826
|
+
this.sendJson(res, status, { error: messageOf(error) });
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1645
1829
|
async handleLogout(req, res) {
|
|
1646
1830
|
if (!this.trustedManagementRequest(req, res))
|
|
1647
1831
|
return;
|
|
@@ -1656,7 +1840,7 @@ export class OpenAICodexAuth extends Service {
|
|
|
1656
1840
|
this.sendJson(res, 200, { ok: true });
|
|
1657
1841
|
}
|
|
1658
1842
|
catch (error) {
|
|
1659
|
-
this.sendJson(res, 500, { error: messageOf(error) });
|
|
1843
|
+
this.sendJson(res, error instanceof CredentialNotWritableError ? 409 : 500, { error: messageOf(error) });
|
|
1660
1844
|
}
|
|
1661
1845
|
}
|
|
1662
1846
|
}
|
package/lib/native-adapter.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ export interface NativeCodexTransportMode {
|
|
|
24
24
|
turnState?: string;
|
|
25
25
|
/** @internal Receives a newly observed bounded turn-state token. */
|
|
26
26
|
captureTurnState?: (state: string) => void;
|
|
27
|
+
/** @internal Pins WebSocket reconnects and HTTP fallback to one account. */
|
|
28
|
+
pinnedAccountId?: string;
|
|
27
29
|
}
|
|
28
30
|
export interface NativeCodexTransport {
|
|
29
31
|
stream(options: GenerateOptions, mode?: NativeCodexTransportMode): AsyncIterable<StreamChunk>;
|
package/lib/native-http.js
CHANGED
|
@@ -436,6 +436,7 @@ export class NativeCodexHttpTransport {
|
|
|
436
436
|
let transientRetries = 0;
|
|
437
437
|
let connectionRetryDelayMs = INITIAL_CONNECTION_RETRY_DELAY_MS;
|
|
438
438
|
let recovered = false;
|
|
439
|
+
let pinnedAccountId = mode.pinnedAccountId;
|
|
439
440
|
while (true) {
|
|
440
441
|
throwIfAborted(generation.signal);
|
|
441
442
|
const watchdog = attemptWatchdog(generation.signal, this.requestTimeoutMs, this.idleTimeoutMs);
|
|
@@ -445,6 +446,11 @@ export class NativeCodexHttpTransport {
|
|
|
445
446
|
try {
|
|
446
447
|
credential = await this.options.resolveCredential(watchdog.signal);
|
|
447
448
|
throwIfAborted(watchdog.signal);
|
|
449
|
+
if (pinnedAccountId === undefined)
|
|
450
|
+
pinnedAccountId = credential.accountId;
|
|
451
|
+
else if (credential.accountId !== pinnedAccountId) {
|
|
452
|
+
throw fixedFailure('native Codex account changed during request', 'AUTH');
|
|
453
|
+
}
|
|
448
454
|
if (mode.serviceTier !== undefined
|
|
449
455
|
&& (mode.authorityHash === undefined
|
|
450
456
|
|| nativeCodexAuthorityHash(credential.accountId) !== mode.authorityHash)) {
|
|
@@ -22,6 +22,8 @@ export interface NativeCodexWebSocketConnectOptions {
|
|
|
22
22
|
export interface NativeCodexWebSocketFactory {
|
|
23
23
|
connect(options: NativeCodexWebSocketConnectOptions): Promise<NativeCodexWebSocket>;
|
|
24
24
|
}
|
|
25
|
+
/** Resolve the same opt-in environment proxy contract used by Node fetch. */
|
|
26
|
+
export declare function nativeCodexWebSocketProxy(endpoint: string): string | undefined;
|
|
25
27
|
export declare function nativeCodexWebSocketUrl(endpoint: string): string;
|
|
26
28
|
export declare class NodeNativeCodexWebSocketFactory implements NativeCodexWebSocketFactory {
|
|
27
29
|
connect(options: NativeCodexWebSocketConnectOptions): Promise<NativeCodexWebSocket>;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** Bounded Node WebSocket client seam with injectable deterministic factories. */
|
|
2
2
|
import { LlmError, ProviderRequestId } from '@deepseek-ai/dsh-llm';
|
|
3
|
+
import { HttpsProxyAgent } from 'https-proxy-agent';
|
|
4
|
+
import { getProxyForUrl } from 'proxy-from-env';
|
|
3
5
|
import { nativeCodexEndpoint } from './endpoint.js';
|
|
4
6
|
import { NATIVE_CODEX_CONNECTION_FAILED_CODE, isNativeCodexConnectionFailure, } from './native-adapter.js';
|
|
5
7
|
import WebSocket from 'ws';
|
|
@@ -34,6 +36,13 @@ function positive(value, fallback, label) {
|
|
|
34
36
|
throw failure(`native Codex ${label} is invalid`, 'INVALID_ARGS');
|
|
35
37
|
return value;
|
|
36
38
|
}
|
|
39
|
+
/** Resolve the same opt-in environment proxy contract used by Node fetch. */
|
|
40
|
+
export function nativeCodexWebSocketProxy(endpoint) {
|
|
41
|
+
if (process.env.NODE_USE_ENV_PROXY !== '1')
|
|
42
|
+
return undefined;
|
|
43
|
+
const proxy = getProxyForUrl(nativeCodexEndpoint(endpoint, true).toString());
|
|
44
|
+
return proxy.length === 0 ? undefined : proxy;
|
|
45
|
+
}
|
|
37
46
|
export function nativeCodexWebSocketUrl(endpoint) {
|
|
38
47
|
const url = nativeCodexEndpoint(endpoint, true);
|
|
39
48
|
if (url.protocol === 'https:')
|
|
@@ -165,8 +174,11 @@ export class NodeNativeCodexWebSocketFactory {
|
|
|
165
174
|
throw abortFailure(options.signal);
|
|
166
175
|
return new Promise((resolve, reject) => {
|
|
167
176
|
const responseHeaders = {};
|
|
177
|
+
const proxy = nativeCodexWebSocketProxy(options.url);
|
|
178
|
+
const agent = proxy === undefined ? undefined : new HttpsProxyAgent(proxy);
|
|
168
179
|
const socket = new WebSocket(nativeCodexWebSocketUrl(options.url), {
|
|
169
180
|
headers: options.headers,
|
|
181
|
+
...(agent === undefined ? {} : { agent }),
|
|
170
182
|
handshakeTimeout: timeout,
|
|
171
183
|
maxPayload: maximum,
|
|
172
184
|
perMessageDeflate: true,
|
package/lib/native-websocket.js
CHANGED
|
@@ -29,6 +29,7 @@ function reconnectable(code) {
|
|
|
29
29
|
return [
|
|
30
30
|
'WS_RETRYABLE', 'WS_RETRYABLE_RESET', 'WS_PROTOCOL_ERROR',
|
|
31
31
|
'WS_FRAME_TOO_LARGE', 'WS_RESPONSE_TOO_LARGE', 'TIMEOUT',
|
|
32
|
+
NATIVE_CODEX_CONNECTION_FAILED_CODE,
|
|
32
33
|
].includes(code);
|
|
33
34
|
}
|
|
34
35
|
function failedStepRetryable(code) {
|
|
@@ -499,10 +500,12 @@ export class NativeCodexWebSocketTransport {
|
|
|
499
500
|
if (entry.turnState === undefined)
|
|
500
501
|
entry.turnState = state;
|
|
501
502
|
},
|
|
503
|
+
...(pinnedAccountId === undefined ? {} : { pinnedAccountId }),
|
|
502
504
|
});
|
|
503
505
|
let reconnects = 0;
|
|
504
506
|
let connectionRetryDelayMs = INITIAL_CONNECTION_RETRY_DELAY_MS;
|
|
505
507
|
let recovered = false;
|
|
508
|
+
let pinnedAccountId = mode.pinnedAccountId;
|
|
506
509
|
let requestCompleted = false;
|
|
507
510
|
try {
|
|
508
511
|
if (entry.disabled) {
|
|
@@ -517,6 +520,11 @@ export class NativeCodexWebSocketTransport {
|
|
|
517
520
|
try {
|
|
518
521
|
const credential = await this.options.resolveCredential(signal);
|
|
519
522
|
attemptedCredential = credential;
|
|
523
|
+
if (pinnedAccountId === undefined)
|
|
524
|
+
pinnedAccountId = credential.accountId;
|
|
525
|
+
else if (credential.accountId !== pinnedAccountId) {
|
|
526
|
+
throw failure('native Codex account changed during request', 'AUTH');
|
|
527
|
+
}
|
|
520
528
|
this.assertFastAuthority(credential, mode);
|
|
521
529
|
for await (const chunk of this.attempt(entry, prepared, credential, signal)) {
|
|
522
530
|
emitted = true;
|
|
@@ -538,9 +546,12 @@ export class NativeCodexWebSocketTransport {
|
|
|
538
546
|
throw failedStepRetry(failureValue);
|
|
539
547
|
throw failureValue;
|
|
540
548
|
}
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
549
|
+
// Credential resolution may need the network itself, so wait until it recovers.
|
|
550
|
+
// Once a credential is available, however, a WebSocket-only outage must consume
|
|
551
|
+
// the bounded reconnect budget and then fall back to HTTP/SSE instead of leaving
|
|
552
|
+
// the DSH turn in an invisible infinite reconnect loop.
|
|
553
|
+
if (attemptedCredential === undefined
|
|
554
|
+
&& isNativeCodexConnectionFailure(failureValue)) {
|
|
544
555
|
await this.waitForConnection(connectionRetryDelayMs, signal);
|
|
545
556
|
connectionRetryDelayMs = Math.min(connectionRetryDelayMs * 2, MAX_CONNECTION_RETRY_DELAY_MS);
|
|
546
557
|
continue;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pure01fx/dsh-openai-codex-auth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Native ChatGPT Codex provider, device-code-first login, and same-origin Web integration for DeepSeek Harness",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -84,6 +84,7 @@
|
|
|
84
84
|
"client": {
|
|
85
85
|
"inject": [
|
|
86
86
|
"@deepseek-ai/dsh-client-runtime",
|
|
87
|
+
"@deepseek-ai/dsh-client-ui-model-selection",
|
|
87
88
|
"@deepseek-ai/dsh-client-ui-conversation",
|
|
88
89
|
"@deepseek-ai/dsh-client-ui-settings"
|
|
89
90
|
],
|
|
@@ -100,6 +101,8 @@
|
|
|
100
101
|
"@deepseek-ai/dsh-atomic-write": "0.1.1-rc.2",
|
|
101
102
|
"@deepseek-ai/dsh-home-paths": "0.1.1-rc.2",
|
|
102
103
|
"@deepseek-ai/schemastery": "^3.18.1",
|
|
104
|
+
"https-proxy-agent": "7.0.6",
|
|
105
|
+
"proxy-from-env": "1.1.0",
|
|
103
106
|
"ws": "8.21.3"
|
|
104
107
|
},
|
|
105
108
|
"devDependencies": {
|
|
@@ -110,6 +113,7 @@
|
|
|
110
113
|
"@deepseek-ai/dsh-llm": "0.1.1-rc.2",
|
|
111
114
|
"@deepseek-ai/dsh-session": "0.1.1-rc.2",
|
|
112
115
|
"@types/node": "^22.20.0",
|
|
116
|
+
"@types/proxy-from-env": "1.0.4",
|
|
113
117
|
"typescript": "^6.0.3",
|
|
114
118
|
"vitest": "^4.1.8"
|
|
115
119
|
}
|