dsh-codex-oauth 0.2.5 → 0.2.7
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 +2 -0
- package/README.zh.md +2 -0
- package/bin/dsh-openai-codex.js +1 -5
- package/client/index.jsx +13 -6
- package/lib/client.js +17 -8
- package/lib/safe-message.js +8 -0
- package/lib/store.js +19 -6
- package/lib/web-auth.js +51 -20
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -61,6 +61,8 @@ The default is `~/.dsh/.openai-codex-auth.json`. The file is written atomically
|
|
|
61
61
|
npm test
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
+
`npm test` rebuilds the Web bundle before running the smoke checks.
|
|
65
|
+
|
|
64
66
|
The test requires the DSH and `pi-ai` peer dependencies supplied by a DSH profile.
|
|
65
67
|
|
|
66
68
|
## Limitations
|
package/README.zh.md
CHANGED
package/bin/dsh-openai-codex.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { createInterface } from "node:readline/promises";
|
|
4
4
|
import { authPath, login, logout, status } from "../lib/auth.js";
|
|
5
|
+
import { safeMessage } from "../lib/safe-message.js";
|
|
5
6
|
|
|
6
7
|
function openBrowser(raw) {
|
|
7
8
|
const url = new URL(raw);
|
|
@@ -10,11 +11,6 @@ function openBrowser(raw) {
|
|
|
10
11
|
const child = spawn(command[0], command.slice(1), { detached: true, stdio: "ignore", windowsHide: true });
|
|
11
12
|
child.on("error", () => {}); child.unref();
|
|
12
13
|
}
|
|
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
14
|
function help() {
|
|
19
15
|
console.log("Usage: dsh-openai-codex <login|logout|status> [--device-code]");
|
|
20
16
|
}
|
package/client/index.jsx
CHANGED
|
@@ -8,7 +8,7 @@ const h = React.createElement;
|
|
|
8
8
|
async function request(path, method = "GET") {
|
|
9
9
|
const response = await fetch(path, { method, credentials: "same-origin", headers: { accept: "application/json" } });
|
|
10
10
|
const value = await response.json().catch(() => ({}));
|
|
11
|
-
if (!response.ok) throw new Error(value.error
|
|
11
|
+
if (!response.ok) throw new Error(value && typeof value === "object" && typeof value.error === "string" ? value.error : `HTTP ${response.status}`);
|
|
12
12
|
return value;
|
|
13
13
|
}
|
|
14
14
|
|
|
@@ -22,11 +22,18 @@ function CodexSettings() {
|
|
|
22
22
|
const timer = setInterval(refresh, 1000);
|
|
23
23
|
return () => clearInterval(timer);
|
|
24
24
|
}, [refresh, state.status]);
|
|
25
|
-
const signIn = () => {
|
|
25
|
+
const signIn = async () => {
|
|
26
|
+
const popup = window.open("about:blank", "_blank");
|
|
27
|
+
if (!popup) { setState({ status: "error", message: "Please allow popups to sign in" }); return; }
|
|
28
|
+
popup.opener = null;
|
|
26
29
|
setBusy(true); setState({ status: "signing-in" });
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
try {
|
|
31
|
+
const challenge = await request(LOGIN, "POST");
|
|
32
|
+
popup.location.replace(challenge.url);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
popup?.close();
|
|
35
|
+
setState({ status: "error", message: error.message });
|
|
36
|
+
} finally { setBusy(false); }
|
|
30
37
|
};
|
|
31
38
|
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); } };
|
|
32
39
|
const signedIn = state.status === "signed-in";
|
|
@@ -38,7 +45,7 @@ function CodexSettings() {
|
|
|
38
45
|
h("span", null, label),
|
|
39
46
|
state.status === "loading" ? null : h("button", { type: "button", disabled: busy || state.status === "signing-in", onClick: signedIn ? signOut : signIn }, signedIn ? "退出登录" : "使用 ChatGPT 登录")),
|
|
40
47
|
state.status === "error" ? h("p", { style: { color: "var(--dsw-alias-state-error-primary)" } }, state.message) : null,
|
|
41
|
-
|
|
48
|
+
);
|
|
42
49
|
}
|
|
43
50
|
|
|
44
51
|
export const name = "dsh-codex-oauth-client";
|
package/lib/client.js
CHANGED
|
@@ -43,7 +43,7 @@ var h = import_react.default.createElement;
|
|
|
43
43
|
async function request(path, method = "GET") {
|
|
44
44
|
const response = await fetch(path, { method, credentials: "same-origin", headers: { accept: "application/json" } });
|
|
45
45
|
const value = await response.json().catch(() => ({}));
|
|
46
|
-
if (!response.ok) throw new Error(value.error
|
|
46
|
+
if (!response.ok) throw new Error(value && typeof value === "object" && typeof value.error === "string" ? value.error : `HTTP ${response.status}`);
|
|
47
47
|
return value;
|
|
48
48
|
}
|
|
49
49
|
function CodexSettings() {
|
|
@@ -58,14 +58,24 @@ function CodexSettings() {
|
|
|
58
58
|
const timer = setInterval(refresh, 1e3);
|
|
59
59
|
return () => clearInterval(timer);
|
|
60
60
|
}, [refresh, state.status]);
|
|
61
|
-
const signIn = () => {
|
|
61
|
+
const signIn = async () => {
|
|
62
|
+
const popup = window.open("about:blank", "_blank");
|
|
63
|
+
if (!popup) {
|
|
64
|
+
setState({ status: "error", message: "Please allow popups to sign in" });
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
popup.opener = null;
|
|
62
68
|
setBusy(true);
|
|
63
69
|
setState({ status: "signing-in" });
|
|
64
|
-
|
|
65
|
-
|
|
70
|
+
try {
|
|
71
|
+
const challenge = await request(LOGIN, "POST");
|
|
72
|
+
popup.location.replace(challenge.url);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
popup?.close();
|
|
75
|
+
setState({ status: "error", message: error.message });
|
|
76
|
+
} finally {
|
|
66
77
|
setBusy(false);
|
|
67
|
-
|
|
68
|
-
} else setTimeout(() => setBusy(false), 500);
|
|
78
|
+
}
|
|
69
79
|
};
|
|
70
80
|
const signOut = async () => {
|
|
71
81
|
setBusy(true);
|
|
@@ -91,8 +101,7 @@ function CodexSettings() {
|
|
|
91
101
|
h("span", null, label),
|
|
92
102
|
state.status === "loading" ? null : h("button", { type: "button", disabled: busy || state.status === "signing-in", onClick: signedIn ? signOut : signIn }, signedIn ? "\u9000\u51FA\u767B\u5F55" : "\u4F7F\u7528 ChatGPT \u767B\u5F55")
|
|
93
103
|
),
|
|
94
|
-
state.status === "error" ? h("p", { style: { color: "var(--dsw-alias-state-error-primary)" } }, state.message) : null
|
|
95
|
-
h("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")
|
|
104
|
+
state.status === "error" ? h("p", { style: { color: "var(--dsw-alias-state-error-primary)" } }, state.message) : null
|
|
96
105
|
);
|
|
97
106
|
}
|
|
98
107
|
var name = "dsh-codex-oauth-client";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function safeMessage(error) {
|
|
2
|
+
return String(error?.message ?? error)
|
|
3
|
+
.replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/gu, "[redacted token]")
|
|
4
|
+
.replace(/([?&](?:code|token|refresh_token|access_token|id_token)=)[^&#\s]+/giu, "$1[redacted]")
|
|
5
|
+
.replace(/(["']?(?:access_token|refresh_token|id_token|authorization_code|code_verifier|token|code)["']?\s*[:=]\s*)("[^"]*"|'[^']*'|[^,}\s]+)/giu, "$1[redacted]")
|
|
6
|
+
.replace(/(\bBearer\s+)[^\s,]+/giu, "$1[redacted]")
|
|
7
|
+
.slice(0, 500);
|
|
8
|
+
}
|
package/lib/store.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir, readFile
|
|
1
|
+
import { chmod, lstat, mkdir, readFile } from "node:fs/promises";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
|
|
@@ -10,11 +10,22 @@ export function authPath(home = process.env.DSH_HOME || join(homedir(), ".dsh"))
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
function clone(value) { return value === undefined ? undefined : structuredClone(value); }
|
|
13
|
+
async function privateDirectory(filename) {
|
|
14
|
+
const directory = dirname(filename);
|
|
15
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
16
|
+
if (process.platform === "win32") return;
|
|
17
|
+
const info = await lstat(directory);
|
|
18
|
+
if (info.isSymbolicLink()) throw new Error(`${directory} must not be a symbolic link`);
|
|
19
|
+
if (info.mode & 0o077) await chmod(directory, 0o700);
|
|
20
|
+
}
|
|
13
21
|
async function ownerOnly(filename) {
|
|
14
22
|
if (process.platform === "win32") return;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
23
|
+
for (const [path, label] of [[dirname(filename), "directory"], [filename, "file"]]) {
|
|
24
|
+
try {
|
|
25
|
+
const info = await lstat(path);
|
|
26
|
+
if (info.isSymbolicLink() || info.mode & 0o077) throw new Error(`${path} must be an owner-only ${label}`);
|
|
27
|
+
} catch (error) { if (error?.code !== "ENOENT") throw error; }
|
|
28
|
+
}
|
|
18
29
|
}
|
|
19
30
|
function parse(text, filename) {
|
|
20
31
|
let value;
|
|
@@ -30,6 +41,7 @@ function parse(text, filename) {
|
|
|
30
41
|
export class CodexCredentialStore {
|
|
31
42
|
constructor(filename = authPath()) { this.filename = filename; }
|
|
32
43
|
async readCurrent() {
|
|
44
|
+
await privateDirectory(this.filename);
|
|
33
45
|
await ownerOnly(this.filename);
|
|
34
46
|
try { return parse(await readFile(this.filename, "utf8"), this.filename); }
|
|
35
47
|
catch (error) { if (error?.code === "ENOENT") return undefined; throw error; }
|
|
@@ -38,7 +50,7 @@ export class CodexCredentialStore {
|
|
|
38
50
|
async list() { return (await this.readCurrent()) ? [{ providerId: OPENAI_CODEX_PROVIDER, type: "oauth" }] : []; }
|
|
39
51
|
async modify(providerId, fn) {
|
|
40
52
|
if (providerId !== OPENAI_CODEX_PROVIDER) throw new Error(`credential store does not own ${providerId}`);
|
|
41
|
-
await
|
|
53
|
+
await privateDirectory(this.filename);
|
|
42
54
|
return withFileLock(this.filename, async () => {
|
|
43
55
|
const current = await this.readCurrent();
|
|
44
56
|
const next = await fn(clone(current));
|
|
@@ -51,8 +63,9 @@ export class CodexCredentialStore {
|
|
|
51
63
|
}
|
|
52
64
|
async delete(providerId) {
|
|
53
65
|
if (providerId !== OPENAI_CODEX_PROVIDER) return;
|
|
54
|
-
await
|
|
66
|
+
await privateDirectory(this.filename);
|
|
55
67
|
await withFileLock(this.filename, async () => {
|
|
68
|
+
await ownerOnly(this.filename);
|
|
56
69
|
const { rm } = await import("node:fs/promises");
|
|
57
70
|
await rm(this.filename, { force: true });
|
|
58
71
|
});
|
package/lib/web-auth.js
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
import { login, logout, status } from "./auth.js";
|
|
2
|
+
import { safeMessage } from "./safe-message.js";
|
|
2
3
|
|
|
3
4
|
export const AUTH_STATUS_PATH = "/plugins/dsh-codex-oauth/auth/status";
|
|
4
5
|
export const AUTH_LOGIN_PATH = "/plugins/dsh-codex-oauth/auth/login";
|
|
5
6
|
export const AUTH_LOGOUT_PATH = "/plugins/dsh-codex-oauth/auth/logout";
|
|
6
7
|
|
|
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
8
|
function trusted(req) {
|
|
14
9
|
const remote = req.socket.remoteAddress;
|
|
15
10
|
if (!["127.0.0.1", "::1", "::ffff:127.0.0.1"].includes(remote)) return false;
|
|
@@ -26,42 +21,78 @@ function json(res, statusCode, value) {
|
|
|
26
21
|
|
|
27
22
|
export function registerWebAuth(ctx, store) {
|
|
28
23
|
let operation;
|
|
24
|
+
let cancellation;
|
|
29
25
|
let challenge;
|
|
30
26
|
let state = { status: "signed-out" };
|
|
31
|
-
const
|
|
32
|
-
|
|
27
|
+
const update = async () => {
|
|
28
|
+
const value = await status(store);
|
|
29
|
+
state = value.authenticated ? { status: "signed-in" } : { status: "signed-out" };
|
|
30
|
+
return state;
|
|
31
|
+
};
|
|
33
32
|
const start = () => {
|
|
34
33
|
if (operation) return;
|
|
34
|
+
const controller = new AbortController();
|
|
35
|
+
cancellation = controller;
|
|
35
36
|
state = { status: "signing-in" };
|
|
36
37
|
challenge = undefined;
|
|
37
|
-
operation =
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
operation = (async () => {
|
|
39
|
+
try {
|
|
40
|
+
await login(store, {
|
|
41
|
+
signal: controller.signal,
|
|
42
|
+
prompt: async (prompt) => prompt.type === "select"
|
|
43
|
+
? "browser"
|
|
44
|
+
: new Promise((_resolve, reject) => controller.signal.addEventListener("abort", () => reject(controller.signal.reason), { once: true })),
|
|
45
|
+
notify: (event) => {
|
|
46
|
+
if (event.type !== "auth_url") return;
|
|
47
|
+
try {
|
|
48
|
+
const url = new URL(event.url);
|
|
49
|
+
if (url.protocol !== "https:") throw new Error("OAuth URL must use HTTPS");
|
|
50
|
+
challenge = { url: url.href };
|
|
51
|
+
} catch (error) { controller.abort(error); }
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
await update();
|
|
55
|
+
} catch (error) {
|
|
56
|
+
state = controller.signal.aborted ? { status: "signed-out" } : { status: "error", message: safeMessage(error) };
|
|
57
|
+
} finally {
|
|
58
|
+
if (cancellation === controller) cancellation = undefined;
|
|
59
|
+
operation = undefined;
|
|
60
|
+
}
|
|
61
|
+
})();
|
|
42
62
|
};
|
|
43
63
|
ctx.inject(["webServer"], (web) => ctx.effect(() => {
|
|
44
64
|
const routes = [
|
|
45
65
|
web.webServer.register({ kind: "exact", path: AUTH_STATUS_PATH, handler: async (req, res) => {
|
|
46
66
|
if (req.method !== "GET" || !trusted(req)) return json(res, req.method === "GET" ? 403 : 405, { error: "forbidden" });
|
|
47
|
-
|
|
48
|
-
|
|
67
|
+
try {
|
|
68
|
+
if (!operation) await update();
|
|
69
|
+
json(res, 200, state);
|
|
70
|
+
} catch (error) { json(res, 500, { error: safeMessage(error) }); }
|
|
49
71
|
}}),
|
|
50
72
|
web.webServer.register({ kind: "exact", path: AUTH_LOGIN_PATH, handler: async (req, res) => {
|
|
51
|
-
if (
|
|
73
|
+
if (req.method !== "POST" || !trusted(req)) return json(res, req.method === "POST" ? 403 : 405, { error: "forbidden" });
|
|
52
74
|
try {
|
|
53
75
|
start();
|
|
54
76
|
for (let i = 0; i < 50 && !challenge && operation; i++) await new Promise((resolve) => setTimeout(resolve, 100));
|
|
55
77
|
if (!challenge) throw new Error("OAuth URL was not created");
|
|
56
|
-
|
|
57
|
-
else json(res, 200, challenge);
|
|
78
|
+
json(res, 200, challenge);
|
|
58
79
|
} catch (error) { json(res, 500, { error: safeMessage(error) }); }
|
|
59
80
|
}}),
|
|
60
81
|
web.webServer.register({ kind: "exact", path: AUTH_LOGOUT_PATH, handler: async (req, res) => {
|
|
61
82
|
if (req.method !== "POST" || !trusted(req)) return json(res, req.method === "POST" ? 403 : 405, { error: "forbidden" });
|
|
62
|
-
|
|
83
|
+
try {
|
|
84
|
+
cancellation?.abort(new Error("sign-in cancelled"));
|
|
85
|
+
await operation?.catch(() => {});
|
|
86
|
+
await logout(store);
|
|
87
|
+
state = { status: "signed-out" };
|
|
88
|
+
json(res, 200, { ok: true });
|
|
89
|
+
} catch (error) { json(res, 500, { error: safeMessage(error) }); }
|
|
63
90
|
}}),
|
|
64
91
|
];
|
|
65
|
-
return async () => {
|
|
92
|
+
return async () => {
|
|
93
|
+
cancellation?.abort(new Error("plugin disposed"));
|
|
94
|
+
await operation?.catch(() => {});
|
|
95
|
+
for (const dispose of routes) dispose();
|
|
96
|
+
};
|
|
66
97
|
}, "dsh-codex-oauth: Web OAuth routes"));
|
|
67
98
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-codex-oauth",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"description": "Use a ChatGPT Codex subscription in DeepSeek Harness through standalone OAuth",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
},
|
|
13
13
|
"keywords": ["dsh-plugin", "deepseek-harness", "openai-codex", "oauth"],
|
|
14
14
|
"license": "MIT",
|
|
15
|
-
"scripts": { "test": "node test/smoke.mjs" },
|
|
15
|
+
"scripts": { "build": "node scripts/build-client.mjs", "test": "npm run build && node test/smoke.mjs && node test/web-auth.mjs" },
|
|
16
16
|
"type": "module",
|
|
17
17
|
"main": "lib/index.js",
|
|
18
18
|
"bin": { "dsh-openai-codex": "bin/dsh-openai-codex.js" },
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"platform": "web"
|
|
26
26
|
}
|
|
27
27
|
},
|
|
28
|
+
"devDependencies": { "esbuild": "^0.28.2" },
|
|
28
29
|
"peerDependencies": {
|
|
29
30
|
"@earendil-works/pi-ai": "^0.84.1",
|
|
30
31
|
"@deepseek-ai/cordis": "^4.0.1",
|