dsh-codex-oauth 0.1.1 → 0.2.1
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 +48 -23
- package/README.zh.md +42 -36
- package/SPEC.md +73 -0
- package/bin/dsh-openai-codex.js +52 -0
- package/client/index.jsx +49 -0
- package/lib/auth.js +21 -0
- package/lib/client.js +14 -0
- package/lib/index.js +6 -3
- package/lib/store.js +30 -35
- package/lib/web-auth.js +62 -0
- package/package.json +12 -5
package/README.md
CHANGED
|
@@ -2,47 +2,72 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) | [中文](README.zh.md)
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
`~/.pi/agent/auth.json` → `openai-codex`.
|
|
5
|
+
A standalone [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) plugin for using ChatGPT Plus/Pro Codex models through OpenAI's Codex OAuth flow. Pi is **not** required.
|
|
7
6
|
|
|
8
|
-
It uses
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
token into an API-key environment variable.
|
|
7
|
+
It uses the official `openaiCodexProvider` from `@earendil-works/pi-ai` and DSH's public `dsh-llm-pi-ai` adapter. It supports streaming, tool calls, reasoning, images, replay, compaction, automatic token refresh, and the Codex model catalog.
|
|
8
|
+
|
|
9
|
+
This is the ChatGPT Codex backend, not a general OpenAI Platform API-key adapter.
|
|
12
10
|
|
|
13
11
|
## Install
|
|
14
12
|
|
|
15
13
|
```bash
|
|
16
14
|
dsh plugin --profile web add dsh-codex-oauth
|
|
15
|
+
dsh web
|
|
17
16
|
```
|
|
18
17
|
|
|
19
|
-
For
|
|
20
|
-
entry in its `cordis.patch.yml`:
|
|
18
|
+
For an `npx` installation:
|
|
21
19
|
|
|
22
|
-
```
|
|
23
|
-
-
|
|
24
|
-
|
|
25
|
-
name: dsh-codex-oauth
|
|
20
|
+
```bash
|
|
21
|
+
npx @deepseek-ai/dsh plugin --profile web add dsh-codex-oauth
|
|
22
|
+
npx @deepseek-ai/dsh web
|
|
26
23
|
```
|
|
27
24
|
|
|
28
|
-
|
|
25
|
+
## Sign in
|
|
26
|
+
|
|
27
|
+
The plugin owns its own credential file and login flow. No Pi installation or token copying is needed.
|
|
28
|
+
|
|
29
|
+
### Web UI
|
|
30
|
+
|
|
31
|
+
Open **Settings → OpenAI Codex** and click **Sign in with ChatGPT**. A browser window opens for the OAuth approval; after it completes, return to DSH and select `openai-codex` in the model picker.
|
|
32
|
+
|
|
33
|
+
### CLI
|
|
29
34
|
|
|
30
35
|
```bash
|
|
31
|
-
|
|
36
|
+
dsh plugin --profile web exec dsh-openai-codex login
|
|
37
|
+
# headless machines:
|
|
38
|
+
dsh plugin --profile web exec dsh-openai-codex login --device-code
|
|
39
|
+
|
|
40
|
+
dsh plugin --profile web exec dsh-openai-codex status
|
|
41
|
+
dsh plugin --profile web exec dsh-openai-codex logout
|
|
32
42
|
```
|
|
33
43
|
|
|
34
|
-
|
|
44
|
+
The browser login opens OpenAI's authorization page and completes through a localhost callback. `--device-code` is the fallback for machines without a usable browser.
|
|
45
|
+
|
|
46
|
+
After signing in, choose `openai-codex` in the DSH model picker.
|
|
47
|
+
|
|
48
|
+
### Credentials
|
|
49
|
+
|
|
50
|
+
Credentials are stored at:
|
|
51
|
+
|
|
52
|
+
```text
|
|
53
|
+
$DSH_HOME/.openai-codex-auth.json
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The default is `~/.dsh/.openai-codex-auth.json`. The file is written atomically with owner-only permissions, and refresh/login/logout operations use a cross-process file lock. The plugin never reads or modifies `~/.pi/agent/auth.json` or `~/.codex/auth.json`.
|
|
57
|
+
|
|
58
|
+
## Development
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npm test
|
|
62
|
+
```
|
|
35
63
|
|
|
36
|
-
|
|
64
|
+
The test requires the DSH and `pi-ai` peer dependencies supplied by a DSH profile.
|
|
37
65
|
|
|
38
|
-
|
|
39
|
-
reasoning replay, and compaction to DSH's public `dsh-llm-pi-ai` adapter. It only
|
|
40
|
-
bridges the credential store to Pi's existing OAuth document.
|
|
66
|
+
## Limitations
|
|
41
67
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
credential file and its own login flow to remove that race.
|
|
68
|
+
- ChatGPT plan eligibility, model access, quotas, and backend behavior are controlled by OpenAI and may change.
|
|
69
|
+
- Codex endpoints are not public OpenAI Platform API endpoints.
|
|
70
|
+
- Filesystem, shell, MCP, permissions, attachments, and other DSH capabilities come from the active profile.
|
|
46
71
|
|
|
47
72
|
## License
|
|
48
73
|
|
package/README.zh.md
CHANGED
|
@@ -2,71 +2,71 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) | 中文
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
复用 Pi 已登录的 ChatGPT Plus/Pro Codex OAuth,在不使用 OpenAI Platform API Key 的情况下调用 `openai-codex` 模型。
|
|
5
|
+
一个独立的 [DeepSeek Harness(dsh)](https://github.com/deepseek-ai/deepseek-harness) 插件,通过 OpenAI Codex OAuth 使用 ChatGPT Plus/Pro 订阅模型。
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
**不需要安装 Pi。** 插件使用 `@earendil-works/pi-ai` 提供的 Codex provider,以及 DSH 公共的 `dsh-llm-pi-ai` 适配器,支持流式输出、工具调用、推理、图片、replay、上下文压缩、OAuth 自动刷新和 Codex 模型目录。
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
- 复用 `~/.pi/agent/auth.json` 中的 `openai-codex` OAuth 凭据;
|
|
13
|
-
- access token 过期时自动使用 refresh token 更新;
|
|
14
|
-
- 使用 Pi 同款 Codex 模型目录;
|
|
15
|
-
- 支持流式输出、工具调用、推理、图片输入、replay 和上下文压缩;
|
|
16
|
-
- 不读取或打印 API Key,也不会把 token 放进环境变量。
|
|
9
|
+
这是 ChatGPT Codex 后端接入,不是普通的 OpenAI Platform API Key 适配器。
|
|
17
10
|
|
|
18
11
|
## 安装
|
|
19
12
|
|
|
20
|
-
将插件安装到 dsh 的 `web` profile:
|
|
21
|
-
|
|
22
13
|
```bash
|
|
23
14
|
dsh plugin --profile web add dsh-codex-oauth
|
|
24
15
|
dsh web
|
|
25
16
|
```
|
|
26
17
|
|
|
27
|
-
|
|
18
|
+
如果使用 `npx`:
|
|
28
19
|
|
|
29
20
|
```bash
|
|
30
21
|
npx @deepseek-ai/dsh plugin --profile web add dsh-codex-oauth
|
|
31
22
|
npx @deepseek-ai/dsh web
|
|
32
23
|
```
|
|
33
24
|
|
|
34
|
-
|
|
25
|
+
## 登录
|
|
35
26
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
27
|
+
插件拥有独立的 OAuth 登录流程和凭据文件。新用户不需要安装 Pi,也不需要复制任何 token。
|
|
28
|
+
|
|
29
|
+
### Web UI
|
|
30
|
+
|
|
31
|
+
打开 **设置 → OpenAI Codex**,点击 **使用 ChatGPT 登录**。插件会打开浏览器完成 OAuth 授权;返回 DSH 后,在模型选择器中选择 `openai-codex`。
|
|
39
32
|
|
|
40
|
-
|
|
33
|
+
### 命令行登录
|
|
41
34
|
|
|
42
|
-
```
|
|
43
|
-
-
|
|
44
|
-
- id: llm-codex-oauth
|
|
45
|
-
name: dsh-codex-oauth
|
|
35
|
+
```bash
|
|
36
|
+
dsh plugin --profile web exec dsh-openai-codex login
|
|
46
37
|
```
|
|
47
38
|
|
|
48
|
-
|
|
39
|
+
无图形浏览器的机器:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
dsh plugin --profile web exec dsh-openai-codex login --device-code
|
|
43
|
+
```
|
|
49
44
|
|
|
50
|
-
|
|
45
|
+
查看状态和退出登录:
|
|
51
46
|
|
|
52
|
-
```
|
|
53
|
-
|
|
47
|
+
```bash
|
|
48
|
+
dsh plugin --profile web exec dsh-openai-codex status
|
|
49
|
+
dsh plugin --profile web exec dsh-openai-codex logout
|
|
54
50
|
```
|
|
55
51
|
|
|
56
|
-
|
|
52
|
+
浏览器登录会打开 OpenAI 授权页面,并通过 localhost 回调完成。登录后,在 DSH 模型选择器中选择 `openai-codex`。
|
|
57
53
|
|
|
58
|
-
|
|
54
|
+
### 凭据存储
|
|
59
55
|
|
|
60
|
-
|
|
56
|
+
凭据默认存储在:
|
|
61
57
|
|
|
62
|
-
|
|
58
|
+
```text
|
|
59
|
+
~/.dsh/.openai-codex-auth.json
|
|
60
|
+
```
|
|
63
61
|
|
|
64
|
-
|
|
62
|
+
也可以通过 `DSH_HOME` 改变目录。文件采用原子写入、仅所有者可读写(`0600`),登录、刷新和退出登录使用跨进程文件锁。
|
|
65
63
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
64
|
+
插件**不会**读取或修改:
|
|
65
|
+
|
|
66
|
+
```text
|
|
67
|
+
~/.pi/agent/auth.json
|
|
68
|
+
~/.codex/auth.json
|
|
69
|
+
```
|
|
70
70
|
|
|
71
71
|
## 开发与测试
|
|
72
72
|
|
|
@@ -74,7 +74,13 @@ Pi 必须已经完成 OpenAI Codex 登录,并且存在:
|
|
|
74
74
|
npm test
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
测试需要当前 DSH profile 提供 DSH 和 `pi-ai` peer dependencies。
|
|
78
|
+
|
|
79
|
+
## 限制
|
|
80
|
+
|
|
81
|
+
- ChatGPT 套餐资格、模型权限、配额和后端行为由 OpenAI 控制,可能变化;
|
|
82
|
+
- Codex 端点不是公开的 OpenAI Platform API 端点;
|
|
83
|
+
- 文件系统、Shell、MCP、权限、附件等能力仍由当前 DSH profile 提供。
|
|
78
84
|
|
|
79
85
|
## 许可证
|
|
80
86
|
|
package/SPEC.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# dsh-codex-oauth 独立登录规格
|
|
2
|
+
|
|
3
|
+
## 目标
|
|
4
|
+
|
|
5
|
+
让新用户只安装 DSH 插件即可登录并使用 ChatGPT Codex 订阅,不要求安装 Pi,也不读取或修改 `~/.pi/agent/auth.json`。
|
|
6
|
+
|
|
7
|
+
## 用户流程
|
|
8
|
+
|
|
9
|
+
### CLI(第一版)
|
|
10
|
+
|
|
11
|
+
1. 安装插件并重启 DSH;
|
|
12
|
+
2. 执行 `dsh-openai-codex login`;
|
|
13
|
+
3. 插件打开 OpenAI OAuth 授权页;
|
|
14
|
+
4. 用户在浏览器中批准;
|
|
15
|
+
5. localhost 回调完成登录;
|
|
16
|
+
6. DSH 使用 `openai-codex` 模型。
|
|
17
|
+
|
|
18
|
+
无图形浏览器时使用 `login --device-code`。
|
|
19
|
+
|
|
20
|
+
### Web
|
|
21
|
+
|
|
22
|
+
在 DSH 设置页提供登录状态、浏览器 OAuth 登录和退出登录按钮。
|
|
23
|
+
|
|
24
|
+
### 命令
|
|
25
|
+
|
|
26
|
+
提供:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
dsh plugin --profile web exec dsh-openai-codex login
|
|
30
|
+
dsh plugin --profile web exec dsh-openai-codex status
|
|
31
|
+
dsh plugin --profile web exec dsh-openai-codex logout
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
无图形浏览器时提供 `login --device-code`。
|
|
35
|
+
|
|
36
|
+
## 凭据
|
|
37
|
+
|
|
38
|
+
- 存储位置:`$DSH_HOME/.openai-codex-auth.json`,默认 `~/.dsh/.openai-codex-auth.json`;
|
|
39
|
+
- 格式:版本化 JSON,字段为 `version` 和 `credential`;
|
|
40
|
+
- `credential` 保留 `type`, `access`, `refresh`, `expires`, `accountId`;
|
|
41
|
+
- 原子写入,文件权限 `0600`,目录权限 `0700`;
|
|
42
|
+
- 刷新、登录、登出使用跨进程文件锁;
|
|
43
|
+
- 不读取、复制、修改 `~/.pi/agent/auth.json` 或 `~/.codex/auth.json`。
|
|
44
|
+
|
|
45
|
+
## API 与模型
|
|
46
|
+
|
|
47
|
+
- 使用 `@earendil-works/pi-ai` 的 `openaiCodexProvider`;
|
|
48
|
+
- 使用 DSH 公共 `PiAiAdapter`;
|
|
49
|
+
- 使用 Codex Responses endpoint,不伪装成普通 OpenAI Platform API;
|
|
50
|
+
- 保留流式输出、工具调用、推理、图片、replay 和 compaction;
|
|
51
|
+
- 新建 agent 默认模型沿用 Codex catalog 中的可用模型,不硬编码不存在的模型。
|
|
52
|
+
|
|
53
|
+
## 安全边界
|
|
54
|
+
|
|
55
|
+
- OAuth 密码、授权码、access token、refresh token 不进入日志、session、Git 或错误信息;
|
|
56
|
+
- Web 登录回调只接受 localhost 同源请求,并返回 `no-store`;
|
|
57
|
+
- 插件移除不自动删除凭据;登出必须由用户明确执行;
|
|
58
|
+
- 不实现普通 OpenAI API Key 兼容层。
|
|
59
|
+
|
|
60
|
+
## 明确不做
|
|
61
|
+
|
|
62
|
+
- 不依赖 Pi;
|
|
63
|
+
- 不导入 Pi 的 auth 文件;
|
|
64
|
+
- 不修改 DSH 源码;
|
|
65
|
+
- 第一版不加入独立搜索、图片生成和额度面板;这些属于后续功能。
|
|
66
|
+
|
|
67
|
+
## 验收标准
|
|
68
|
+
|
|
69
|
+
- 干净环境安装插件后,`status` 返回 signed out;
|
|
70
|
+
- CLI OAuth 登录后,`status` 返回 signed in,且不打印 token;
|
|
71
|
+
- access token 过期后请求会自动刷新并持久化;
|
|
72
|
+
- 删除 Pi auth 文件不影响 DSH 插件;
|
|
73
|
+
- Web profile 能加载 `openai-codex`,且公共 DSH adapter 的图片、工具和 replay 测试继续通过。
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { createInterface } from "node:readline/promises";
|
|
4
|
+
import { authPath, login, logout, status } from "../lib/auth.js";
|
|
5
|
+
|
|
6
|
+
function openBrowser(raw) {
|
|
7
|
+
const url = new URL(raw);
|
|
8
|
+
if (url.protocol !== "https:") throw new Error("Refusing to open a non-HTTPS OAuth URL");
|
|
9
|
+
const command = process.platform === "darwin" ? ["open", url.href] : process.platform === "win32" ? ["rundll32.exe", "url.dll,FileProtocolHandler", url.href] : ["xdg-open", url.href];
|
|
10
|
+
const child = spawn(command[0], command.slice(1), { detached: true, stdio: "ignore", windowsHide: true });
|
|
11
|
+
child.on("error", () => {}); child.unref();
|
|
12
|
+
}
|
|
13
|
+
function safeMessage(error) {
|
|
14
|
+
return String(error?.message ?? error)
|
|
15
|
+
.replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/gu, "[redacted token]")
|
|
16
|
+
.replace(/(\b(?:code|token|refresh_token|access_token)=)[^&\s]+/giu, "$1[redacted]");
|
|
17
|
+
}
|
|
18
|
+
function help() {
|
|
19
|
+
console.log("Usage: dsh-openai-codex <login|logout|status> [--device-code]");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const [action, ...flags] = process.argv.slice(2);
|
|
23
|
+
if (!action || action === "--help" || action === "-h") { help(); process.exit(0); }
|
|
24
|
+
if (!["login", "logout", "status"].includes(action) || flags.some((flag) => flag !== "--device-code") || (action !== "login" && flags.length)) { help(); process.exit(1); }
|
|
25
|
+
try {
|
|
26
|
+
if (action === "status") {
|
|
27
|
+
const value = await status();
|
|
28
|
+
if (!value.authenticated) { console.log("OpenAI Codex for dsh: signed out"); process.exitCode = 1; }
|
|
29
|
+
else console.log(`OpenAI Codex for dsh: signed in; refresh is automatic`);
|
|
30
|
+
} else if (action === "logout") {
|
|
31
|
+
await logout(); console.log(`OpenAI Codex for dsh: signed out; removed ${authPath()}`);
|
|
32
|
+
} else {
|
|
33
|
+
const readline = createInterface({ input: process.stdin, output: process.stdout });
|
|
34
|
+
try {
|
|
35
|
+
await login(undefined, {
|
|
36
|
+
prompt: async (prompt) => {
|
|
37
|
+
if (prompt.type === "select") return flags.includes("--device-code") ? "device_code" : "browser";
|
|
38
|
+
return readline.question(`${prompt.message}: `, { signal: prompt.signal });
|
|
39
|
+
},
|
|
40
|
+
notify: (event) => {
|
|
41
|
+
if (event.type === "auth_url") { console.log(`Open this URL to sign in:\n${event.url}`); openBrowser(event.url); }
|
|
42
|
+
else if (event.type === "device_code") { console.log(`Open ${event.verificationUri} and enter ${event.userCode}`); openBrowser(event.verificationUri); }
|
|
43
|
+
else if (event.message) console.log(event.message);
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
} finally { readline.close(); }
|
|
47
|
+
console.log(`OpenAI Codex for dsh: signed in; credentials saved to ${authPath()}`);
|
|
48
|
+
}
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error(`dsh-openai-codex: ${safeMessage(error)}`);
|
|
51
|
+
process.exitCode = 1;
|
|
52
|
+
}
|
package/client/index.jsx
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
const STATUS = "/plugins/dsh-codex-oauth/auth/status";
|
|
4
|
+
const LOGIN = "/plugins/dsh-codex-oauth/auth/login";
|
|
5
|
+
const LOGOUT = "/plugins/dsh-codex-oauth/auth/logout";
|
|
6
|
+
const h = React.createElement;
|
|
7
|
+
|
|
8
|
+
async function request(path, method = "GET") {
|
|
9
|
+
const response = await fetch(path, { method, credentials: "same-origin", headers: { accept: "application/json" } });
|
|
10
|
+
const value = await response.json().catch(() => ({}));
|
|
11
|
+
if (!response.ok) throw new Error(value.error || `HTTP ${response.status}`);
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function CodexSettings() {
|
|
16
|
+
const [state, setState] = useState({ status: "loading" });
|
|
17
|
+
const [busy, setBusy] = useState(false);
|
|
18
|
+
const refresh = useCallback(() => request(STATUS).then(setState).catch((error) => setState({ status: "error", message: error.message })), []);
|
|
19
|
+
useEffect(() => { refresh(); }, [refresh]);
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (state.status !== "signing-in") return undefined;
|
|
22
|
+
const timer = setInterval(refresh, 1000);
|
|
23
|
+
return () => clearInterval(timer);
|
|
24
|
+
}, [refresh, state.status]);
|
|
25
|
+
const signIn = async () => {
|
|
26
|
+
setBusy(true); setState({ status: "signing-in" });
|
|
27
|
+
const popup = window.open("about:blank", "_blank", "noopener,noreferrer");
|
|
28
|
+
try { const challenge = await request(LOGIN, "POST"); if (!popup) throw new Error("Please allow popups to sign in"); popup.location.replace(challenge.url); }
|
|
29
|
+
catch (error) { popup?.close(); setState({ status: "error", message: error.message }); }
|
|
30
|
+
finally { setBusy(false); }
|
|
31
|
+
};
|
|
32
|
+
const signOut = async () => { setBusy(true); try { await request(LOGOUT, "POST"); setState({ status: "signed-out" }); } catch (error) { setState({ status: "error", message: error.message }); } finally { setBusy(false); } };
|
|
33
|
+
const signedIn = state.status === "signed-in";
|
|
34
|
+
const label = state.status === "signed-in" ? "已登录" : state.status === "signing-in" ? "等待浏览器完成登录…" : state.status === "loading" ? "检查登录状态…" : state.status === "error" ? "登录失败" : "未登录";
|
|
35
|
+
return h("section", { style: { display: "flex", flexDirection: "column", gap: 12, maxWidth: 640 } },
|
|
36
|
+
h("h2", { style: { margin: 0 } }, "OpenAI Codex"),
|
|
37
|
+
h("p", { style: { margin: 0, color: "var(--dsw-alias-label-secondary)" } }, "使用 ChatGPT Plus/Pro 的 Codex 订阅,无需 OpenAI API Key。"),
|
|
38
|
+
h("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", padding: 16, border: "1px solid var(--dsw-alias-border-l2)", borderRadius: 10 } },
|
|
39
|
+
h("span", null, label),
|
|
40
|
+
state.status === "loading" ? null : h("button", { type: "button", disabled: busy || state.status === "signing-in", onClick: signedIn ? signOut : signIn }, signedIn ? "退出登录" : "使用 ChatGPT 登录")),
|
|
41
|
+
state.status === "error" ? h("p", { style: { color: "var(--dsw-alias-state-error-primary)" } }, state.message) : null,
|
|
42
|
+
h("p", { style: { margin: 0, color: "var(--dsw-alias-label-secondary)", fontSize: 13 } }, "凭据保存在 DSH_HOME/.openai-codex-auth.json,不会读取 Pi 或 Codex CLI 的登录文件。"));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const name = "dsh-codex-oauth-client";
|
|
46
|
+
export const inject = ["slots"];
|
|
47
|
+
export function apply(ctx) {
|
|
48
|
+
ctx.slots.inject("settings.section", () => ctx.slots.register({ name: "settings.section", id: "dsh-codex-oauth", order: 15, label: () => "OpenAI Codex" }, CodexSettings));
|
|
49
|
+
}
|
package/lib/auth.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createModels } from "@earendil-works/pi-ai";
|
|
2
|
+
import { openaiCodexProvider } from "@earendil-works/pi-ai/providers/openai-codex";
|
|
3
|
+
import { CodexCredentialStore, OPENAI_CODEX_PROVIDER, authPath } from "./store.js";
|
|
4
|
+
|
|
5
|
+
function models(store) {
|
|
6
|
+
const value = createModels({ credentials: store });
|
|
7
|
+
value.setProvider(openaiCodexProvider());
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function login(store = new CodexCredentialStore(), interaction) {
|
|
12
|
+
await models(store).login(OPENAI_CODEX_PROVIDER, "oauth", interaction);
|
|
13
|
+
}
|
|
14
|
+
export async function logout(store = new CodexCredentialStore()) {
|
|
15
|
+
await store.delete(OPENAI_CODEX_PROVIDER);
|
|
16
|
+
}
|
|
17
|
+
export async function status(store = new CodexCredentialStore()) {
|
|
18
|
+
const credential = await store.read(OPENAI_CODEX_PROVIDER);
|
|
19
|
+
return credential?.type === "oauth" ? { authenticated: true, expiresAt: new Date(credential.expires) } : { authenticated: false };
|
|
20
|
+
}
|
|
21
|
+
export { authPath };
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
var G=Object.create;var k=Object.defineProperty;var H=Object.getOwnPropertyDescriptor;var M=Object.getOwnPropertyNames;var z=Object.getPrototypeOf,B=Object.prototype.hasOwnProperty;var O=(e,t)=>()=>{try{return t||e((t={exports:{}}).exports,t),t.exports}catch(r){throw t=0,r}};var W=(e,t,r,u)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of M(t))!B.call(e,n)&&n!==r&&k(e,n,{get:()=>t[n],enumerable:!(u=H(t,n))||u.enumerable});return e};var K=(e,t,r)=>(r=e!=null?G(z(e)):{},W(t||!e||!e.__esModule?k(r,"default",{value:e,enumerable:!0}):r,e));var V=O(o=>{"use strict";var h=Symbol.for("react.element"),J=Symbol.for("react.portal"),Q=Symbol.for("react.fragment"),X=Symbol.for("react.strict_mode"),Y=Symbol.for("react.profiler"),Z=Symbol.for("react.provider"),ee=Symbol.for("react.context"),te=Symbol.for("react.forward_ref"),re=Symbol.for("react.suspense"),ne=Symbol.for("react.memo"),oe=Symbol.for("react.lazy"),R=Symbol.iterator;function ue(e){return e===null||typeof e!="object"?null:(e=R&&e[R]||e["@@iterator"],typeof e=="function"?e:null)}var P={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},I=Object.assign,T={};function y(e,t,r){this.props=e,this.context=t,this.refs=T,this.updater=r||P}y.prototype.isReactComponent={};y.prototype.setState=function(e,t){if(typeof e!="object"&&typeof e!="function"&&e!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")};y.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")};function A(){}A.prototype=y.prototype;function w(e,t,r){this.props=e,this.context=t,this.refs=T,this.updater=r||P}var b=w.prototype=new A;b.constructor=w;I(b,y.prototype);b.isPureReactComponent=!0;var j=Array.isArray,D=Object.prototype.hasOwnProperty,E={current:null},q={key:!0,ref:!0,__self:!0,__source:!0};function L(e,t,r){var u,n={},c=null,a=null;if(t!=null)for(u in t.ref!==void 0&&(a=t.ref),t.key!==void 0&&(c=""+t.key),t)D.call(t,u)&&!q.hasOwnProperty(u)&&(n[u]=t[u]);var s=arguments.length-2;if(s===1)n.children=r;else if(1<s){for(var i=Array(s),l=0;l<s;l++)i[l]=arguments[l+2];n.children=i}if(e&&e.defaultProps)for(u in s=e.defaultProps,s)n[u]===void 0&&(n[u]=s[u]);return{$$typeof:h,type:e,key:c,ref:a,props:n,_owner:E.current}}function se(e,t){return{$$typeof:h,type:e.type,key:t,ref:e.ref,props:e.props,_owner:e._owner}}function x(e){return typeof e=="object"&&e!==null&&e.$$typeof===h}function ie(e){var t={"=":"=0",":":"=2"};return"$"+e.replace(/[=:]/g,function(r){return t[r]})}var $=/\/+/g;function S(e,t){return typeof e=="object"&&e!==null&&e.key!=null?ie(""+e.key):t.toString(36)}function g(e,t,r,u,n){var c=typeof e;(c==="undefined"||c==="boolean")&&(e=null);var a=!1;if(e===null)a=!0;else switch(c){case"string":case"number":a=!0;break;case"object":switch(e.$$typeof){case h:case J:a=!0}}if(a)return a=e,n=n(a),e=u===""?"."+S(a,0):u,j(n)?(r="",e!=null&&(r=e.replace($,"$&/")+"/"),g(n,t,r,"",function(l){return l})):n!=null&&(x(n)&&(n=se(n,r+(!n.key||a&&a.key===n.key?"":(""+n.key).replace($,"$&/")+"/")+e)),t.push(n)),1;if(a=0,u=u===""?".":u+":",j(e))for(var s=0;s<e.length;s++){c=e[s];var i=u+S(c,s);a+=g(c,t,r,i,n)}else if(i=ue(e),typeof i=="function")for(e=i.call(e),s=0;!(c=e.next()).done;)c=c.value,i=u+S(c,s++),a+=g(c,t,r,i,n);else if(c==="object")throw t=String(e),Error("Objects are not valid as a React child (found: "+(t==="[object Object]"?"object with keys {"+Object.keys(e).join(", ")+"}":t)+"). If you meant to render a collection of children, use an array instead.");return a}function m(e,t,r){if(e==null)return e;var u=[],n=0;return g(e,u,"","",function(c){return t.call(r,c,n++)}),u}function ce(e){if(e._status===-1){var t=e._result;t=t(),t.then(function(r){(e._status===0||e._status===-1)&&(e._status=1,e._result=r)},function(r){(e._status===0||e._status===-1)&&(e._status=2,e._result=r)}),e._status===-1&&(e._status=0,e._result=t)}if(e._status===1)return e._result.default;throw e._result}var f={current:null},v={transition:null},ae={ReactCurrentDispatcher:f,ReactCurrentBatchConfig:v,ReactCurrentOwner:E};function U(){throw Error("act(...) is not supported in production builds of React.")}o.Children={map:m,forEach:function(e,t,r){m(e,function(){t.apply(this,arguments)},r)},count:function(e){var t=0;return m(e,function(){t++}),t},toArray:function(e){return m(e,function(t){return t})||[]},only:function(e){if(!x(e))throw Error("React.Children.only expected to receive a single React element child.");return e}};o.Component=y;o.Fragment=Q;o.Profiler=Y;o.PureComponent=w;o.StrictMode=X;o.Suspense=re;o.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED=ae;o.act=U;o.cloneElement=function(e,t,r){if(e==null)throw Error("React.cloneElement(...): The argument must be a React element, but you passed "+e+".");var u=I({},e.props),n=e.key,c=e.ref,a=e._owner;if(t!=null){if(t.ref!==void 0&&(c=t.ref,a=E.current),t.key!==void 0&&(n=""+t.key),e.type&&e.type.defaultProps)var s=e.type.defaultProps;for(i in t)D.call(t,i)&&!q.hasOwnProperty(i)&&(u[i]=t[i]===void 0&&s!==void 0?s[i]:t[i])}var i=arguments.length-2;if(i===1)u.children=r;else if(1<i){s=Array(i);for(var l=0;l<i;l++)s[l]=arguments[l+2];u.children=s}return{$$typeof:h,type:e.type,key:n,ref:c,props:u,_owner:a}};o.createContext=function(e){return e={$$typeof:ee,_currentValue:e,_currentValue2:e,_threadCount:0,Provider:null,Consumer:null,_defaultValue:null,_globalName:null},e.Provider={$$typeof:Z,_context:e},e.Consumer=e};o.createElement=L;o.createFactory=function(e){var t=L.bind(null,e);return t.type=e,t};o.createRef=function(){return{current:null}};o.forwardRef=function(e){return{$$typeof:te,render:e}};o.isValidElement=x;o.lazy=function(e){return{$$typeof:oe,_payload:{_status:-1,_result:e},_init:ce}};o.memo=function(e,t){return{$$typeof:ne,type:e,compare:t===void 0?null:t}};o.startTransition=function(e){var t=v.transition;v.transition={};try{e()}finally{v.transition=t}};o.unstable_act=U;o.useCallback=function(e,t){return f.current.useCallback(e,t)};o.useContext=function(e){return f.current.useContext(e)};o.useDebugValue=function(){};o.useDeferredValue=function(e){return f.current.useDeferredValue(e)};o.useEffect=function(e,t){return f.current.useEffect(e,t)};o.useId=function(){return f.current.useId()};o.useImperativeHandle=function(e,t,r){return f.current.useImperativeHandle(e,t,r)};o.useInsertionEffect=function(e,t){return f.current.useInsertionEffect(e,t)};o.useLayoutEffect=function(e,t){return f.current.useLayoutEffect(e,t)};o.useMemo=function(e,t){return f.current.useMemo(e,t)};o.useReducer=function(e,t,r){return f.current.useReducer(e,t,r)};o.useRef=function(e){return f.current.useRef(e)};o.useState=function(e){return f.current.useState(e)};o.useSyncExternalStore=function(e,t,r){return f.current.useSyncExternalStore(e,t,r)};o.useTransition=function(){return f.current.useTransition()};o.version="18.3.1"});var F=O((me,N)=>{"use strict";N.exports=V()});var p=K(F(),1),le="/plugins/dsh-codex-oauth/auth/status",fe="/plugins/dsh-codex-oauth/auth/login",pe="/plugins/dsh-codex-oauth/auth/logout",d=p.default.createElement;async function C(e,t="GET"){let r=await fetch(e,{method:t,credentials:"same-origin",headers:{accept:"application/json"}}),u=await r.json().catch(()=>({}));if(!r.ok)throw new Error(u.error||`HTTP ${r.status}`);return u}function de(){let[e,t]=(0,p.useState)({status:"loading"}),[r,u]=(0,p.useState)(!1),n=(0,p.useCallback)(()=>C(le).then(t).catch(l=>t({status:"error",message:l.message})),[]);(0,p.useEffect)(()=>{n()},[n]),(0,p.useEffect)(()=>{if(e.status!=="signing-in")return;let l=setInterval(n,1e3);return()=>clearInterval(l)},[n,e.status]);let c=async()=>{u(!0),t({status:"signing-in"});let l=window.open("about:blank","_blank","noopener,noreferrer");try{let _=await C(fe,"POST");if(!l)throw new Error("Please allow popups to sign in");l.location.replace(_.url)}catch(_){l?.close(),t({status:"error",message:_.message})}finally{u(!1)}},a=async()=>{u(!0);try{await C(pe,"POST"),t({status:"signed-out"})}catch(l){t({status:"error",message:l.message})}finally{u(!1)}},s=e.status==="signed-in",i=e.status==="signed-in"?"\u5DF2\u767B\u5F55":e.status==="signing-in"?"\u7B49\u5F85\u6D4F\u89C8\u5668\u5B8C\u6210\u767B\u5F55\u2026":e.status==="loading"?"\u68C0\u67E5\u767B\u5F55\u72B6\u6001\u2026":e.status==="error"?"\u767B\u5F55\u5931\u8D25":"\u672A\u767B\u5F55";return d("section",{style:{display:"flex",flexDirection:"column",gap:12,maxWidth:640}},d("h2",{style:{margin:0}},"OpenAI Codex"),d("p",{style:{margin:0,color:"var(--dsw-alias-label-secondary)"}},"\u4F7F\u7528 ChatGPT Plus/Pro \u7684 Codex \u8BA2\u9605\uFF0C\u65E0\u9700 OpenAI API Key\u3002"),d("div",{style:{display:"flex",alignItems:"center",justifyContent:"space-between",padding:16,border:"1px solid var(--dsw-alias-border-l2)",borderRadius:10}},d("span",null,i),e.status==="loading"?null:d("button",{type:"button",disabled:r||e.status==="signing-in",onClick:s?a:c},s?"\u9000\u51FA\u767B\u5F55":"\u4F7F\u7528 ChatGPT \u767B\u5F55")),e.status==="error"?d("p",{style:{color:"var(--dsw-alias-state-error-primary)"}},e.message):null,d("p",{style:{margin:0,color:"var(--dsw-alias-label-secondary)",fontSize:13}},"\u51ED\u636E\u4FDD\u5B58\u5728 DSH_HOME/.openai-codex-auth.json\uFF0C\u4E0D\u4F1A\u8BFB\u53D6 Pi \u6216 Codex CLI \u7684\u767B\u5F55\u6587\u4EF6\u3002"))}var ge="dsh-codex-oauth-client",ve=["slots"];function _e(e){e.slots.inject("settings.section",()=>e.slots.register({name:"settings.section",id:"dsh-codex-oauth",order:15,label:()=>"OpenAI Codex"},de))}export{_e as apply,ve as inject,ge as name};
|
|
2
|
+
/*! Bundled license information:
|
|
3
|
+
|
|
4
|
+
react/cjs/react.production.min.js:
|
|
5
|
+
(**
|
|
6
|
+
* @license React
|
|
7
|
+
* react.production.min.js
|
|
8
|
+
*
|
|
9
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
10
|
+
*
|
|
11
|
+
* This source code is licensed under the MIT license found in the
|
|
12
|
+
* LICENSE file in the root directory of this source tree.
|
|
13
|
+
*)
|
|
14
|
+
*/
|
package/lib/index.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { createCodexAdapter } from "./adapter.js";
|
|
2
|
-
import {
|
|
2
|
+
import { CodexCredentialStore, OPENAI_CODEX_PROVIDER, authPath } from "./store.js";
|
|
3
|
+
import { registerWebAuth } from "./web-auth.js";
|
|
4
|
+
export { login, logout, status } from "./auth.js";
|
|
3
5
|
|
|
4
6
|
export const name = "llm-codex-oauth";
|
|
5
7
|
export const inject = ["llm"];
|
|
6
8
|
|
|
7
|
-
export { OPENAI_CODEX_PROVIDER,
|
|
9
|
+
export { OPENAI_CODEX_PROVIDER, CodexCredentialStore, authPath, createCodexAdapter };
|
|
8
10
|
|
|
9
11
|
export function apply(ctx) {
|
|
10
|
-
const credentials = new
|
|
12
|
+
const credentials = new CodexCredentialStore();
|
|
11
13
|
ctx.llm.registerAdapter(
|
|
12
14
|
[OPENAI_CODEX_PROVIDER],
|
|
13
15
|
createCodexAdapter(credentials, () => ctx.get?.("attachments")),
|
|
14
16
|
);
|
|
17
|
+
registerWebAuth(ctx, credentials);
|
|
15
18
|
}
|
package/lib/store.js
CHANGED
|
@@ -1,65 +1,60 @@
|
|
|
1
1
|
import { mkdir, readFile, stat } from "node:fs/promises";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
|
|
5
5
|
|
|
6
6
|
export const OPENAI_CODEX_PROVIDER = "openai-codex";
|
|
7
|
-
export const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
return value === undefined ? undefined : structuredClone(value);
|
|
7
|
+
export const OPENAI_CODEX_AUTH_FILE = ".openai-codex-auth.json";
|
|
8
|
+
export function authPath(home = process.env.DSH_HOME || join(homedir(), ".dsh")) {
|
|
9
|
+
return resolve(join(home, OPENAI_CODEX_AUTH_FILE));
|
|
11
10
|
}
|
|
12
11
|
|
|
12
|
+
function clone(value) { return value === undefined ? undefined : structuredClone(value); }
|
|
13
13
|
async function ownerOnly(filename) {
|
|
14
14
|
if (process.platform === "win32") return;
|
|
15
15
|
try {
|
|
16
16
|
if ((await stat(filename)).mode & 0o077) throw new Error(`${filename} must be owner-only (chmod 600)`);
|
|
17
|
-
} catch (error) {
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
} catch (error) { if (error?.code !== "ENOENT") throw error; }
|
|
18
|
+
}
|
|
19
|
+
function parse(text, filename) {
|
|
20
|
+
let value;
|
|
21
|
+
try { value = JSON.parse(text); } catch { throw new Error(`${filename} is not valid JSON`); }
|
|
22
|
+
const credential = value?.credential;
|
|
23
|
+
if (value?.version !== 1 || !credential || credential.type !== "oauth") throw new Error(`${filename} has an invalid auth format`);
|
|
24
|
+
for (const key of ["access", "refresh", "accountId"])
|
|
25
|
+
if (typeof credential[key] !== "string" || credential[key].length === 0) throw new Error(`${filename} credential is invalid`);
|
|
26
|
+
if (!Number.isFinite(credential.expires) || credential.expires <= 0) throw new Error(`${filename} expiry is invalid`);
|
|
27
|
+
return clone(credential);
|
|
20
28
|
}
|
|
21
29
|
|
|
22
|
-
export class
|
|
23
|
-
constructor(filename =
|
|
24
|
-
|
|
25
|
-
async readAll() {
|
|
30
|
+
export class CodexCredentialStore {
|
|
31
|
+
constructor(filename = authPath()) { this.filename = filename; }
|
|
32
|
+
async readCurrent() {
|
|
26
33
|
await ownerOnly(this.filename);
|
|
27
|
-
try { return
|
|
28
|
-
catch (error) { if (error?.code === "ENOENT") return
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
async read(providerId) {
|
|
32
|
-
if (providerId !== OPENAI_CODEX_PROVIDER) return undefined;
|
|
33
|
-
const value = (await this.readAll())[providerId];
|
|
34
|
-
return value?.type === "oauth" ? clone(value) : undefined;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async list() {
|
|
38
|
-
return (await this.read(OPENAI_CODEX_PROVIDER) === undefined)
|
|
39
|
-
? [] : [{ providerId: OPENAI_CODEX_PROVIDER, type: "oauth" }];
|
|
34
|
+
try { return parse(await readFile(this.filename, "utf8"), this.filename); }
|
|
35
|
+
catch (error) { if (error?.code === "ENOENT") return undefined; throw error; }
|
|
40
36
|
}
|
|
41
|
-
|
|
37
|
+
async read(providerId) { return providerId === OPENAI_CODEX_PROVIDER ? this.readCurrent() : undefined; }
|
|
38
|
+
async list() { return (await this.readCurrent()) ? [{ providerId: OPENAI_CODEX_PROVIDER, type: "oauth" }] : []; }
|
|
42
39
|
async modify(providerId, fn) {
|
|
43
40
|
if (providerId !== OPENAI_CODEX_PROVIDER) throw new Error(`credential store does not own ${providerId}`);
|
|
44
41
|
await mkdir(dirname(this.filename), { recursive: true, mode: 0o700 });
|
|
45
42
|
return withFileLock(this.filename, async () => {
|
|
46
|
-
const
|
|
47
|
-
const current = auth[providerId];
|
|
43
|
+
const current = await this.readCurrent();
|
|
48
44
|
const next = await fn(clone(current));
|
|
49
|
-
if (next === undefined) return
|
|
50
|
-
|
|
51
|
-
|
|
45
|
+
if (next === undefined) return current;
|
|
46
|
+
const document = { version: 1, credential: next };
|
|
47
|
+
parse(JSON.stringify(document), this.filename);
|
|
48
|
+
await writeFileAtomic(this.filename, `${JSON.stringify(document, null, 2)}\n`, { mode: 0o600, dirMode: 0o700 });
|
|
52
49
|
return clone(next);
|
|
53
50
|
});
|
|
54
51
|
}
|
|
55
|
-
|
|
56
52
|
async delete(providerId) {
|
|
57
53
|
if (providerId !== OPENAI_CODEX_PROVIDER) return;
|
|
58
54
|
await mkdir(dirname(this.filename), { recursive: true, mode: 0o700 });
|
|
59
55
|
await withFileLock(this.filename, async () => {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
await writeFileAtomic(this.filename, `${JSON.stringify(auth, null, 2)}\n`, { mode: 0o600, dirMode: 0o700 });
|
|
56
|
+
const { rm } = await import("node:fs/promises");
|
|
57
|
+
await rm(this.filename, { force: true });
|
|
63
58
|
});
|
|
64
59
|
}
|
|
65
60
|
}
|
package/lib/web-auth.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { login, logout, status } from "./auth.js";
|
|
2
|
+
|
|
3
|
+
export const AUTH_STATUS_PATH = "/plugins/dsh-codex-oauth/auth/status";
|
|
4
|
+
export const AUTH_LOGIN_PATH = "/plugins/dsh-codex-oauth/auth/login";
|
|
5
|
+
export const AUTH_LOGOUT_PATH = "/plugins/dsh-codex-oauth/auth/logout";
|
|
6
|
+
|
|
7
|
+
function safeMessage(error) {
|
|
8
|
+
return String(error?.message ?? error)
|
|
9
|
+
.replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/gu, "[redacted token]")
|
|
10
|
+
.replace(/(\b(?:code|token|refresh_token|access_token)=)[^&\s]+/giu, "$1[redacted]")
|
|
11
|
+
.slice(0, 500);
|
|
12
|
+
}
|
|
13
|
+
function trusted(req) {
|
|
14
|
+
const remote = req.socket.remoteAddress;
|
|
15
|
+
if (!["127.0.0.1", "::1", "::ffff:127.0.0.1"].includes(remote)) return false;
|
|
16
|
+
if (req.headers["sec-fetch-site"] === "cross-site") return false;
|
|
17
|
+
if (!req.headers.host) return false;
|
|
18
|
+
if (!req.headers.origin) return true;
|
|
19
|
+
try { return new URL(req.headers.origin).host === new URL(`http://${req.headers.host}`).host; }
|
|
20
|
+
catch { return false; }
|
|
21
|
+
}
|
|
22
|
+
function json(res, statusCode, value) {
|
|
23
|
+
res.writeHead(statusCode, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store", "x-content-type-options": "nosniff" });
|
|
24
|
+
res.end(JSON.stringify(value));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function registerWebAuth(ctx, store) {
|
|
28
|
+
let operation;
|
|
29
|
+
let challenge;
|
|
30
|
+
let state = { status: "signed-out" };
|
|
31
|
+
const cancellation = new AbortController();
|
|
32
|
+
const update = () => status(store).then((value) => { state = value.authenticated ? { status: "signed-in" } : { status: "signed-out" }; return state; });
|
|
33
|
+
const start = () => {
|
|
34
|
+
if (operation) return;
|
|
35
|
+
state = { status: "signing-in" };
|
|
36
|
+
challenge = undefined;
|
|
37
|
+
operation = login(store, {
|
|
38
|
+
signal: cancellation.signal,
|
|
39
|
+
prompt: async (prompt) => prompt.type === "select" ? "browser" : new Promise((_resolve, reject) => cancellation.signal.addEventListener("abort", () => reject(cancellation.signal.reason), { once: true })),
|
|
40
|
+
notify: (event) => { if (event.type === "auth_url") challenge = { url: event.url }; },
|
|
41
|
+
}).then(update, (error) => { state = { status: "error", message: safeMessage(error) }; }).finally(() => { operation = undefined; });
|
|
42
|
+
};
|
|
43
|
+
ctx.inject(["webServer"], (web) => ctx.effect(() => {
|
|
44
|
+
const routes = [
|
|
45
|
+
web.webServer.register({ kind: "exact", path: AUTH_STATUS_PATH, handler: async (req, res) => {
|
|
46
|
+
if (req.method !== "GET" || !trusted(req)) return json(res, req.method === "GET" ? 403 : 405, { error: "forbidden" });
|
|
47
|
+
if (!operation) await update();
|
|
48
|
+
json(res, 200, state);
|
|
49
|
+
}}),
|
|
50
|
+
web.webServer.register({ kind: "exact", path: AUTH_LOGIN_PATH, handler: async (req, res) => {
|
|
51
|
+
if (req.method !== "POST" || !trusted(req)) return json(res, req.method === "POST" ? 403 : 405, { error: "forbidden" });
|
|
52
|
+
try { start(); for (let i = 0; i < 50 && !challenge && operation; i++) await new Promise((resolve) => setTimeout(resolve, 100)); if (!challenge) throw new Error("OAuth URL was not created"); json(res, 200, challenge); }
|
|
53
|
+
catch (error) { json(res, 500, { error: safeMessage(error) }); }
|
|
54
|
+
}}),
|
|
55
|
+
web.webServer.register({ kind: "exact", path: AUTH_LOGOUT_PATH, handler: async (req, res) => {
|
|
56
|
+
if (req.method !== "POST" || !trusted(req)) return json(res, req.method === "POST" ? 403 : 405, { error: "forbidden" });
|
|
57
|
+
cancellation.abort(new Error("sign-in cancelled")); await operation?.catch(() => {}); await logout(store); state = { status: "signed-out" }; json(res, 200, { ok: true });
|
|
58
|
+
}}),
|
|
59
|
+
];
|
|
60
|
+
return async () => { cancellation.abort(new Error("plugin disposed")); await operation?.catch(() => {}); for (const dispose of routes) dispose(); };
|
|
61
|
+
}, "dsh-codex-oauth: Web OAuth routes"));
|
|
62
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-codex-oauth",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Use
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Use a ChatGPT Codex subscription in DeepSeek Harness through standalone OAuth",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/eons2long/dsh-codex-oauth.git"
|
|
@@ -15,9 +15,16 @@
|
|
|
15
15
|
"scripts": { "test": "node test/smoke.mjs" },
|
|
16
16
|
"type": "module",
|
|
17
17
|
"main": "lib/index.js",
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
18
|
+
"bin": { "dsh-openai-codex": "bin/dsh-openai-codex.js" },
|
|
19
|
+
"exports": { ".": "./lib/index.js", "./client": "./lib/client.js", "./cordis.patch.yml": "./cordis.patch.yml" },
|
|
20
|
+
"files": ["lib", "bin", "client", "cordis.patch.yml", "README.md", "README.zh.md", "SPEC.md", "LICENSE"],
|
|
21
|
+
"dsh": {
|
|
22
|
+
"bundle": { "patch": "./cordis.patch.yml" },
|
|
23
|
+
"client": {
|
|
24
|
+
"inject": ["@deepseek-ai/dsh-client-runtime", "@deepseek-ai/dsh-client-ui-settings", "@deepseek-ai/dsh-client-locale"],
|
|
25
|
+
"platform": "web"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
21
28
|
"peerDependencies": {
|
|
22
29
|
"@earendil-works/pi-ai": "^0.84.1",
|
|
23
30
|
"@deepseek-ai/cordis": "^4.0.1",
|