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 CHANGED
@@ -2,47 +2,72 @@
2
2
 
3
3
  [English](README.md) | [中文](README.zh.md)
4
4
 
5
- DSH adapter that uses the same OpenAI Codex OAuth credential as Pi:
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 Pi's installed `@earendil-works/pi-ai` Codex provider and DSH's public
9
- `dsh-llm-pi-ai` adapter, including the official Codex Responses transport, images,
10
- tool calls, reasoning replay, compaction, and OAuth refresh. It does not copy the
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 a manual profile install, add the package to the profile and include this
20
- entry in its `cordis.patch.yml`:
18
+ For an `npx` installation:
21
19
 
22
- ```yaml
23
- - insert:
24
- - id: llm-codex-oauth
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
- The profile must also include the normal `llm` service. Restart:
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
- npx @deepseek-ai/dsh web
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
- Then select provider `openai-codex` in the model picker.
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
- ## Scope
64
+ The test requires the DSH and `pi-ai` peer dependencies supplied by a DSH profile.
37
65
 
38
- The plugin delegates message conversion, streaming, image attachments, tool calls,
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
- The plugin reads/writes `~/.pi/agent/auth.json` and never prints token values.
43
- Because Pi and DSH do not share a cross-application lock, avoid starting both at the
44
- same time while an OAuth refresh may occur; a future version may use a separate DSH
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
- 让 [DeepSeek Harness(dsh)](https://github.com/deepseek-ai/deepseek-harness)
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
- 本插件不是普通的 OpenAI API 适配器,也不会把 ChatGPT 订阅变成通用 API 凭据。它使用 Pi 的 Codex provider、官方 Codex Responses 传输和 DSH 公共的 `dsh-llm-pi-ai` 适配器。
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
- 如果使用的是 `npx` 版本:
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
- ```text
37
- Provider: openai-codex
38
- ```
27
+ 插件拥有独立的 OAuth 登录流程和凭据文件。新用户不需要安装 Pi,也不需要复制任何 token。
28
+
29
+ ### Web UI
30
+
31
+ 打开 **设置 → OpenAI Codex**,点击 **使用 ChatGPT 登录**。插件会打开浏览器完成 OAuth 授权;返回 DSH 后,在模型选择器中选择 `openai-codex`。
39
32
 
40
- 也可以手动把下面的条目加入 profile 的 `cordis.patch.yml`:
33
+ ### 命令行登录
41
34
 
42
- ```yaml
43
- - insert:
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
- Pi 必须已经完成 OpenAI Codex 登录,并且存在:
45
+ 查看状态和退出登录:
51
46
 
52
- ```text
53
- ~/.pi/agent/auth.json
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
- 文件中应有 `openai-codex` OAuth 条目。插件不会替用户登录,也不会要求复制 token。
52
+ 浏览器登录会打开 OpenAI 授权页面,并通过 localhost 回调完成。登录后,在 DSH 模型选择器中选择 `openai-codex`。
57
53
 
58
- ## 凭据与并发注意事项
54
+ ### 凭据存储
59
55
 
60
- 插件只使用 Pi 的现有凭据文件,并会在 OAuth 刷新后写回该文件。Pi 与 DSH 并不共享同一个跨应用锁,因此在 token 可能刷新时,不建议同时运行 Pi 和 DSH。
56
+ 凭据默认存储在:
61
57
 
62
- 卸载插件不会删除 Pi 的登录凭据。若要退出登录,请通过 Pi 自己的 `/logout` 完成。
58
+ ```text
59
+ ~/.dsh/.openai-codex-auth.json
60
+ ```
63
61
 
64
- ## 限制
62
+ 也可以通过 `DSH_HOME` 改变目录。文件采用原子写入、仅所有者可读写(`0600`),登录、刷新和退出登录使用跨进程文件锁。
65
63
 
66
- - ChatGPT 订阅权限、模型目录、配额和 Codex 后端行为由 OpenAI 控制,可能变化;
67
- - 插件依赖 DSH 的公共 `dsh-llm-pi-ai` 适配器,需与当前 DSH 版本兼容;
68
- - 这是 Codex 后端接入,不是公开的 OpenAI Platform API;
69
- - DSH 的其他能力(文件系统、Shell、MCP、权限、附件等)仍由当前 profile 提供。
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
- 测试需要 DSH 和 Pi 的依赖已经安装或由 DSH profile 提供。
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
+ }
@@ -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 { PiCodexCredentialStore, OPENAI_CODEX_PROVIDER, PI_AUTH_FILE } from "./store.js";
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, PI_AUTH_FILE, PiCodexCredentialStore, createCodexAdapter };
9
+ export { OPENAI_CODEX_PROVIDER, CodexCredentialStore, authPath, createCodexAdapter };
8
10
 
9
11
  export function apply(ctx) {
10
- const credentials = new PiCodexCredentialStore();
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 PI_AUTH_FILE = join(homedir(), ".pi", "agent", "auth.json");
8
-
9
- function clone(value) {
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
- if (error?.code !== "ENOENT") throw error;
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 PiCodexCredentialStore {
23
- constructor(filename = PI_AUTH_FILE) { this.filename = 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 JSON.parse(await readFile(this.filename, "utf8")); }
28
- catch (error) { if (error?.code === "ENOENT") return {}; throw error; }
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 auth = await this.readAll();
47
- const current = auth[providerId];
43
+ const current = await this.readCurrent();
48
44
  const next = await fn(clone(current));
49
- if (next === undefined) return clone(current);
50
- auth[providerId] = next;
51
- await writeFileAtomic(this.filename, `${JSON.stringify(auth, null, 2)}\n`, { mode: 0o600, dirMode: 0o700 });
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 auth = await this.readAll();
61
- delete auth[providerId];
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
  }
@@ -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.1.1",
4
- "description": "Use Pi's OpenAI Codex OAuth subscription in DeepSeek Harness",
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
- "exports": { ".": "./lib/index.js" },
19
- "files": ["lib", "cordis.patch.yml", "README.md", "README.zh.md", "LICENSE"],
20
- "dsh": { "bundle": { "patch": "./cordis.patch.yml" } },
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",