@tnnevol/dsh-codex-auth 0.1.0-rc.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 ADDED
@@ -0,0 +1,56 @@
1
+ # @tnnevol/dsh-codex-auth
2
+
3
+ 一个独立的 DeepSeek Harness Codex OAuth 插件,适配 DSH `0.1.0-rc.7`。
4
+
5
+ 作者:tnnevol
6
+
7
+ ## 功能
8
+
9
+ - 在 DSH Web 的“插件”设置中提供 ChatGPT 登录和退出登录入口。
10
+ - 通过 `@earendil-works/pi-ai` 发起 Codex device-code 授权,由插件展示一次性授权码并轮询登录结果。
11
+ - 授权页面不要求选择工作空间;用户只需在 Codex 授权页面提交插件生成的一次性代码。
12
+ - 复用 `dsh-codex-connect` 的凭据文件格式:`$DSH_HOME/.openai-codex-auth.json`。
13
+ - 复用 `dsh-llm-pi-ai` 的官方模型设置入口,使 OpenAI Codex 在模型列表中显示登录状态,并直接展示可编辑的模型目录。
14
+ - OAuth 登录成功后,将当前 access token 同步到 `OPENAI_CODEX_AUTH_TOKEN` 凭据引用;OAuth 文件仍是唯一登录源,凭据引用只是给通用 pi-ai adapter 和官方模型页使用的镜像。
15
+ - Codex 编辑页隐藏通用的 API 密钥和 API 地址字段,避免用户误改 OAuth 管理的认证与端点;模型目录仍使用 DSH 官方编辑器。
16
+ - 模型列表弹框增加“全选”,确认按钮文案为“确定”,并使用官方保存按钮的主色。
17
+ - 在“设置 → 插件 → Codex Auth”中提供图片识别开关;开启后按 DSH 附件规范注册 `view_image`,仅允许当前图片模型读取经过文件系统权限控制的本地图片。
18
+ - 图像生成开关会明确显示为当前不可用:DSH `rc.7` 的 Codex provider 只声明图片输入,不提供图像输出能力。
19
+ - 通过 DSH `rc.7` 的 keyed slot `settings.plugin.item` 注册 UI,并显式提供 `key`。
20
+
21
+ ## 本地构建
22
+
23
+ 在仓库根目录执行:
24
+
25
+ ```sh
26
+ pnpm --filter @tnnevol/dsh-codex-auth install
27
+ pnpm --filter @tnnevol/dsh-codex-auth run check
28
+ ```
29
+
30
+ ## 使用本地 DSH CLI 调试
31
+
32
+ 先确保本地 DSH 源码已经安装依赖并可运行,然后在 DSH 仓库目录执行:
33
+
34
+ ```sh
35
+ pnpm --filter @tnnevol/dsh-codex-auth run build
36
+ pnpm dsh web --patch /absolute/path/to/fn-os-apps/plugins/dsh-codex-auth-plugin/cordis.patch.yml
37
+ ```
38
+
39
+ `cordis.patch.yml` 使用包名加载 Host bundle;如果要调试尚未构建的源码,可将 patch 中的 `name` 改成源码入口的绝对路径,并使用 DSH skill 中的 `--patch` 调试流程。
40
+
41
+ ## 发布 rc 版本
42
+
43
+ 当前版本为 npm prerelease,发布时必须使用 `rc` dist-tag:
44
+
45
+ ```sh
46
+ npm run publish:rc
47
+ ```
48
+
49
+ ## 安装到 profile
50
+
51
+ ```sh
52
+ dsh plugin --profile web add @tnnevol/dsh-codex-auth
53
+ dsh --profile web --dump-config
54
+ ```
55
+
56
+ 安装后重新启动 Web profile。该插件不应与旧的 `dsh-codex-connect` 同时注册同一套 Codex 模型能力;迁移时请先停用旧插件,再启用本插件。
@@ -0,0 +1,26 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "engines": {
4
+ "node": "^22.19.0 || >=24.0.0"
5
+ },
6
+ "dshPluginApi": {
7
+ "version": "0.1.0-rc.7",
8
+ "packages": [
9
+ "@deepseek-ai/dsh-atomic-write",
10
+ "@deepseek-ai/dsh-attachment",
11
+ "@deepseek-ai/dsh-credentials",
12
+ "@deepseek-ai/dsh-home-paths",
13
+ "@deepseek-ai/dsh-host-webserver",
14
+ "@deepseek-ai/dsh-invariants",
15
+ "@deepseek-ai/dsh-launch-environment",
16
+ "@deepseek-ai/dsh-llm",
17
+ "@deepseek-ai/dsh-llm-pi-ai",
18
+ "@deepseek-ai/dsh-settings",
19
+ "@deepseek-ai/dsh-timeout"
20
+ ]
21
+ },
22
+ "piAi": {
23
+ "package": "@earendil-works/pi-ai",
24
+ "version": "0.82.1"
25
+ }
26
+ }
@@ -0,0 +1,28 @@
1
+ - insert:
2
+ - id: dsh-codex-auth
3
+ name: '@tnnevol/dsh-codex-auth'
4
+
5
+ # Use the official pi-ai settings family so the DSH Models page renders the
6
+ # provider's credential status and its standard model-catalog editor. The
7
+ # access token is mirrored to this reference by the plugin after OAuth login.
8
+ - id: llm-pi-ai
9
+ config:
10
+ providers:
11
+ openai-codex:
12
+ displayName: OpenAI Codex
13
+ apiKeyEnv: OPENAI_CODEX_AUTH_TOKEN
14
+ models:
15
+ - id: gpt-5.3-codex-spark
16
+ name: GPT-5.3 Codex Spark
17
+ - id: gpt-5.4
18
+ name: GPT-5.4
19
+ - id: gpt-5.4-mini
20
+ name: GPT-5.4 mini
21
+ - id: gpt-5.5
22
+ name: GPT-5.5
23
+ - id: gpt-5.6-luna
24
+ name: GPT-5.6 Luna
25
+ - id: gpt-5.6-sol
26
+ name: GPT-5.6 Sol
27
+ - id: gpt-5.6-terra
28
+ name: GPT-5.6 Terra
@@ -0,0 +1,31 @@
1
+ import { AuthInteraction, Credential, CredentialInfo, CredentialStore } from "@earendil-works/pi-ai";
2
+ //#region src/store.d.ts
3
+ /** pi-ai provider id used by ChatGPT Codex OAuth. */
4
+ declare const CODEX_PROVIDER = "openai-codex";
5
+ /** Kept compatible with dsh-codex-connect so a login can be reused. */
6
+ declare const CODEX_AUTH_FILENAME = ".openai-codex-auth.json";
7
+ declare function codexAuthPath(dshHome?: string): string;
8
+ /** File-backed CredentialStore that owns only the Codex OAuth route. */
9
+ declare class CodexCredentialStore implements CredentialStore {
10
+ readonly filename: string;
11
+ constructor(filename?: string);
12
+ private readCurrent;
13
+ read(providerId: string): Promise<Credential | undefined>;
14
+ list(): Promise<readonly CredentialInfo[]>;
15
+ modify(providerId: string, fn: (current: Credential | undefined) => Promise<Credential | undefined>): Promise<Credential | undefined>;
16
+ delete(providerId: string): Promise<void>;
17
+ }
18
+ //#endregion
19
+ //#region src/auth.d.ts
20
+ interface CodexAuthStatus {
21
+ authenticated: boolean;
22
+ expiresAt?: Date;
23
+ }
24
+ /** Start provider-native ChatGPT OAuth and persist its credential. */
25
+ declare function loginCodex(interaction: AuthInteraction, store?: CodexCredentialStore): Promise<void>;
26
+ /** Delete the plugin-owned Codex credential. */
27
+ declare function logoutCodex(store?: CodexCredentialStore): Promise<void>;
28
+ /** Read login state without refreshing the token. */
29
+ declare function codexAuthStatus(store?: CodexCredentialStore): Promise<CodexAuthStatus>;
30
+ //#endregion
31
+ export { CODEX_AUTH_FILENAME as a, codexAuthPath as c, logoutCodex as i, codexAuthStatus as n, CODEX_PROVIDER as o, loginCodex as r, CodexCredentialStore as s, CodexAuthStatus as t };
@@ -0,0 +1,142 @@
1
+ import { createModels } from "@earendil-works/pi-ai";
2
+ import { openaiCodexProvider } from "@earendil-works/pi-ai/providers/openai-codex";
3
+ import { mkdir, readFile, rm, stat } from "node:fs/promises";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
6
+ import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
7
+ //#region src/store.ts
8
+ /** Owner-only persistent storage for the shared OpenAI Codex OAuth document. */
9
+ /** pi-ai provider id used by ChatGPT Codex OAuth. */
10
+ const CODEX_PROVIDER = "openai-codex";
11
+ /** Kept compatible with dsh-codex-connect so a login can be reused. */
12
+ const CODEX_AUTH_FILENAME = ".openai-codex-auth.json";
13
+ const AUTH_FORMAT_VERSION = 1;
14
+ function isENOENT(error) {
15
+ return error?.code === "ENOENT";
16
+ }
17
+ async function assertOwnerOnly(filename) {
18
+ let mode;
19
+ try {
20
+ mode = (await stat(filename)).mode;
21
+ } catch (error) {
22
+ if (isENOENT(error)) return;
23
+ throw error;
24
+ }
25
+ if (process.platform === "win32") return;
26
+ if ((mode & 63) !== 0) throw new Error(`codex-auth: ${filename} is readable beyond its owner (mode ${(mode & 511).toString(8)}); run "chmod 600 ${filename}" before starting again`);
27
+ }
28
+ function parseDocument(text, filename) {
29
+ let value;
30
+ try {
31
+ value = JSON.parse(text);
32
+ } catch {
33
+ throw new Error(`codex-auth: ${filename} is not valid JSON`);
34
+ }
35
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`codex-auth: ${filename} must contain an object`);
36
+ const document = value;
37
+ if (document["version"] !== AUTH_FORMAT_VERSION) throw new Error(`codex-auth: ${filename} has unsupported auth format version ${String(document["version"])}`);
38
+ if (Object.keys(document).some((key) => key !== "version" && key !== "credential")) throw new Error(`codex-auth: ${filename} contains an unknown top-level field`);
39
+ const raw = document["credential"];
40
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) throw new Error(`codex-auth: ${filename} credential must be an object`);
41
+ const credential = raw;
42
+ if (Object.keys(credential).some((key) => ![
43
+ "type",
44
+ "access",
45
+ "refresh",
46
+ "expires",
47
+ "accountId"
48
+ ].includes(key))) throw new Error(`codex-auth: ${filename} credential contains an unknown field`);
49
+ if (credential["type"] !== "oauth") throw new Error(`codex-auth: ${filename} credential type must be oauth`);
50
+ for (const key of [
51
+ "access",
52
+ "refresh",
53
+ "accountId"
54
+ ]) if (typeof credential[key] !== "string" || credential[key].length === 0) throw new Error(`codex-auth: ${filename} credential ${key} must be a non-empty string`);
55
+ if (typeof credential["expires"] !== "number" || !Number.isFinite(credential["expires"]) || credential["expires"] <= 0) throw new Error(`codex-auth: ${filename} credential expires must be a positive finite number`);
56
+ return {
57
+ version: AUTH_FORMAT_VERSION,
58
+ credential
59
+ };
60
+ }
61
+ function codexAuthPath(dshHome) {
62
+ return resolve(join(resolveDshHome(dshHome), CODEX_AUTH_FILENAME));
63
+ }
64
+ /** File-backed CredentialStore that owns only the Codex OAuth route. */
65
+ var CodexCredentialStore = class {
66
+ filename;
67
+ constructor(filename = codexAuthPath()) {
68
+ this.filename = resolve(filename);
69
+ }
70
+ async readCurrent() {
71
+ await assertOwnerOnly(this.filename);
72
+ let text;
73
+ try {
74
+ text = await readFile(this.filename, "utf8");
75
+ } catch (error) {
76
+ if (isENOENT(error)) return void 0;
77
+ throw error;
78
+ }
79
+ return structuredClone(parseDocument(text, this.filename).credential);
80
+ }
81
+ async read(providerId) {
82
+ return providerId === "openai-codex" ? this.readCurrent() : void 0;
83
+ }
84
+ async list() {
85
+ return await this.readCurrent() === void 0 ? [] : [{
86
+ providerId: CODEX_PROVIDER,
87
+ type: "oauth"
88
+ }];
89
+ }
90
+ async modify(providerId, fn) {
91
+ if (providerId !== "openai-codex") throw new Error(`codex-auth: credential store does not own provider "${providerId}"`);
92
+ await mkdir(dirname(this.filename), {
93
+ recursive: true,
94
+ mode: 448
95
+ });
96
+ return withFileLock(this.filename, async () => {
97
+ const current = await this.readCurrent();
98
+ const candidate = await fn(current);
99
+ if (candidate === void 0) return current;
100
+ const document = parseDocument(JSON.stringify({
101
+ version: AUTH_FORMAT_VERSION,
102
+ credential: candidate
103
+ }), this.filename);
104
+ await writeFileAtomic(this.filename, `${JSON.stringify(document, null, 2)}\n`, {
105
+ mode: 384,
106
+ dirMode: 448
107
+ });
108
+ return structuredClone(document.credential);
109
+ });
110
+ }
111
+ async delete(providerId) {
112
+ if (providerId !== "openai-codex") return;
113
+ await mkdir(dirname(this.filename), {
114
+ recursive: true,
115
+ mode: 448
116
+ });
117
+ await withFileLock(this.filename, () => rm(this.filename, { force: true }));
118
+ }
119
+ };
120
+ //#endregion
121
+ //#region src/auth.ts
122
+ /** ChatGPT OAuth orchestration used by the Host routes and CLI consumers. */
123
+ /** Start provider-native ChatGPT OAuth and persist its credential. */
124
+ async function loginCodex(interaction, store = new CodexCredentialStore()) {
125
+ const models = createModels({ credentials: store });
126
+ models.setProvider(openaiCodexProvider());
127
+ await models.login(CODEX_PROVIDER, "oauth", interaction);
128
+ }
129
+ /** Delete the plugin-owned Codex credential. */
130
+ async function logoutCodex(store = new CodexCredentialStore()) {
131
+ await store.delete(CODEX_PROVIDER);
132
+ }
133
+ /** Read login state without refreshing the token. */
134
+ async function codexAuthStatus(store = new CodexCredentialStore()) {
135
+ const credential = await store.read(CODEX_PROVIDER);
136
+ return credential?.type === "oauth" ? {
137
+ authenticated: true,
138
+ expiresAt: new Date(credential.expires)
139
+ } : { authenticated: false };
140
+ }
141
+ //#endregion
142
+ export { CODEX_PROVIDER as a, CODEX_AUTH_FILENAME as i, loginCodex as n, CodexCredentialStore as o, logoutCodex as r, codexAuthPath as s, codexAuthStatus as t };
@@ -0,0 +1,10 @@
1
+ //#region src/auth-paths.d.ts
2
+ /** Browser-facing paths owned by the standalone Codex authentication plugin. */
3
+ /** Host settings namespace used to dispatch the browser settings card. */
4
+ declare const CODEX_AUTH_SETTINGS_NAMESPACE = "dsh-codex-auth";
5
+ declare const CODEX_AUTH_STATUS_PATH = "/plugins/dsh-codex-auth-plugin/auth/status";
6
+ declare const CODEX_AUTH_LOGIN_PATH = "/plugins/dsh-codex-auth-plugin/auth/login";
7
+ declare const CODEX_AUTH_LOGOUT_PATH = "/plugins/dsh-codex-auth-plugin/auth/logout";
8
+ declare const CODEX_USAGE_PATH = "/plugins/dsh-codex-auth-plugin/auth/usage";
9
+ //#endregion
10
+ export { CODEX_AUTH_LOGIN_PATH, CODEX_AUTH_LOGOUT_PATH, CODEX_AUTH_SETTINGS_NAMESPACE, CODEX_AUTH_STATUS_PATH, CODEX_USAGE_PATH };
@@ -0,0 +1,10 @@
1
+ //#region src/auth-paths.ts
2
+ /** Browser-facing paths owned by the standalone Codex authentication plugin. */
3
+ /** Host settings namespace used to dispatch the browser settings card. */
4
+ const CODEX_AUTH_SETTINGS_NAMESPACE = "dsh-codex-auth";
5
+ const CODEX_AUTH_STATUS_PATH = "/plugins/dsh-codex-auth-plugin/auth/status";
6
+ const CODEX_AUTH_LOGIN_PATH = "/plugins/dsh-codex-auth-plugin/auth/login";
7
+ const CODEX_AUTH_LOGOUT_PATH = "/plugins/dsh-codex-auth-plugin/auth/logout";
8
+ const CODEX_USAGE_PATH = "/plugins/dsh-codex-auth-plugin/auth/usage";
9
+ //#endregion
10
+ export { CODEX_AUTH_LOGIN_PATH, CODEX_AUTH_LOGOUT_PATH, CODEX_AUTH_SETTINGS_NAMESPACE, CODEX_AUTH_STATUS_PATH, CODEX_USAGE_PATH };
package/lib/auth.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import { i as logoutCodex, n as codexAuthStatus, r as loginCodex, t as CodexAuthStatus } from "./auth-BBBeI24-.js";
2
+ export { CodexAuthStatus, codexAuthStatus, loginCodex, logoutCodex };
package/lib/auth.js ADDED
@@ -0,0 +1,2 @@
1
+ import { n as loginCodex, r as logoutCodex, t as codexAuthStatus } from "./auth-DQ2F4vc6.js";
2
+ export { codexAuthStatus, loginCodex, logoutCodex };