@sunshaobei/dsh-codex-auth 0.0.0-stage → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/client.ts ADDED
@@ -0,0 +1,416 @@
1
+ /**
2
+ * dsh-codex-auth — client half (browser bundle, served at
3
+ * /plugins/dsh-codex-auth/client.js through the `dsh.client` manifest).
4
+ *
5
+ * Registers:
6
+ * 1. settings.section "codex-subscription" — Codex 订阅 page: status,
7
+ * expiry, authorize / re-authorize / logout.
8
+ * 2. shell.overlay "codex-auth-expired-modal" — pops the re-auth dialog
9
+ * when the active session's model routes to openai-codex while the
10
+ * credential is expired (snoozes 10 minutes on dismiss).
11
+ *
12
+ * Talks to the host half over the loopback Connection RPC channel
13
+ * `/dsh-codex-auth` (status / login / logout).
14
+ */
15
+ (window as any).__ModuleLoader__.load({
16
+ id: "dsh-codex-auth",
17
+ factory: (require) => {
18
+ var module = { exports: {} as any };
19
+ var exports = module.exports;
20
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
21
+ var React = require("react");
22
+
23
+ var CHANNEL = "/dsh-codex-auth";
24
+ var NS = "codex-auth";
25
+ var PROVIDER = "openai-codex";
26
+ var SNOOZE_MS = 10 * 60 * 1000;
27
+
28
+ var DICT = {
29
+ zh: {
30
+ nav: "Codex 订阅",
31
+ title: "Codex 订阅(ChatGPT 授权)",
32
+ desc: "通过 OpenAI OAuth 授权使用 Codex 订阅内的 GPT 模型,无需 API key。凭证保存在本机 ~/.dsh/.credentials.yaml。",
33
+ status: "授权状态",
34
+ active: "已授权",
35
+ expired: "已过期",
36
+ missing: "未授权",
37
+ failed: "授权失败",
38
+ checking: "检查中…",
39
+ account: "授权账号",
40
+ plan: "订阅类型",
41
+ validUntil: "有效期至",
42
+ expiredAt: "已于 {time} 过期",
43
+ leftDays: "{days} 天后过期",
44
+ leftHours: "{hours} 小时后过期",
45
+ authorize: "授权 Codex",
46
+ reauthorize: "重新授权",
47
+ logout: "取消授权",
48
+ waitingBrowser: "已打开浏览器,等待授权完成…(最多 5 分钟)",
49
+ loginDone: "授权成功",
50
+ modelsReady: "已添加 {count} 个 Codex 模型,可在会话中选择",
51
+ modelsFailed: "授权已保存,模型添加失败:{error}",
52
+ loginFailed: "授权失败:{error}",
53
+ logoutFailed: "取消授权失败:{error}",
54
+ loadFailed: "无法读取授权状态",
55
+ neverUsed: "尚未授权过",
56
+ modalTitle: "Codex 授权已过期",
57
+ modalBody: "当前会话使用的 GPT 模型依赖 Codex 订阅授权,授权已于 {time} 过期。重新授权后即可继续使用。",
58
+ modalAuthorize: "重新授权",
59
+ modalLater: "稍后再说",
60
+ cancelLogin: "取消本次登录",
61
+ browserHelp: "如果浏览器显示 403 或验证失败,本应用无法读取该页面错误。可取消本次登录后重试;已有授权会保留。",
62
+ authorizing: "授权中…"
63
+ },
64
+ en: {
65
+ nav: "Codex Subscription",
66
+ title: "Codex Subscription (ChatGPT OAuth)",
67
+ desc: "Use the GPT models included in your Codex subscription via OpenAI OAuth — no API key required. The credential is stored locally in ~/.dsh/.credentials.yaml.",
68
+ status: "Status",
69
+ active: "Authorized",
70
+ expired: "Expired",
71
+ missing: "Not authorized",
72
+ failed: "Authorization failed",
73
+ checking: "Checking…",
74
+ account: "Account",
75
+ plan: "Plan",
76
+ validUntil: "Valid until",
77
+ expiredAt: "Expired at {time}",
78
+ leftDays: "Expires in {days} days",
79
+ leftHours: "Expires in {hours} h",
80
+ authorize: "Authorize Codex",
81
+ reauthorize: "Re-authorize",
82
+ logout: "Sign out",
83
+ waitingBrowser: "Browser opened — waiting for authorization… (up to 5 min)",
84
+ loginDone: "Authorized",
85
+ modelsReady: "{count} Codex models are ready to select",
86
+ modelsFailed: "Authorized, but model setup failed: {error}",
87
+ loginFailed: "Authorization failed: {error}",
88
+ logoutFailed: "Sign out failed: {error}",
89
+ loadFailed: "Cannot read authorization status",
90
+ neverUsed: "Never authorized",
91
+ modalTitle: "Codex authorization expired",
92
+ modalBody: "The GPT model of this session relies on your Codex subscription authorization, which expired at {time}. Re-authorize to continue.",
93
+ modalAuthorize: "Re-authorize",
94
+ modalLater: "Later",
95
+ cancelLogin: "Cancel login",
96
+ browserHelp: "If the browser shows a 403 or verification error, cancel this login and retry. This app cannot read browser errors; your existing authorization is preserved.",
97
+ authorizing: "Authorizing…"
98
+ }
99
+ };
100
+
101
+ function fmtTime(ts: number) {
102
+ if (!ts) return "—";
103
+ var d = new Date(ts);
104
+ var pad = function (n: number) { return (n < 10 ? "0" : "") + n; };
105
+ return d.getFullYear() + "-" + pad(d.getMonth() + 1) + "-" + pad(d.getDate()) + " " + pad(d.getHours()) + ":" + pad(d.getMinutes());
106
+ }
107
+
108
+ function tpl(s: string, vars: Record<string, any>) {
109
+ return s.replace(/\{(\w+)\}/g, (_, k) => String(vars[k] ?? ""));
110
+ }
111
+
112
+ function statusText(t: any, st: any) {
113
+ if (!st || st.configured === undefined) return t("checking");
114
+ if (!st.configured) return t("missing");
115
+ return st.expired ? t("expired") : t("active");
116
+ }
117
+
118
+ function validityText(t: any, st: any) {
119
+ if (!st || !st.configured) return st && st.configured === false ? t("neverUsed") : "—";
120
+ if (!st.expires) return "—";
121
+ if (st.expired) return tpl(t("expiredAt"), { time: fmtTime(st.expires) });
122
+ var ms = st.msLeft || 0;
123
+ if (ms > 36 * 3600 * 1000) return tpl(t("leftDays"), { days: Math.round(ms / 86400000) }) + "(" + fmtTime(st.expires) + ")";
124
+ return tpl(t("leftHours"), { hours: Math.round(ms / 3600000) }) + "(" + fmtTime(st.expires) + ")";
125
+ }
126
+
127
+ // ------------------------------------------------------------------
128
+ // styles (Harness design tokens)
129
+ // ------------------------------------------------------------------
130
+ // OpenAI mark: Simple Icons v14 (CC0), https://github.com/simple-icons/simple-icons/blob/14.0.0/icons/openai.svg
131
+ var CSS = `
132
+ .codex-auth-nav > svg{display:none!important}
133
+ .codex-auth-nav::before{content:"";display:block;flex:none;width:16px;height:16px;background:currentColor;mask:url("data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHZpZXdCb3g9IjAgMCAyNCAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48dGl0bGU+T3BlbkFJPC90aXRsZT48cGF0aCBkPSJNMjIuMjgxOSA5LjgyMTFhNS45ODQ3IDUuOTg0NyAwIDAgMC0uNTE1Ny00LjkxMDggNi4wNDYyIDYuMDQ2MiAwIDAgMC02LjUwOTgtMi45QTYuMDY1MSA2LjA2NTEgMCAwIDAgNC45ODA3IDQuMTgxOGE1Ljk4NDcgNS45ODQ3IDAgMCAwLTMuOTk3NyAyLjkgNi4wNDYyIDYuMDQ2MiAwIDAgMCAuNzQyNyA3LjA5NjYgNS45OCA1Ljk4IDAgMCAwIC41MTEgNC45MTA3IDYuMDUxIDYuMDUxIDAgMCAwIDYuNTE0NiAyLjkwMDFBNS45ODQ3IDUuOTg0NyAwIDAgMCAxMy4yNTk5IDI0YTYuMDU1NyA2LjA1NTcgMCAwIDAgNS43NzE4LTQuMjA1OCA1Ljk4OTQgNS45ODk0IDAgMCAwIDMuOTk3Ny0yLjkwMDEgNi4wNTU3IDYuMDU1NyAwIDAgMC0uNzQ3NS03LjA3Mjl6bS05LjAyMiAxMi42MDgxYTQuNDc1NSA0LjQ3NTUgMCAwIDEtMi44NzY0LTEuMDQwOGwuMTQxOS0uMDgwNCA0Ljc3ODMtMi43NTgyYS43OTQ4Ljc5NDggMCAwIDAgLjM5MjctLjY4MTN2LTYuNzM2OWwyLjAyIDEuMTY4NmEuMDcxLjA3MSAwIDAgMSAuMDM4LjA1MnY1LjU4MjZhNC41MDQgNC41MDQgMCAwIDEtNC40OTQ1IDQuNDk0NHptLTkuNjYwNy00LjEyNTRhNC40NzA4IDQuNDcwOCAwIDAgMS0uNTM0Ni0zLjAxMzdsLjE0Mi4wODUyIDQuNzgzIDIuNzU4MmEuNzcxMi43NzEyIDAgMCAwIC43ODA2IDBsNS44NDI4LTMuMzY4NXYyLjMzMjRhLjA4MDQuMDgwNCAwIDAgMS0uMDMzMi4wNjE1TDkuNzQgMTkuOTUwMmE0LjQ5OTIgNC40OTkyIDAgMCAxLTYuMTQwOC0xLjY0NjR6TTIuMzQwOCA3Ljg5NTZhNC40ODUgNC40ODUgMCAwIDEgMi4zNjU1LTEuOTcyOFYxMS42YS43NjY0Ljc2NjQgMCAwIDAgLjM4NzkuNjc2NWw1LjgxNDQgMy4zNTQzLTIuMDIwMSAxLjE2ODVhLjA3NTcuMDc1NyAwIDAgMS0uMDcxIDBsLTQuODMwMy0yLjc4NjVBNC41MDQgNC41MDQgMCAwIDEgMi4zNDA4IDcuODcyem0xNi41OTYzIDMuODU1OEwxMy4xMDM4IDguMzY0IDE1LjExOTIgNy4yYS4wNzU3LjA3NTcgMCAwIDEgLjA3MSAwbDQuODMwMyAyLjc5MTNhNC40OTQ0IDQuNDk0NCAwIDAgMS0uNjc2NSA4LjEwNDJ2LTUuNjc3MmEuNzkuNzkgMCAwIDAtLjQwNy0uNjY3em0yLjAxMDctMy4wMjMxbC0uMTQyLS4wODUyLTQuNzczNS0yLjc4MThhLjc3NTkuNzc1OSAwIDAgMC0uNzg1NCAwTDkuNDA5IDkuMjI5N1Y2Ljg5NzRhLjA2NjIuMDY2MiAwIDAgMSAuMDI4NC0uMDYxNWw0LjgzMDMtMi43ODY2YTQuNDk5MiA0LjQ5OTIgMCAwIDEgNi42ODAyIDQuNjZ6TTguMzA2NSAxMi44NjNsLTIuMDItMS4xNjM4YS4wODA0LjA4MDQgMCAwIDEtLjAzOC0uMDU2N1Y2LjA3NDJhNC40OTkyIDQuNDk5MiAwIDAgMSA3LjM3NTctMy40NTM3bC0uMTQyLjA4MDVMOC43MDQgNS40NTlhLjc5NDguNzk0OCAwIDAgMC0uMzkyNy42ODEzem0xLjA5NzYtMi4zNjU0bDIuNjAyLTEuNDk5OCAyLjYwNjkgMS40OTk4djIuOTk5NGwtMi41OTc0IDEuNDk5Ny0yLjYwNjctMS40OTk3WiIvPjwvc3ZnPg==") center/contain no-repeat;-webkit-mask:url("data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHZpZXdCb3g9IjAgMCAyNCAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48dGl0bGU+T3BlbkFJPC90aXRsZT48cGF0aCBkPSJNMjIuMjgxOSA5LjgyMTFhNS45ODQ3IDUuOTg0NyAwIDAgMC0uNTE1Ny00LjkxMDggNi4wNDYyIDYuMDQ2MiAwIDAgMC02LjUwOTgtMi45QTYuMDY1MSA2LjA2NTEgMCAwIDAgNC45ODA3IDQuMTgxOGE1Ljk4NDcgNS45ODQ3IDAgMCAwLTMuOTk3NyAyLjkgNi4wNDYyIDYuMDQ2MiAwIDAgMCAuNzQyNyA3LjA5NjYgNS45OCA1Ljk4IDAgMCAwIC41MTEgNC45MTA3IDYuMDUxIDYuMDUxIDAgMCAwIDYuNTE0NiAyLjkwMDFBNS45ODQ3IDUuOTg0NyAwIDAgMCAxMy4yNTk5IDI0YTYuMDU1NyA2LjA1NTcgMCAwIDAgNS43NzE4LTQuMjA1OCA1Ljk4OTQgNS45ODk0IDAgMCAwIDMuOTk3Ny0yLjkwMDEgNi4wNTU3IDYuMDU1NyAwIDAgMC0uNzQ3NS03LjA3Mjl6bS05LjAyMiAxMi42MDgxYTQuNDc1NSA0LjQ3NTUgMCAwIDEtMi44NzY0LTEuMDQwOGwuMTQxOS0uMDgwNCA0Ljc3ODMtMi43NTgyYS43OTQ4Ljc5NDggMCAwIDAgLjM5MjctLjY4MTN2LTYuNzM2OWwyLjAyIDEuMTY4NmEuMDcxLjA3MSAwIDAgMSAuMDM4LjA1MnY1LjU4MjZhNC41MDQgNC41MDQgMCAwIDEtNC40OTQ1IDQuNDk0NHptLTkuNjYwNy00LjEyNTRhNC40NzA4IDQuNDcwOCAwIDAgMS0uNTM0Ni0zLjAxMzdsLjE0Mi4wODUyIDQuNzgzIDIuNzU4MmEuNzcxMi43NzEyIDAgMCAwIC43ODA2IDBsNS44NDI4LTMuMzY4NXYyLjMzMjRhLjA4MDQuMDgwNCAwIDAgMS0uMDMzMi4wNjE1TDkuNzQgMTkuOTUwMmE0LjQ5OTIgNC40OTkyIDAgMCAxLTYuMTQwOC0xLjY0NjR6TTIuMzQwOCA3Ljg5NTZhNC40ODUgNC40ODUgMCAwIDEgMi4zNjU1LTEuOTcyOFYxMS42YS43NjY0Ljc2NjQgMCAwIDAgLjM4NzkuNjc2NWw1LjgxNDQgMy4zNTQzLTIuMDIwMSAxLjE2ODVhLjA3NTcuMDc1NyAwIDAgMS0uMDcxIDBsLTQuODMwMy0yLjc4NjVBNC41MDQgNC41MDQgMCAwIDEgMi4zNDA4IDcuODcyem0xNi41OTYzIDMuODU1OEwxMy4xMDM4IDguMzY0IDE1LjExOTIgNy4yYS4wNzU3LjA3NTcgMCAwIDEgLjA3MSAwbDQuODMwMyAyLjc5MTNhNC40OTQ0IDQuNDk0NCAwIDAgMS0uNjc2NSA4LjEwNDJ2LTUuNjc3MmEuNzkuNzkgMCAwIDAtLjQwNy0uNjY3em0yLjAxMDctMy4wMjMxbC0uMTQyLS4wODUyLTQuNzczNS0yLjc4MThhLjc3NTkuNzc1OSAwIDAgMC0uNzg1NCAwTDkuNDA5IDkuMjI5N1Y2Ljg5NzRhLjA2NjIuMDY2MiAwIDAgMSAuMDI4NC0uMDYxNWw0LjgzMDMtMi43ODY2YTQuNDk5MiA0LjQ5OTIgMCAwIDEgNi42ODAyIDQuNjZ6TTguMzA2NSAxMi44NjNsLTIuMDItMS4xNjM4YS4wODA0LjA4MDQgMCAwIDEtLjAzOC0uMDU2N1Y2LjA3NDJhNC40OTkyIDQuNDk5MiAwIDAgMSA3LjM3NTctMy40NTM3bC0uMTQyLjA4MDVMOC43MDQgNS40NTlhLjc5NDguNzk0OCAwIDAgMC0uMzkyNy42ODEzem0xLjA5NzYtMi4zNjU0bDIuNjAyLTEuNDk5OCAyLjYwNjkgMS40OTk4djIuOTk5NGwtMi41OTc0IDEuNDk5Ny0yLjYwNjctMS40OTk3WiIvPjwvc3ZnPg==") center/contain no-repeat}
134
+
135
+ .codex-auth-card{border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border-l2,#ddd));border-radius:10px;padding:16px;display:flex;flex-direction:column;gap:12px;width:100%;min-width:0;box-sizing:border-box;background:var(--dsw-alias-bg-layer-2,#fff)}
136
+ .codex-auth-head{display:flex;flex-wrap:wrap;align-items:center;gap:10px}
137
+ .codex-auth-title{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary,#111)}
138
+ .codex-auth-pill{font-size:11px;padding:2px 8px;border-radius:999px;border:1px solid}
139
+ .codex-auth-pill.ok{color:#0a7d38;border-color:#0a7d3855;background:#0a7d3811}
140
+ .codex-auth-pill.bad{color:#c62828;border-color:#c6282855;background:#c6282811}
141
+ .codex-auth-pill.idle{color:#888;border-color:#88888855;background:#88888811}
142
+ .codex-auth-desc{font-size:12px;color:var(--dsw-alias-label-secondary,#666);line-height:1.5}
143
+ .codex-auth-grid{display:grid;grid-template-columns:96px minmax(0,1fr);gap:6px 12px;font-size:12.5px}
144
+ .codex-auth-grid dt{color:var(--dsw-alias-label-secondary,#666)}
145
+ .codex-auth-grid dd{margin:0;color:var(--dsw-alias-label-primary,#111);word-break:break-all}
146
+ .codex-auth-actions{display:flex;gap:8px;flex-wrap:wrap}
147
+ .codex-auth-btn{border:1px solid var(--dsw-alias-border-l2,#ccc);background:var(--dsw-alias-bg-layer-2,#fff);border-radius:8px;padding:6px 14px;font-size:12.5px;cursor:pointer;color:var(--dsw-alias-label-primary,#111)}
148
+ .codex-auth-btn.primary{background:var(--dsw-static-deepseek-500,#4d6bfe);border-color:var(--dsw-static-deepseek-500,#4d6bfe);color:#fff}
149
+ .codex-auth-btn.danger{color:#c62828;border-color:#c6282866}
150
+ .codex-auth-btn:disabled{opacity:.55;cursor:default}
151
+ .codex-auth-msg{overflow-wrap:anywhere;font-size:12px;line-height:1.5}
152
+ .codex-auth-msg.err{color:#c62828}
153
+ .codex-auth-msg.ok{color:#0a7d38}
154
+ .codex-auth-overlay{position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,.45);display:grid;place-items:center}
155
+ .codex-auth-dialog{background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,#111);border-radius:14px;box-shadow:var(--dsw-shadow-modal,0 12px 40px rgba(0,0,0,.3));padding:22px;width:400px;max-width:90vw;display:flex;flex-direction:column;gap:12px}
156
+ .codex-auth-dialog h3{margin:0;font-size:15px}
157
+ .codex-auth-dialog p{margin:0;font-size:12.5px;line-height:1.6;color:var(--dsw-alias-label-secondary,#555)}
158
+ .codex-auth-dialog .codex-auth-actions{justify-content:flex-end}
159
+ `;
160
+
161
+ // ------------------------------------------------------------------
162
+ // shared stores (module scope so both slots see the same state)
163
+ // ------------------------------------------------------------------
164
+ var loginStore = { phase: "idle", error: "", listeners: new Set<Function>() };
165
+ function updateLogin(login: any) {
166
+ if (!login) return;
167
+ Object.assign(loginStore, login);
168
+ loginStore.listeners.forEach(l => l());
169
+ setModal({ busy: login.phase === "waiting" || login.phase === "exchanging" });
170
+ if (login.phase === "done") setModal({ open: false });
171
+ }
172
+ var statusStore = { value: null as any, listeners: new Set<Function>() };
173
+ function setStatus(v: any) {
174
+ statusStore.value = v;
175
+ statusStore.listeners.forEach((l) => l(v));
176
+ }
177
+
178
+ var modalStore = { open: false, busy: false, listeners: new Set<Function>() };
179
+ function setModal(patch: any) {
180
+ Object.assign(modalStore, patch);
181
+ modalStore.listeners.forEach((l) => l(modalStore));
182
+ }
183
+ var snoozedUntil = 0;
184
+
185
+ function useStore(store: any) {
186
+ var [, force] = React.useReducer((n: number) => n + 1, 0);
187
+ React.useEffect(() => {
188
+ var l = () => force();
189
+ store.listeners.add(l);
190
+ return () => { store.listeners.delete(l); };
191
+ }, [store]);
192
+ return store;
193
+ }
194
+
195
+ // ------------------------------------------------------------------
196
+ // components
197
+ // ------------------------------------------------------------------
198
+ function StatusCard(props: any) {
199
+ var t = props.t;
200
+ var call = props.call;
201
+ var st = useStore(statusStore).value;
202
+ var [msg, setMsg] = React.useState<{ kind: string; text: string } | null>(null);
203
+ var [busy, setBusy] = React.useState<"" | "login" | "logout">("");
204
+
205
+ var login = useStore(loginStore);
206
+ var pending = login.phase === "waiting" || login.phase === "exchanging";
207
+ var refresh = React.useCallback(() => {
208
+ call("status", null).then(() => setMsg(null)).catch((e: any) => setMsg({ kind: "err", text: String(e.message || e) }));
209
+ }, [call]);
210
+ React.useEffect(() => { refresh(); }, [refresh]);
211
+ var authorize = React.useCallback(() => {
212
+ setMsg(null);
213
+ updateLogin({ phase: "waiting", error: "" });
214
+ call("login", null).catch((e: any) => updateLogin({ phase: "error", error: String(e.message || e) }));
215
+ }, [call]);
216
+ var loginMessage = pending ? { kind: "wait", text: login.phase === "exchanging" ? "正在验证并保存凭证…" : t("waitingBrowser") }
217
+ : login.phase === "error" ? { kind: "err", text: tpl(t("loginFailed"), { error: login.error }) }
218
+ : login.phase === "done" ? { kind: "ok", text: t("loginDone") } : null;
219
+ msg = msg || loginMessage;
220
+
221
+ var logout = React.useCallback(() => {
222
+ setBusy("logout");
223
+ call("logout", null).then((r: any) => {
224
+ setBusy("");
225
+ if (r && r.status) setStatus(r.status);
226
+ setMsg(null);
227
+ }).catch((e: any) => {
228
+ setBusy("");
229
+ setMsg({ kind: "err", text: tpl(t("logoutFailed"), { error: String(e && e.message || e) }) });
230
+ });
231
+ }, [call, t]);
232
+
233
+ var configured = !!(st && st.configured);
234
+ var expired = !!(st && st.expired);
235
+ var pillClass = !st || st.configured === undefined ? "idle" : !configured ? "idle" : expired ? "bad" : "ok";
236
+
237
+ return React.createElement("div", { className: "codex-auth-card" },
238
+ React.createElement("div", { className: "codex-auth-head" },
239
+ React.createElement("span", { className: "codex-auth-title" }, t("title")),
240
+ React.createElement("span", { className: "codex-auth-pill " + pillClass }, pending ? t("authorizing") : login.phase === "error" ? t("failed") : statusText(t, st))),
241
+ React.createElement("div", { className: "codex-auth-desc" }, t("desc")),
242
+ React.createElement("dl", { className: "codex-auth-grid" },
243
+ React.createElement("dt", null, t("status")), React.createElement("dd", null, statusText(t, st)),
244
+ React.createElement("dt", null, t("validUntil")), React.createElement("dd", null, validityText(t, st)),
245
+ st && st.email ? React.createElement("dt", null, t("account")) : null,
246
+ st && st.email ? React.createElement("dd", null, st.email + (st.accountId ? " · " + st.accountId.slice(0, 8) : "")) : null,
247
+ st && st.plan ? React.createElement("dt", null, t("plan")) : null,
248
+ st && st.plan ? React.createElement("dd", null, String(st.plan).toUpperCase()) : null),
249
+ React.createElement("div", { className: "codex-auth-actions" },
250
+ React.createElement("button", { className: "codex-auth-btn primary", type: "button", disabled: busy !== "" || pending, onClick: authorize },
251
+ pending ? t("authorizing") : configured ? t("reauthorize") : t("authorize")),
252
+ pending ? React.createElement("button", { className: "codex-auth-btn", type: "button", disabled: login.phase === "exchanging", onClick: () => {
253
+ call("cancel", null).then(() => setMsg(null)).catch((e: any) => setMsg({ kind: "err", text: String(e.message || e) }));
254
+ } }, t("cancelLogin")) : null,
255
+ configured ? React.createElement("button", { className: "codex-auth-btn danger", type: "button", disabled: busy !== "" || pending, onClick: logout }, t("logout")) : null),
256
+ pending ? React.createElement("div", { className: "codex-auth-desc" }, t("browserHelp")) : null,
257
+ msg ? React.createElement("div", { role: "status", "aria-live": "polite", className: "codex-auth-msg " + (msg.kind === "err" ? "err" : msg.kind === "ok" ? "ok" : "") }, msg.text) : null,
258
+ st && st.models ? React.createElement("div", { role: "status", className: "codex-auth-msg " + (st.models.ready ? "ok" : "err") }, st.models.ready ? tpl(t("modelsReady"), { count: st.models.count }) : tpl(t("modelsFailed"), { error: st.models.error })) : null,
259
+ st && st.error ? React.createElement("div", { className: "codex-auth-msg err" }, t("loadFailed") + ": " + String(st.error)) : null,
260
+ React.createElement("div", { style: { textAlign: "right" } },
261
+ React.createElement("button", { className: "codex-auth-btn", type: "button", onClick: refresh, "aria-label": "刷新授权状态" }, "↻")));
262
+ }
263
+
264
+ function ExpiredModal(props: any) {
265
+ var t = props.t;
266
+ var call = props.call;
267
+ var modal = useStore(modalStore);
268
+ var login = useStore(loginStore);
269
+ if (!modal.open) return null;
270
+ var st = statusStore.value;
271
+ var authorize = () => {
272
+ updateLogin({ phase: "waiting", error: "" });
273
+ call("login", null).catch((e: any) => updateLogin({ phase: "error", error: String(e.message || e) }));
274
+ };
275
+ return React.createElement("div", { className: "codex-auth-overlay" },
276
+ React.createElement("div", { className: "codex-auth-dialog", role: "dialog", "aria-modal": "true" },
277
+ React.createElement("h3", null, t("modalTitle")),
278
+ login.phase === "error" ? React.createElement("p", { role: "alert", className: "codex-auth-msg err" }, tpl(t("loginFailed"), { error: login.error })) : null,
279
+ React.createElement("p", null, tpl(t("modalBody"), { time: st && st.expires ? fmtTime(st.expires) : "—" })),
280
+ React.createElement("div", { className: "codex-auth-actions" },
281
+ React.createElement("button", { className: "codex-auth-btn", type: "button", disabled: modal.busy, onClick: () => { snoozedUntil = Date.now() + SNOOZE_MS; setModal({ open: false }); } }, t("modalLater")),
282
+ React.createElement("button", { className: "codex-auth-btn primary", type: "button", disabled: modal.busy, onClick: authorize }, modal.busy ? t("authorizing") : t("modalAuthorize")))));
283
+ }
284
+
285
+ // ------------------------------------------------------------------
286
+ // apply
287
+ // ------------------------------------------------------------------
288
+ const inject = ["slots", "timer", "connection", "locale", "remote"];
289
+
290
+ function apply(ctx: any) {
291
+ var call = async (endpoint: string, payload: any) => {
292
+ let timeout: any;
293
+ try {
294
+ const result: any = await Promise.race([
295
+ ctx.connection.rpc.call(CHANNEL, endpoint, payload),
296
+ new Promise((_, reject) => { timeout = setTimeout(() => reject(new Error("连接授权服务超时,请刷新重试")), 12000); })
297
+ ]);
298
+ if (!result || result.ok !== true) throw new Error(result?.error?.message || "授权服务返回无效响应");
299
+ const value = result.value;
300
+ if (!value || typeof value !== "object" || !value.login || (endpoint !== "login" && typeof value.status?.configured !== "boolean" && !value.status?.error)) {
301
+ throw new Error("授权服务响应格式不匹配,请重启 DeepSeek Harness 加载新版插件");
302
+ }
303
+ if (value.status) setStatus(value.status);
304
+ updateLogin(value.login);
305
+ return value;
306
+ } finally { clearTimeout(timeout); }
307
+ };
308
+
309
+ var refreshStatus = function () {
310
+ return call("status", null).catch((e: any) => { setStatus({ error: String(e.message || e) });
311
+ if (loginStore.phase === "waiting" || loginStore.phase === "exchanging") updateLogin({ phase: "error", error: String(e.message || e) });
312
+ });
313
+ };
314
+
315
+ ctx.effect(function () {
316
+ let stopped = false;
317
+ let timer: any;
318
+ const poll = async () => {
319
+ await refreshStatus();
320
+ if (!stopped) timer = setTimeout(poll, loginStore.phase === "waiting" || loginStore.phase === "exchanging" ? 1000 : 5000);
321
+ };
322
+ const focus = () => { void refreshStatus(); };
323
+ window.addEventListener("focus", focus);
324
+ void poll();
325
+ return () => { stopped = true; clearTimeout(timer); window.removeEventListener("focus", focus); };
326
+ }, "dsh-codex-auth: status polling");
327
+
328
+ // Token refresh (by llm-pi-ai) and our own login/logout both touch
329
+ // the same record; the forwarded remote event lets the UI react
330
+ // instantly instead of waiting for the next poll.
331
+ ctx.effect(function () {
332
+ var off = ctx.remote.$on("credentials/record-updated", function (key: any) {
333
+ if (String(key).indexOf("openai-codex") > -1) refreshStatus();
334
+ });
335
+ return function () { try { off && off(); } catch (_) {} };
336
+ }, "dsh-codex-auth: record-updated listener");
337
+
338
+ ctx.effect(function () {
339
+ var tag = document.createElement("style");
340
+ tag.dataset.plugin = "dsh-codex-auth";
341
+ tag.textContent = CSS;
342
+ document.head.append(tag);
343
+ return function () { tag.remove(); };
344
+ }, "dsh-codex-auth: styles");
345
+
346
+ ctx.effect(function () {
347
+ return ctx.locale.register(NS, DICT);
348
+ }, "dsh-codex-auth: dictionaries");
349
+ var t = ctx.locale.bind(NS);
350
+
351
+ // The host hardcodes navIcon(id) and exposes no icon slot. Scope the
352
+ // replacement to our own settings navigation button and clean it up.
353
+ ctx.effect(function () {
354
+ const marked = new Set<Element>();
355
+ const mark = () => {
356
+ for (const button of document.querySelectorAll('[data-shortcut-modal="settings"] nav button')) {
357
+ if (button.textContent?.trim() === t("nav")) {
358
+ if (!button.classList.contains("codex-auth-nav")) button.classList.add("codex-auth-nav");
359
+ marked.add(button);
360
+ }
361
+ }
362
+ };
363
+ const observer = new MutationObserver(mark);
364
+ observer.observe(document.body, { childList: true, subtree: true });
365
+ mark();
366
+ return () => { observer.disconnect(); marked.forEach(button => button.classList.remove("codex-auth-nav")); };
367
+ }, "dsh-codex-auth: ChatGPT navigation icon");
368
+
369
+ // ---- settings page ----
370
+ ctx.slots.inject("settings.section", () => ctx.slots.register({
371
+ name: "settings.section",
372
+ id: "codex-subscription",
373
+ order: 25,
374
+ label: () => t("nav"),
375
+ locale: NS
376
+ }, () => React.createElement(StatusCard, { t: t, call: call })));
377
+
378
+ // ---- expired modal ----
379
+ ctx.slots.inject("shell.overlay", () => ctx.slots.register({
380
+ name: "shell.overlay",
381
+ id: "codex-auth-expired-modal",
382
+ locale: NS
383
+ }, () => React.createElement(ExpiredModal, { t: t, call: call })));
384
+
385
+ // Watcher: poll status; open the modal when the active session's
386
+ // selected model routes to openai-codex and the credential is
387
+ // expired (same modelDirectories binding as dsh-balance-panel).
388
+ ctx.effect(function () {
389
+ var iv = setInterval(function () {
390
+ var st = statusStore.value;
391
+ if (!st || !st.configured || !st.expired) { setModal({ open: false }); return; }
392
+ var provider = "";
393
+ try {
394
+ var dirs = (ctx as any).modelDirectories;
395
+ var list = (ctx as any).sessions.list() || [];
396
+ list.sort(function (a: any, b: any) {
397
+ return Number(b.updatedAt || b.touchedAt || b.seq || 0) - Number(a.updatedAt || a.touchedAt || a.seq || 0);
398
+ });
399
+ for (var i = 0; i < list.length; i++) {
400
+ var dir = dirs.directoryFor(list[i].id);
401
+ var cur = dir && dir.store && dir.store.getSnapshot && dir.store.getSnapshot();
402
+ cur = cur && (cur.current || cur.next || cur.lastUsed);
403
+ if (cur && cur.provider) { provider = String(cur.provider); break; }
404
+ }
405
+ } catch (_) { /* service unavailable */ }
406
+ if (provider.indexOf(PROVIDER) > -1 && Date.now() > snoozedUntil) setModal({ open: true });
407
+ }, 5000);
408
+ return function () { clearInterval(iv); };
409
+ }, "dsh-codex-auth: expiry watcher");
410
+ }
411
+
412
+ exports.inject = inject;
413
+ exports.apply = apply;
414
+ return module.exports;
415
+ }
416
+ });