dsh-codex-subscription 1.11.4 → 1.12.1-beta.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/AGENTS.md +4 -4
- package/README.en.md +2 -2
- package/README.md +2 -2
- package/compatibility.json +2 -1
- package/lib/client.js +161 -16
- package/lib/index.js +539 -55
- package/package.json +17 -17
package/AGENTS.md
CHANGED
|
@@ -6,7 +6,7 @@ Use this guide when a user asks an Agent to install, update, verify, or remove
|
|
|
6
6
|
## Safety
|
|
7
7
|
|
|
8
8
|
- Confirm the target DSH installation and profile. Use `web` only when it is the user's target.
|
|
9
|
-
- Use the exact `1.
|
|
9
|
+
- Use the exact `1.12.0` package below; do not install a moving branch.
|
|
10
10
|
- Never print OAuth credentials, account IDs, authorization callbacks, or credential-store contents.
|
|
11
11
|
- Preserve the DSH profile, unrelated plugins, sessions, and saved sign-in.
|
|
12
12
|
- Do not start, stop, or restart DSH without explicit permission.
|
|
@@ -34,13 +34,13 @@ For an official DSH installation run through npm, keep the complete `npx` prefix
|
|
|
34
34
|
run command does not create a global `dsh` command:
|
|
35
35
|
|
|
36
36
|
```sh
|
|
37
|
-
npx -y @deepseek-ai/dsh@0.1.1-rc.2 plugin --profile web add dsh-codex-subscription@1.
|
|
37
|
+
npx -y @deepseek-ai/dsh@0.1.1-rc.2 plugin --profile web add dsh-codex-subscription@1.12.0
|
|
38
38
|
```
|
|
39
39
|
|
|
40
40
|
With an existing global `dsh` command:
|
|
41
41
|
|
|
42
42
|
```sh
|
|
43
|
-
dsh plugin --profile web add dsh-codex-subscription@1.
|
|
43
|
+
dsh plugin --profile web add dsh-codex-subscription@1.12.0
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
Use the same `add` command to update or repair. This is the complete package-changing operation.
|
|
@@ -58,7 +58,7 @@ dsh --profile web --dump-config
|
|
|
58
58
|
|
|
59
59
|
For the official npm route, run the same checks with `npx -y @deepseek-ai/dsh@0.1.1-rc.2` in place of `dsh`.
|
|
60
60
|
|
|
61
|
-
1. `dsh-codex-subscription` version `1.
|
|
61
|
+
1. `dsh-codex-subscription` version `1.12.0` appears exactly once.
|
|
62
62
|
2. `codex-subscription` appears exactly once in the composed config.
|
|
63
63
|
3. No unrelated plugin or profile was changed and DSH was not restarted.
|
|
64
64
|
|
package/README.en.md
CHANGED
|
@@ -133,7 +133,7 @@ Restart DSH manually after installation, then:
|
|
|
133
133
|
|
|
134
134
|
## Features
|
|
135
135
|
|
|
136
|
-
- ChatGPT OAuth sign-in with credentials kept on the host;
|
|
136
|
+
- ChatGPT OAuth sign-in with credentials kept on the host; manually add, switch, and remove accounts without automatic rotation or quota pooling;
|
|
137
137
|
- Codex models and Beta image generation/editing directly inside DSH conversations;
|
|
138
138
|
- A clear global choice between DSH default search and Codex subscription search; it applies to every model and session rather than following the selected model;
|
|
139
139
|
- Actual backend-provided quota, reset time, and freshness;
|
|
@@ -153,7 +153,7 @@ The plugin can follow an existing HTTPS proxy from the process environment or op
|
|
|
153
153
|
<img src="https://raw.githubusercontent.com/WSL043/dsh-codex-subscription/main/docs/assets/composer-quota-en.png" width="800" alt="Codex quota inside the composer">
|
|
154
154
|
</p>
|
|
155
155
|
|
|
156
|
-
Choose Off, Percent, Progress bar, or Beta Runway in Settings. The compact display appears only for a selected Codex model. Runway is opt-in and estimates
|
|
156
|
+
Choose Off, Percent, Progress bar, or Beta Runway in Settings. The compact display appears only for a selected Codex model. Runway is opt-in and estimates pace only from official remaining-percentage observations. It needs at least three samples; sustained high use usually produces a range in 5–10 minutes, while low use takes longer or reports a stable state. Non-sensitive observations from the last 24 hours are kept locally so calibration can continue after restart; a quota reset, account switch, or disabling the feature starts a new calibration period.
|
|
157
157
|
Standard Codex uses the lowest remaining window returned by the service; Spark uses its independent quota. The plugin does not hard-code a
|
|
158
158
|
“5-hour + weekly” layout or invent Credits and spending caps that the service did not return.
|
|
159
159
|
|
package/README.md
CHANGED
|
@@ -134,7 +134,7 @@ dsh --profile web --dump-config
|
|
|
134
134
|
|
|
135
135
|
## 功能
|
|
136
136
|
|
|
137
|
-
- ChatGPT OAuth
|
|
137
|
+
- ChatGPT OAuth 登录,凭据保留在本机;可手动添加、切换和移除多个账号,不会自动轮换或合并额度;
|
|
138
138
|
- Codex 模型和 Beta 图片生成与编辑直接出现在 DSH 会话中;
|
|
139
139
|
- 搜索来源是全局设置,可在 DSH 默认搜索与 Codex 订阅搜索之间切换;它对所有模型和会话生效,不会随当前模型自动切换;
|
|
140
140
|
- 设置页显示服务端返回的额度、重置时间和更新时间;
|
|
@@ -154,7 +154,7 @@ dsh --profile web --dump-config
|
|
|
154
154
|
<img src="https://raw.githubusercontent.com/WSL043/dsh-codex-subscription/main/docs/assets/composer-quota.png" width="800" alt="中文 DSH 输入框内的 Codex 剩余额度进度条">
|
|
155
155
|
</p>
|
|
156
156
|
|
|
157
|
-
可在设置中选择关闭、百分比、进度条或 Beta 续航预测;紧凑额度只在选择 Codex
|
|
157
|
+
可在设置中选择关闭、百分比、进度条或 Beta 续航预测;紧凑额度只在选择 Codex 模型时显示。续航预测仅在用户主动选择后,根据官方剩余百分比估算当前消耗速度。它至少需要 3 个样本;持续高消耗时通常 5–10 分钟即可给出范围,消耗较低时会延长观察或显示稳定。最近 24 小时的无敏感信息观测会保存在本机,重启后可以继续校准;额度重置、账号切换或关闭功能会开启新的校准周期。普通 Codex 使用服务端返回窗口中剩余最少的一项,
|
|
158
158
|
Spark 使用独立额度。插件不会写死“5 小时 + 每周”,也不会虚构服务端没有返回的 Credits 或消费上限。
|
|
159
159
|
|
|
160
160
|
### 安全使用额度重置
|
package/compatibility.json
CHANGED
package/lib/client.js
CHANGED
|
@@ -245,6 +245,13 @@ window.__ModuleLoader__.load({
|
|
|
245
245
|
browserLogin: "浏览器登录",
|
|
246
246
|
deviceLogin: "设备代码登录",
|
|
247
247
|
logout: "退出登录",
|
|
248
|
+
addAccount: "添加账号",
|
|
249
|
+
accountLabel: "账号名称",
|
|
250
|
+
switchAccount: "切换",
|
|
251
|
+
removeAccount: "移除",
|
|
252
|
+
removeConfirm: "确认移除",
|
|
253
|
+
removeCancel: "保留",
|
|
254
|
+
signOutAll: "退出全部账号",
|
|
248
255
|
cancel: "取消",
|
|
249
256
|
submit: "提交授权码",
|
|
250
257
|
openLogin: "打开登录页",
|
|
@@ -329,7 +336,7 @@ window.__ModuleLoader__.load({
|
|
|
329
336
|
quickQuotaBar: "进度条",
|
|
330
337
|
quickQuotaForecast: "续航预测",
|
|
331
338
|
quickQuotaBeta: "Beta",
|
|
332
|
-
quickQuotaForecastHint: "
|
|
339
|
+
quickQuotaForecastHint: "按消耗速度自适应校准;高消耗通常 5–10 分钟可估算,低消耗会显示用量稳定。进度会在本机保留。",
|
|
333
340
|
contextTitle: "上下文窗口",
|
|
334
341
|
contextStandard: "标准",
|
|
335
342
|
contextStandardHint: "使用模型目录默认值;官方 Agent 预设会自动管理上下文。",
|
|
@@ -421,6 +428,13 @@ window.__ModuleLoader__.load({
|
|
|
421
428
|
browserLogin: "Browser sign-in",
|
|
422
429
|
deviceLogin: "Device-code sign-in",
|
|
423
430
|
logout: "Sign out",
|
|
431
|
+
addAccount: "Add account",
|
|
432
|
+
accountLabel: "Account name",
|
|
433
|
+
switchAccount: "Switch",
|
|
434
|
+
removeAccount: "Remove",
|
|
435
|
+
removeConfirm: "Confirm remove",
|
|
436
|
+
removeCancel: "Keep",
|
|
437
|
+
signOutAll: "Sign out all",
|
|
424
438
|
cancel: "Cancel",
|
|
425
439
|
submit: "Submit authorization code",
|
|
426
440
|
openLogin: "Open sign-in page",
|
|
@@ -505,7 +519,7 @@ window.__ModuleLoader__.load({
|
|
|
505
519
|
quickQuotaBar: "Progress bar",
|
|
506
520
|
quickQuotaForecast: "Runway",
|
|
507
521
|
quickQuotaBeta: "Beta",
|
|
508
|
-
quickQuotaForecastHint: "
|
|
522
|
+
quickQuotaForecastHint: "Calibrates to actual consumption: high use is usually estimated in 5–10 minutes, while low use is shown as stable. Progress is kept locally.",
|
|
509
523
|
contextTitle: "Context window",
|
|
510
524
|
contextStandard: "Standard",
|
|
511
525
|
contextStandardHint: "Use the model catalog default; official agent presets manage context automatically.",
|
|
@@ -602,6 +616,7 @@ window.__ModuleLoader__.load({
|
|
|
602
616
|
.codexSubscriptionSwitch{position:relative;flex:0 0 auto;width:32px;height:18px;padding:0;border:1px solid var(--dsw-alias-border-l3);border-radius:999px;background:var(--dsw-alias-bg-module-platform);cursor:pointer}.codexSubscriptionSwitch:disabled{cursor:not-allowed;opacity:.5}.codexSubscriptionSwitch[aria-checked=true]{background:var(--dsw-alias-label-secondary);border-color:var(--dsw-alias-label-secondary)}.codexSubscriptionSwitchKnob{position:absolute;top:2px;left:2px;width:12px;height:12px;border-radius:50%;background:var(--dsw-alias-bg-layer-1);transition:transform 120ms var(--ds-ease-in-out)}.codexSubscriptionSwitch[aria-checked=true] .codexSubscriptionSwitchKnob{transform:translateX(14px)}
|
|
603
617
|
.codexSubscriptionSearch{display:flex;flex-direction:column;gap:7px}.codexSubscriptionSearchChoices{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:6px}.codexSubscriptionSearchChoice{display:grid;grid-template-columns:14px minmax(0,1fr);align-items:center;column-gap:8px;min-width:0;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-primary);padding:9px 10px;text-align:left;cursor:pointer}.codexSubscriptionSearchChoice:has(input:disabled){cursor:not-allowed;opacity:.5}.codexSubscriptionSearchChoice:has(input:checked){border-color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2)}.codexSubscriptionSearchChoice:has(input:focus-visible){outline:2px solid var(--dsw-alias-border-l3);outline-offset:2px}.codexSubscriptionSearchInput{width:14px;height:14px;margin:0;accent-color:var(--dsw-alias-label-primary);cursor:inherit}.codexSubscriptionSearchCopy{display:block;min-width:0;pointer-events:none}.codexSubscriptionSearchCopy strong,.codexSubscriptionSearchCopy span{display:block}.codexSubscriptionSearchCopy strong{font-size:12px;line-height:18px;font-weight:500;color:var(--dsw-alias-label-secondary)}.codexSubscriptionSearchChoice:has(input:checked) strong{color:var(--dsw-alias-label-primary)}.codexSubscriptionSearchCopy span{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary)}.codexSubscriptionDivider{height:1px;background:var(--dsw-alias-border-l2)}
|
|
604
618
|
.codexSubscriptionAccountRow,.codexSubscriptionSectionHead{display:flex;align-items:center;justify-content:space-between;gap:12px}.codexSubscriptionStatus{display:flex;align-items:center;gap:8px;font-size:14px;line-height:22px;font-weight:500}
|
|
619
|
+
.codexSubscriptionAccounts{display:flex;flex-direction:column;border-top:1px solid var(--dsw-alias-border-l2)}.codexSubscriptionAccount{display:flex;align-items:center;justify-content:space-between;gap:10px;min-height:42px;border-bottom:1px solid var(--dsw-alias-border-l2);font-size:13px}.codexSubscriptionAccount:last-child{border-bottom:0}.codexSubscriptionAccount[data-active=true]>span{font-weight:600}.codexSubscriptionFlow label{display:flex;flex-direction:column;gap:6px;font-size:12px;color:var(--dsw-alias-label-secondary)}
|
|
605
620
|
.codexSubscriptionDot{width:8px;height:8px;border-radius:50%;background:var(--dsw-alias-label-dimmed)}.codexSubscriptionDot[data-state=connected]{background:var(--dsw-alias-state-success-primary)}.codexSubscriptionDot[data-state=disconnected]{background:var(--dsw-alias-state-error-primary)}
|
|
606
621
|
.codexSubscriptionActions{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.codexSubscriptionFlow{display:flex;flex-direction:column;gap:10px;padding:12px 14px;border-radius:10px;background:var(--dsw-alias-bg-module-platform)}
|
|
607
622
|
.codexSubscriptionFlow p{font-size:13px;line-height:20px;color:var(--dsw-alias-label-secondary)}.codexSubscriptionCode{width:max-content;max-width:100%;font:600 16px/22px ui-monospace,SFMono-Regular,Consolas,monospace;letter-spacing:.08em;overflow-wrap:anywhere}
|
|
@@ -1873,6 +1888,9 @@ window.__ModuleLoader__.load({
|
|
|
1873
1888
|
function AccountCard({ rpc, t, account, setAccount, onSignedOut }) {
|
|
1874
1889
|
const [flow, setFlow] = (0, react.useState)();
|
|
1875
1890
|
const [manualCode, setManualCode] = (0, react.useState)("");
|
|
1891
|
+
const [adding, setAdding] = (0, react.useState)(false);
|
|
1892
|
+
const [accountLabel, setAccountLabel] = (0, react.useState)("");
|
|
1893
|
+
const [removeId, setRemoveId] = (0, react.useState)();
|
|
1876
1894
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
1877
1895
|
const [error, setError] = (0, react.useState)();
|
|
1878
1896
|
const call = (endpoint, payload = {}) => rpc.call(CHANNEL, endpoint, payload).then(unwrap);
|
|
@@ -1883,35 +1901,52 @@ window.__ModuleLoader__.load({
|
|
|
1883
1901
|
"cancelled"
|
|
1884
1902
|
].includes(flow.phase)) return void 0;
|
|
1885
1903
|
const timer = window.setInterval(() => {
|
|
1886
|
-
|
|
1904
|
+
(adding ? call("login/status", { id: flow.id }).then(async (nextFlow) => ({
|
|
1905
|
+
flow: nextFlow,
|
|
1906
|
+
account: nextFlow.phase === "authenticated" ? await call("status") : void 0
|
|
1907
|
+
})) : readLoginProgress({
|
|
1887
1908
|
flow,
|
|
1888
1909
|
readFlow: () => call("login/status", { id: flow.id }),
|
|
1889
1910
|
readAccount: () => call("status")
|
|
1890
|
-
}).then((next) => {
|
|
1911
|
+
})).then((next) => {
|
|
1891
1912
|
setFlow(next.flow);
|
|
1892
1913
|
setError(void 0);
|
|
1893
1914
|
if (next.account !== void 0) {
|
|
1894
1915
|
setAccount(next.account);
|
|
1916
|
+
onSignedOut();
|
|
1917
|
+
setAdding(false);
|
|
1918
|
+
setAccountLabel("");
|
|
1919
|
+
setFlow(void 0);
|
|
1895
1920
|
notifyQuickQuota();
|
|
1896
1921
|
}
|
|
1897
1922
|
}).catch(() => setError(t("failed")));
|
|
1898
1923
|
}, 800);
|
|
1899
1924
|
return () => window.clearInterval(timer);
|
|
1900
|
-
}, [
|
|
1901
|
-
|
|
1925
|
+
}, [
|
|
1926
|
+
flow?.id,
|
|
1927
|
+
flow?.phase,
|
|
1928
|
+
adding
|
|
1929
|
+
]);
|
|
1930
|
+
const begin = (method, label) => {
|
|
1902
1931
|
setFlow(void 0);
|
|
1903
1932
|
setBusy(true);
|
|
1904
1933
|
setError(void 0);
|
|
1905
1934
|
call("login/start", {
|
|
1906
1935
|
method,
|
|
1907
|
-
openExternal: true
|
|
1936
|
+
openExternal: true,
|
|
1937
|
+
...label === void 0 ? {} : { label }
|
|
1908
1938
|
}).then(setFlow).catch(() => setError(t("failed"))).finally(() => setBusy(false));
|
|
1909
1939
|
};
|
|
1910
1940
|
const cancel = () => {
|
|
1911
1941
|
if (flow?.id === void 0) return;
|
|
1912
1942
|
setBusy(true);
|
|
1913
1943
|
call("login/cancel", { id: flow.id }).then((next) => {
|
|
1914
|
-
setFlow(next);
|
|
1944
|
+
setFlow(adding ? void 0 : next);
|
|
1945
|
+
if (adding) {
|
|
1946
|
+
setAdding(false);
|
|
1947
|
+
setAccountLabel("");
|
|
1948
|
+
}
|
|
1949
|
+
if (adding) return void 0;
|
|
1915
1950
|
return call("status").then((account) => {
|
|
1916
1951
|
if (account.authenticated === true) {
|
|
1917
1952
|
setAccount(account);
|
|
@@ -1948,8 +1983,38 @@ window.__ModuleLoader__.load({
|
|
|
1948
1983
|
notifyQuickQuota();
|
|
1949
1984
|
}).catch(() => setError(t("failed"))).finally(() => setBusy(false));
|
|
1950
1985
|
};
|
|
1986
|
+
const selectAccount = (id) => {
|
|
1987
|
+
setBusy(true);
|
|
1988
|
+
setError(void 0);
|
|
1989
|
+
call("account/select", { id }).then((next) => {
|
|
1990
|
+
setAccount(next);
|
|
1991
|
+
onSignedOut();
|
|
1992
|
+
notifyQuickQuota();
|
|
1993
|
+
}).catch(() => setError(t("failed"))).finally(() => setBusy(false));
|
|
1994
|
+
};
|
|
1995
|
+
const removeAccount = (id) => {
|
|
1996
|
+
if (removeId !== id) {
|
|
1997
|
+
setRemoveId(id);
|
|
1998
|
+
return;
|
|
1999
|
+
}
|
|
2000
|
+
setBusy(true);
|
|
2001
|
+
setError(void 0);
|
|
2002
|
+
call("account/remove", { id }).then((next) => {
|
|
2003
|
+
setAccount(next);
|
|
2004
|
+
setRemoveId(void 0);
|
|
2005
|
+
onSignedOut();
|
|
2006
|
+
notifyQuickQuota();
|
|
2007
|
+
}).catch(() => setError(t("failed"))).finally(() => setBusy(false));
|
|
2008
|
+
};
|
|
1951
2009
|
const signedIn = account?.authenticated === true;
|
|
1952
2010
|
const accountReady = account !== void 0;
|
|
2011
|
+
const accounts = account?.accounts ?? [];
|
|
2012
|
+
const activeAccount = accounts.find((candidate) => candidate.active);
|
|
2013
|
+
const loginVisible = flow !== void 0 && ![
|
|
2014
|
+
"authenticated",
|
|
2015
|
+
"failed",
|
|
2016
|
+
"cancelled"
|
|
2017
|
+
].includes(flow.phase);
|
|
1953
2018
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1954
2019
|
className: "codexSubscriptionCard",
|
|
1955
2020
|
children: [
|
|
@@ -1963,16 +2028,26 @@ window.__ModuleLoader__.load({
|
|
|
1963
2028
|
className: "codexSubscriptionDot",
|
|
1964
2029
|
"data-state": accountReady ? signedIn ? "connected" : "disconnected" : "loading",
|
|
1965
2030
|
"aria-hidden": "true"
|
|
1966
|
-
}), accountReady ? signedIn ? t("connected") : t("disconnected") : t("accountLoading")]
|
|
2031
|
+
}), accountReady ? signedIn ? activeAccount?.label ?? t("connected") : t("disconnected") : t("accountLoading")]
|
|
1967
2032
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1968
2033
|
className: "codexSubscriptionActions",
|
|
1969
|
-
children: signedIn ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
2034
|
+
children: signedIn ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1970
2035
|
type: "button",
|
|
1971
2036
|
variant: "outline",
|
|
1972
|
-
disabled: busy,
|
|
2037
|
+
disabled: busy || loginVisible,
|
|
2038
|
+
onClick: () => {
|
|
2039
|
+
setFlow(void 0);
|
|
2040
|
+
setAdding(true);
|
|
2041
|
+
setAccountLabel(`Account ${accounts.length + 1}`);
|
|
2042
|
+
},
|
|
2043
|
+
children: t("addAccount")
|
|
2044
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
2045
|
+
type: "button",
|
|
2046
|
+
variant: "outline",
|
|
2047
|
+
disabled: busy || loginVisible,
|
|
1973
2048
|
onClick: logout,
|
|
1974
|
-
children: t("
|
|
1975
|
-
}) : accountReady && (flow === void 0 || ["failed", "cancelled"].includes(flow.phase)) ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
2049
|
+
children: t("signOutAll")
|
|
2050
|
+
})] }) : accountReady && (flow === void 0 || ["failed", "cancelled"].includes(flow.phase)) ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1976
2051
|
type: "button",
|
|
1977
2052
|
variant: "primary",
|
|
1978
2053
|
disabled: busy,
|
|
@@ -1987,7 +2062,77 @@ window.__ModuleLoader__.load({
|
|
|
1987
2062
|
})] }) : null
|
|
1988
2063
|
})]
|
|
1989
2064
|
}),
|
|
1990
|
-
|
|
2065
|
+
signedIn && accounts.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2066
|
+
className: "codexSubscriptionAccounts",
|
|
2067
|
+
children: accounts.map((candidate) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2068
|
+
className: "codexSubscriptionAccount",
|
|
2069
|
+
"data-active": candidate.active,
|
|
2070
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: candidate.label }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2071
|
+
className: "codexSubscriptionActions",
|
|
2072
|
+
children: [
|
|
2073
|
+
candidate.active ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
2074
|
+
type: "button",
|
|
2075
|
+
variant: "outline",
|
|
2076
|
+
disabled: busy || loginVisible,
|
|
2077
|
+
onClick: () => selectAccount(candidate.id),
|
|
2078
|
+
children: t("switchAccount")
|
|
2079
|
+
}),
|
|
2080
|
+
accounts.length > 1 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
2081
|
+
type: "button",
|
|
2082
|
+
variant: "outline",
|
|
2083
|
+
disabled: busy || loginVisible,
|
|
2084
|
+
onClick: () => removeAccount(candidate.id),
|
|
2085
|
+
children: removeId === candidate.id ? t("removeConfirm") : t("removeAccount")
|
|
2086
|
+
}) : null,
|
|
2087
|
+
removeId === candidate.id ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
2088
|
+
type: "button",
|
|
2089
|
+
variant: "outline",
|
|
2090
|
+
disabled: busy,
|
|
2091
|
+
onClick: () => setRemoveId(void 0),
|
|
2092
|
+
children: t("removeCancel")
|
|
2093
|
+
}) : null
|
|
2094
|
+
]
|
|
2095
|
+
})]
|
|
2096
|
+
}, candidate.id))
|
|
2097
|
+
}) : null,
|
|
2098
|
+
signedIn && adding && flow === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2099
|
+
className: "codexSubscriptionFlow",
|
|
2100
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [t("accountLabel"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
|
|
2101
|
+
className: "codexSubscriptionInput",
|
|
2102
|
+
value: accountLabel,
|
|
2103
|
+
maxLength: 48,
|
|
2104
|
+
onChange: (event) => setAccountLabel(event.currentTarget.value)
|
|
2105
|
+
})] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2106
|
+
className: "codexSubscriptionActions",
|
|
2107
|
+
children: [
|
|
2108
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
2109
|
+
type: "button",
|
|
2110
|
+
variant: "primary",
|
|
2111
|
+
disabled: busy || accountLabel.trim() === "",
|
|
2112
|
+
onClick: () => begin("browser", accountLabel.trim()),
|
|
2113
|
+
children: t("browserLogin")
|
|
2114
|
+
}),
|
|
2115
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
2116
|
+
type: "button",
|
|
2117
|
+
variant: "outline",
|
|
2118
|
+
disabled: busy || accountLabel.trim() === "",
|
|
2119
|
+
onClick: () => begin("device_code", accountLabel.trim()),
|
|
2120
|
+
children: t("deviceLogin")
|
|
2121
|
+
}),
|
|
2122
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
2123
|
+
type: "button",
|
|
2124
|
+
variant: "outline",
|
|
2125
|
+
disabled: busy,
|
|
2126
|
+
onClick: () => {
|
|
2127
|
+
setAdding(false);
|
|
2128
|
+
setAccountLabel("");
|
|
2129
|
+
},
|
|
2130
|
+
children: t("cancel")
|
|
2131
|
+
})
|
|
2132
|
+
]
|
|
2133
|
+
})]
|
|
2134
|
+
}) : null,
|
|
2135
|
+
flow?.phase === "waiting_device" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1991
2136
|
className: "codexSubscriptionFlow",
|
|
1992
2137
|
children: [
|
|
1993
2138
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("deviceHint") }),
|
|
@@ -2004,7 +2149,7 @@ window.__ModuleLoader__.load({
|
|
|
2004
2149
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("waiting") })
|
|
2005
2150
|
]
|
|
2006
2151
|
}) : null,
|
|
2007
|
-
|
|
2152
|
+
flow?.phase === "waiting_input" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
|
|
2008
2153
|
className: "codexSubscriptionFlow",
|
|
2009
2154
|
onSubmit: submit,
|
|
2010
2155
|
children: [
|
|
@@ -2033,7 +2178,7 @@ window.__ModuleLoader__.load({
|
|
|
2033
2178
|
})
|
|
2034
2179
|
]
|
|
2035
2180
|
}) : null,
|
|
2036
|
-
|
|
2181
|
+
flow !== void 0 && ["starting", "waiting_browser"].includes(flow.phase) ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2037
2182
|
className: "codexSubscriptionFlow",
|
|
2038
2183
|
children: [
|
|
2039
2184
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("waiting") }),
|
package/lib/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as dshCredentials from "@deepseek-ai/dsh-credentials";
|
|
2
|
+
import { dshHomePath, resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
2
3
|
import { LlmError, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
3
4
|
import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
|
|
4
5
|
import z from "@deepseek-ai/schemastery";
|
|
6
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
5
7
|
import { execFile, spawn } from "node:child_process";
|
|
6
8
|
import { request } from "node:https";
|
|
7
9
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
@@ -10,13 +12,269 @@ import { promisify } from "node:util";
|
|
|
10
12
|
import { HttpsProxyAgent } from "https-proxy-agent";
|
|
11
13
|
import { openaiCodexProvider as createOpenAICodexProvider } from "@earendil-works/pi-ai/providers/openai-codex";
|
|
12
14
|
import { createModels } from "@earendil-works/pi-ai";
|
|
13
|
-
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
14
15
|
import { WebError } from "@deepseek-ai/dsh-web";
|
|
15
16
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
16
17
|
import { constants } from "node:fs";
|
|
17
|
-
import { lstat, mkdir, open, readFile, rename, rm } from "node:fs/promises";
|
|
18
|
+
import { lstat, mkdir, open, readFile, rename, rm, stat } from "node:fs/promises";
|
|
18
19
|
import { dirname, join, resolve } from "node:path";
|
|
19
|
-
|
|
20
|
+
//#region src/account-vault.js
|
|
21
|
+
const VERSION = 1;
|
|
22
|
+
const DEFAULT_LABEL = "Account 1";
|
|
23
|
+
const clone$1 = (value) => value === void 0 ? void 0 : structuredClone(value);
|
|
24
|
+
function assertOAuthCredential$1(value) {
|
|
25
|
+
if (value === null || typeof value !== "object" || value.type !== "oauth" || typeof value.access !== "string" || value.access.length === 0 || typeof value.refresh !== "string" || value.refresh.length === 0 || typeof value.expires !== "number" || !Number.isFinite(value.expires)) throw new Error("Codex account vault received a malformed OAuth credential");
|
|
26
|
+
return clone$1(value);
|
|
27
|
+
}
|
|
28
|
+
function parseOAuthCredential$1(value) {
|
|
29
|
+
try {
|
|
30
|
+
return assertOAuthCredential$1(JSON.parse(value));
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (error?.message === "Codex account vault received a malformed OAuth credential") throw error;
|
|
33
|
+
throw new Error("Codex account vault contains malformed OAuth JSON", { cause: error });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function normalizeLabel(value) {
|
|
37
|
+
if (typeof value !== "string") throw new Error("Codex account label must be text");
|
|
38
|
+
const label = value.trim().replace(/\s+/gu, " ");
|
|
39
|
+
if (label.length === 0 || label.length > 48) throw new Error("Codex account label must contain 1 to 48 characters");
|
|
40
|
+
return label;
|
|
41
|
+
}
|
|
42
|
+
function assertVaultRecord(record) {
|
|
43
|
+
if (record?.kind !== "grant" || record.payload?.version !== VERSION || typeof record.payload.activeId !== "string" || !Array.isArray(record.payload.accounts) || record.payload.accounts.length === 0) throw new Error("Codex account vault contains a malformed grant record");
|
|
44
|
+
const ids = /* @__PURE__ */ new Set();
|
|
45
|
+
const accounts = record.payload.accounts.map((account) => {
|
|
46
|
+
if (account === null || typeof account !== "object" || typeof account.id !== "string" || account.id.length === 0 || ids.has(account.id)) throw new Error("Codex account vault contains a malformed account id");
|
|
47
|
+
ids.add(account.id);
|
|
48
|
+
return {
|
|
49
|
+
id: account.id,
|
|
50
|
+
label: normalizeLabel(account.label),
|
|
51
|
+
credential: assertOAuthCredential$1(account.credential)
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
if (!ids.has(record.payload.activeId)) throw new Error("Codex account vault active account is missing");
|
|
55
|
+
const legacyAccountId = record.payload.legacyAccountId;
|
|
56
|
+
if (legacyAccountId !== void 0 && !ids.has(legacyAccountId)) throw new Error("Codex account vault legacy account is missing");
|
|
57
|
+
return {
|
|
58
|
+
version: VERSION,
|
|
59
|
+
activeId: record.payload.activeId,
|
|
60
|
+
legacyAccountId,
|
|
61
|
+
accounts
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const grant = (payload) => ({
|
|
65
|
+
kind: "grant",
|
|
66
|
+
payload
|
|
67
|
+
});
|
|
68
|
+
var PendingOAuthCredentialStore = class {
|
|
69
|
+
#credential;
|
|
70
|
+
async read(providerId) {
|
|
71
|
+
if (providerId !== "openai-codex") throw new Error("Pending Codex login received an unknown provider");
|
|
72
|
+
return clone$1(this.#credential);
|
|
73
|
+
}
|
|
74
|
+
async list() {
|
|
75
|
+
return this.#credential === void 0 ? [] : [{
|
|
76
|
+
providerId: "openai-codex",
|
|
77
|
+
type: "oauth"
|
|
78
|
+
}];
|
|
79
|
+
}
|
|
80
|
+
async modify(providerId, update) {
|
|
81
|
+
if (providerId !== "openai-codex") throw new Error("Pending Codex login received an unknown provider");
|
|
82
|
+
const next = await update(clone$1(this.#credential));
|
|
83
|
+
if (next !== void 0) this.#credential = assertOAuthCredential$1(next);
|
|
84
|
+
return clone$1(this.#credential);
|
|
85
|
+
}
|
|
86
|
+
async delete(providerId) {
|
|
87
|
+
if (providerId !== "openai-codex") throw new Error("Pending Codex login received an unknown provider");
|
|
88
|
+
this.#credential = void 0;
|
|
89
|
+
}
|
|
90
|
+
credential() {
|
|
91
|
+
return clone$1(this.#credential);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Multi-account owner state stored in DSH's atomic plugin credential record.
|
|
96
|
+
* The old single-account reference remains as a rollback source and is kept in
|
|
97
|
+
* sync whenever that imported account rotates its refresh token.
|
|
98
|
+
*/
|
|
99
|
+
var DshOAuthAccountVault = class {
|
|
100
|
+
#tail = Promise.resolve();
|
|
101
|
+
constructor(credentials, options) {
|
|
102
|
+
if (credentials === void 0 || credentials === null || typeof credentials.readRecord !== "function" || typeof credentials.modifyRecord !== "function") throw new Error("Codex multi-account requires DSH credential records");
|
|
103
|
+
this.credentials = credentials;
|
|
104
|
+
this.key = options.key;
|
|
105
|
+
this.legacyRef = options.legacyRef;
|
|
106
|
+
this.legacyRefs = Object.freeze([...options.legacyRefs ?? []]);
|
|
107
|
+
this.createId = options.createId ?? randomUUID;
|
|
108
|
+
this.onLegacySyncFailure = options.onLegacySyncFailure ?? (() => {});
|
|
109
|
+
}
|
|
110
|
+
#enqueue(operation) {
|
|
111
|
+
const current = this.#tail.catch(() => void 0).then(operation);
|
|
112
|
+
this.#tail = current.catch(() => void 0);
|
|
113
|
+
return current;
|
|
114
|
+
}
|
|
115
|
+
async #legacyCredential() {
|
|
116
|
+
for (const ref of [this.legacyRef, ...this.legacyRefs]) {
|
|
117
|
+
const hit = await this.credentials.resolve(ref);
|
|
118
|
+
if (hit?.value === void 0 || hit.value === "") continue;
|
|
119
|
+
return {
|
|
120
|
+
ref,
|
|
121
|
+
credential: parseOAuthCredential$1(hit.value)
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async #ensurePayload() {
|
|
126
|
+
const existing = await this.credentials.readRecord(this.key);
|
|
127
|
+
if (existing !== void 0) return assertVaultRecord(existing);
|
|
128
|
+
const legacy = await this.#legacyCredential();
|
|
129
|
+
if (legacy === void 0) return void 0;
|
|
130
|
+
const id = this.createId();
|
|
131
|
+
return assertVaultRecord(await this.credentials.modifyRecord(this.key, (current) => {
|
|
132
|
+
if (current !== void 0) return Promise.resolve(current);
|
|
133
|
+
return Promise.resolve(grant({
|
|
134
|
+
version: VERSION,
|
|
135
|
+
activeId: id,
|
|
136
|
+
legacyAccountId: id,
|
|
137
|
+
accounts: [{
|
|
138
|
+
id,
|
|
139
|
+
label: DEFAULT_LABEL,
|
|
140
|
+
credential: legacy.credential
|
|
141
|
+
}]
|
|
142
|
+
}));
|
|
143
|
+
}));
|
|
144
|
+
}
|
|
145
|
+
async #modifyPayload(update) {
|
|
146
|
+
await this.#ensurePayload();
|
|
147
|
+
let previousLegacy;
|
|
148
|
+
const payload = assertVaultRecord(await this.credentials.modifyRecord(this.key, async (current) => {
|
|
149
|
+
if (current === void 0) throw new Error("Codex account vault is not signed in");
|
|
150
|
+
const payload = assertVaultRecord(current);
|
|
151
|
+
previousLegacy = payload.accounts.find((account) => account.id === payload.legacyAccountId)?.credential;
|
|
152
|
+
const next = await update(clone$1(payload));
|
|
153
|
+
return grant(next);
|
|
154
|
+
}));
|
|
155
|
+
const legacy = payload.accounts.find((account) => account.id === payload.legacyAccountId)?.credential;
|
|
156
|
+
try {
|
|
157
|
+
if (legacy === void 0) {
|
|
158
|
+
if (previousLegacy !== void 0) await this.credentials.unset(this.legacyRef);
|
|
159
|
+
} else if (JSON.stringify(legacy) !== JSON.stringify(previousLegacy)) await this.credentials.set(this.legacyRef, JSON.stringify(legacy));
|
|
160
|
+
} catch {
|
|
161
|
+
this.onLegacySyncFailure();
|
|
162
|
+
}
|
|
163
|
+
return payload;
|
|
164
|
+
}
|
|
165
|
+
list() {
|
|
166
|
+
return this.#enqueue(async () => {
|
|
167
|
+
const payload = await this.#ensurePayload();
|
|
168
|
+
if (payload === void 0) return [];
|
|
169
|
+
return payload.accounts.map((account) => ({
|
|
170
|
+
id: account.id,
|
|
171
|
+
label: account.label,
|
|
172
|
+
active: account.id === payload.activeId,
|
|
173
|
+
expiresAt: account.credential.expires
|
|
174
|
+
}));
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
readActive() {
|
|
178
|
+
return this.#enqueue(async () => {
|
|
179
|
+
const payload = await this.#ensurePayload();
|
|
180
|
+
return clone$1(payload?.accounts.find((account) => account.id === payload.activeId)?.credential);
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
activeId() {
|
|
184
|
+
return this.#enqueue(async () => (await this.#ensurePayload())?.activeId);
|
|
185
|
+
}
|
|
186
|
+
add(label, credential) {
|
|
187
|
+
return this.#enqueue(async () => {
|
|
188
|
+
const normalizedLabel = normalizeLabel(label);
|
|
189
|
+
const validated = assertOAuthCredential$1(credential);
|
|
190
|
+
await this.#ensurePayload();
|
|
191
|
+
const id = this.createId();
|
|
192
|
+
const account = (await this.#modifyPayload((current) => ({
|
|
193
|
+
...current,
|
|
194
|
+
activeId: id,
|
|
195
|
+
accounts: [...current.accounts, {
|
|
196
|
+
id,
|
|
197
|
+
label: normalizedLabel,
|
|
198
|
+
credential: validated
|
|
199
|
+
}]
|
|
200
|
+
}))).accounts.find((candidate) => candidate.id === id);
|
|
201
|
+
return {
|
|
202
|
+
id,
|
|
203
|
+
label: account.label,
|
|
204
|
+
active: true,
|
|
205
|
+
expiresAt: account.credential.expires
|
|
206
|
+
};
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
select(id) {
|
|
210
|
+
return this.#enqueue(async () => {
|
|
211
|
+
await this.#modifyPayload((current) => {
|
|
212
|
+
if (!current.accounts.some((account) => account.id === id)) throw new Error("Unknown Codex account");
|
|
213
|
+
return {
|
|
214
|
+
...current,
|
|
215
|
+
activeId: id
|
|
216
|
+
};
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
modifyActive(update) {
|
|
221
|
+
return this.#enqueue(async () => {
|
|
222
|
+
if (await this.#ensurePayload() === void 0) {
|
|
223
|
+
const initial = await update(void 0);
|
|
224
|
+
if (initial === void 0) return void 0;
|
|
225
|
+
const credential = assertOAuthCredential$1(initial);
|
|
226
|
+
await this.credentials.set(this.legacyRef, JSON.stringify(credential));
|
|
227
|
+
await this.#ensurePayload();
|
|
228
|
+
return clone$1(credential);
|
|
229
|
+
}
|
|
230
|
+
let result;
|
|
231
|
+
await this.#modifyPayload(async (current) => {
|
|
232
|
+
const index = current.accounts.findIndex((account) => account.id === current.activeId);
|
|
233
|
+
const previous = clone$1(current.accounts[index].credential);
|
|
234
|
+
const next = await update(previous);
|
|
235
|
+
if (next === void 0) {
|
|
236
|
+
result = previous;
|
|
237
|
+
return current;
|
|
238
|
+
}
|
|
239
|
+
const credential = assertOAuthCredential$1(next);
|
|
240
|
+
const accounts = [...current.accounts];
|
|
241
|
+
accounts[index] = {
|
|
242
|
+
...accounts[index],
|
|
243
|
+
credential
|
|
244
|
+
};
|
|
245
|
+
result = clone$1(credential);
|
|
246
|
+
return {
|
|
247
|
+
...current,
|
|
248
|
+
accounts
|
|
249
|
+
};
|
|
250
|
+
});
|
|
251
|
+
return result;
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
deleteAll() {
|
|
255
|
+
return this.#enqueue(async () => {
|
|
256
|
+
await this.credentials.deleteRecord(this.key);
|
|
257
|
+
await this.credentials.unset(this.legacyRef);
|
|
258
|
+
for (const ref of this.legacyRefs) await this.credentials.unset(ref);
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
remove(id) {
|
|
262
|
+
return this.#enqueue(async () => {
|
|
263
|
+
await this.#modifyPayload((current) => {
|
|
264
|
+
if (!current.accounts.some((account) => account.id === id)) throw new Error("Unknown Codex account");
|
|
265
|
+
if (current.accounts.length === 1) throw new Error("Cannot remove the last account; sign out instead");
|
|
266
|
+
const accounts = current.accounts.filter((account) => account.id !== id);
|
|
267
|
+
return {
|
|
268
|
+
...current,
|
|
269
|
+
activeId: current.activeId === id ? accounts[0].id : current.activeId,
|
|
270
|
+
legacyAccountId: current.legacyAccountId === id ? void 0 : current.legacyAccountId,
|
|
271
|
+
accounts
|
|
272
|
+
};
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
//#endregion
|
|
20
278
|
//#region src/credential-store.js
|
|
21
279
|
const PROVIDER$1 = "openai-codex";
|
|
22
280
|
const abortIfNeeded = (options) => options?.signal?.throwIfAborted();
|
|
@@ -52,6 +310,7 @@ var DshOAuthCredentialStore = class {
|
|
|
52
310
|
this.ref = ref;
|
|
53
311
|
this.legacyRefs = Object.freeze([...legacyRefs]);
|
|
54
312
|
this.expirySkewMs = expirySkewMs;
|
|
313
|
+
this.vault = options.vault;
|
|
55
314
|
}
|
|
56
315
|
#enqueue(providerId, operation, options) {
|
|
57
316
|
assertProvider(providerId);
|
|
@@ -69,6 +328,14 @@ var DshOAuthCredentialStore = class {
|
|
|
69
328
|
async #read(providerId, options) {
|
|
70
329
|
assertProvider(providerId);
|
|
71
330
|
abortIfNeeded(options);
|
|
331
|
+
if (this.vault !== void 0) {
|
|
332
|
+
const current = await this.vault.readActive();
|
|
333
|
+
if (current === void 0) return void 0;
|
|
334
|
+
return this.expirySkewMs === 0 ? current : {
|
|
335
|
+
...current,
|
|
336
|
+
expires: current.expires - this.expirySkewMs
|
|
337
|
+
};
|
|
338
|
+
}
|
|
72
339
|
let hit = await this.credentials.resolve(this.ref);
|
|
73
340
|
if (hit?.value === void 0 || hit.value === "") for (const legacyRef of this.legacyRefs) {
|
|
74
341
|
const legacy = await this.credentials.resolve(legacyRef);
|
|
@@ -99,6 +366,18 @@ var DshOAuthCredentialStore = class {
|
|
|
99
366
|
}
|
|
100
367
|
modify(providerId, update, options) {
|
|
101
368
|
return this.#enqueue(providerId, async () => {
|
|
369
|
+
if (this.vault !== void 0) {
|
|
370
|
+
const next = await this.vault.modifyActive(async (current) => {
|
|
371
|
+
const visible = current === void 0 || this.expirySkewMs === 0 ? current : {
|
|
372
|
+
...current,
|
|
373
|
+
expires: current.expires - this.expirySkewMs
|
|
374
|
+
};
|
|
375
|
+
const updated = await update(clone(visible));
|
|
376
|
+
return updated === void 0 ? void 0 : assertOAuthCredential(updated);
|
|
377
|
+
});
|
|
378
|
+
abortIfNeeded(options);
|
|
379
|
+
return clone(next);
|
|
380
|
+
}
|
|
102
381
|
const current = await this.#read(providerId, options);
|
|
103
382
|
const next = await update(clone(current));
|
|
104
383
|
abortIfNeeded(options);
|
|
@@ -112,6 +391,11 @@ var DshOAuthCredentialStore = class {
|
|
|
112
391
|
}
|
|
113
392
|
delete(providerId, options) {
|
|
114
393
|
return this.#enqueue(providerId, async () => {
|
|
394
|
+
if (this.vault !== void 0) {
|
|
395
|
+
await this.vault.deleteAll();
|
|
396
|
+
abortIfNeeded(options);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
115
399
|
await this.credentials.unset(this.ref);
|
|
116
400
|
for (const legacyRef of this.legacyRefs) await this.credentials.unset(legacyRef);
|
|
117
401
|
abortIfNeeded(options);
|
|
@@ -121,23 +405,49 @@ var DshOAuthCredentialStore = class {
|
|
|
121
405
|
/** Return only account state that is safe to expose to the browser client. */
|
|
122
406
|
function createCodexAuthService(models, store, options = {}) {
|
|
123
407
|
const runLogin = options.runLogin ?? ((run) => run());
|
|
408
|
+
const accountVault = options.accountVault;
|
|
409
|
+
const createLoginModels = options.createLoginModels;
|
|
410
|
+
const createPendingStore = options.createPendingStore ?? (() => new PendingOAuthCredentialStore());
|
|
124
411
|
return Object.freeze({
|
|
125
412
|
async status(options) {
|
|
126
413
|
const current = await store.read(PROVIDER$1, options);
|
|
414
|
+
const accounts = await accountVault?.list();
|
|
127
415
|
if (current === void 0) return {
|
|
128
416
|
authenticated: false,
|
|
129
|
-
provider: PROVIDER$1
|
|
417
|
+
provider: PROVIDER$1,
|
|
418
|
+
...accounts === void 0 ? {} : { accounts }
|
|
130
419
|
};
|
|
131
420
|
return {
|
|
132
421
|
authenticated: true,
|
|
133
422
|
provider: PROVIDER$1,
|
|
134
423
|
type: "oauth",
|
|
135
|
-
expiresAt: current.expires
|
|
424
|
+
expiresAt: current.expires,
|
|
425
|
+
...accounts === void 0 ? {} : { accounts }
|
|
136
426
|
};
|
|
137
427
|
},
|
|
138
|
-
login(interaction) {
|
|
428
|
+
login(interaction, input = {}) {
|
|
429
|
+
if (input.label !== void 0) {
|
|
430
|
+
if (accountVault === void 0 || createLoginModels === void 0) throw new Error("Codex multi-account is unavailable");
|
|
431
|
+
return runLogin(async () => {
|
|
432
|
+
const pending = createPendingStore();
|
|
433
|
+
await createLoginModels(pending).login(PROVIDER$1, "oauth", interaction);
|
|
434
|
+
const credential = pending.credential();
|
|
435
|
+
if (credential === void 0) throw new Error("Codex login did not return credentials");
|
|
436
|
+
await accountVault.add(input.label, credential);
|
|
437
|
+
});
|
|
438
|
+
}
|
|
139
439
|
return runLogin(() => models.login(PROVIDER$1, "oauth", interaction));
|
|
140
440
|
},
|
|
441
|
+
async select(id) {
|
|
442
|
+
if (accountVault === void 0) throw new Error("Codex multi-account is unavailable");
|
|
443
|
+
await accountVault.select(id);
|
|
444
|
+
return this.status();
|
|
445
|
+
},
|
|
446
|
+
async remove(id) {
|
|
447
|
+
if (accountVault === void 0) throw new Error("Codex multi-account is unavailable");
|
|
448
|
+
await accountVault.remove(id);
|
|
449
|
+
return this.status();
|
|
450
|
+
},
|
|
141
451
|
logout(options) {
|
|
142
452
|
return models.logout(PROVIDER$1, options);
|
|
143
453
|
}
|
|
@@ -263,8 +573,9 @@ var CodexLoginCoordinator = class {
|
|
|
263
573
|
...active.view.phase === "failed" ? { failure: classifyLoginFailure(active.hostError) } : {}
|
|
264
574
|
};
|
|
265
575
|
}
|
|
266
|
-
async start({ method }) {
|
|
576
|
+
async start({ method, label }) {
|
|
267
577
|
if (!LOGIN_METHODS.has(method)) throw new Error(`unsupported Codex login method: ${String(method)}`);
|
|
578
|
+
if (label !== void 0 && (typeof label !== "string" || label.trim().length === 0 || label.trim().length > 48)) throw new Error("unsupported Codex account label");
|
|
268
579
|
const active = this.#activeId === void 0 ? void 0 : this.#sessions.get(this.#activeId);
|
|
269
580
|
if (active !== void 0 && !TERMINAL_PHASES.has(active.view.phase)) {
|
|
270
581
|
active.view = {
|
|
@@ -349,7 +660,7 @@ var CodexLoginCoordinator = class {
|
|
|
349
660
|
publishReady();
|
|
350
661
|
}
|
|
351
662
|
};
|
|
352
|
-
session.run = Promise.resolve().then(() => this.auth.login(interaction)).then(async () => {
|
|
663
|
+
session.run = Promise.resolve().then(() => this.auth.login(interaction, label === void 0 ? {} : { label: label.trim() })).then(async () => {
|
|
353
664
|
if (controller.signal.aborted) return;
|
|
354
665
|
const status = await this.auth.status();
|
|
355
666
|
session.view = {
|
|
@@ -372,6 +683,7 @@ var CodexLoginCoordinator = class {
|
|
|
372
683
|
return;
|
|
373
684
|
}
|
|
374
685
|
try {
|
|
686
|
+
if (label !== void 0) throw error;
|
|
375
687
|
const status = await this.auth.status();
|
|
376
688
|
if (status.authenticated === true) {
|
|
377
689
|
session.view = {
|
|
@@ -440,6 +752,12 @@ var CodexLoginCoordinator = class {
|
|
|
440
752
|
await this.auth.logout(options);
|
|
441
753
|
return this.accountStatus(options);
|
|
442
754
|
}
|
|
755
|
+
async selectAccount(id) {
|
|
756
|
+
return publicClone(await this.auth.select(id));
|
|
757
|
+
}
|
|
758
|
+
async removeAccount(id) {
|
|
759
|
+
return publicClone(await this.auth.remove(id));
|
|
760
|
+
}
|
|
443
761
|
};
|
|
444
762
|
/** Map the loopback-only DSH Connection channel onto the coordinator. */
|
|
445
763
|
function createCodexRpcHandler(coordinator, options = {}) {
|
|
@@ -450,7 +768,10 @@ function createCodexRpcHandler(coordinator, options = {}) {
|
|
|
450
768
|
const input = asObject(payload);
|
|
451
769
|
if (endpoint === "status") return ok(await coordinator.accountStatus({ signal }));
|
|
452
770
|
if (endpoint === "login/start") {
|
|
453
|
-
const started = await coordinator.start({
|
|
771
|
+
const started = await coordinator.start({
|
|
772
|
+
method: input.method,
|
|
773
|
+
label: input.label
|
|
774
|
+
});
|
|
454
775
|
if (input.openExternal !== true) return ok(started);
|
|
455
776
|
const url = started.authUrl ?? started.deviceCode?.verificationUri;
|
|
456
777
|
if (typeof url !== "string" || openExternal === void 0) return ok({
|
|
@@ -477,6 +798,8 @@ function createCodexRpcHandler(coordinator, options = {}) {
|
|
|
477
798
|
}));
|
|
478
799
|
if (endpoint === "login/cancel") return ok(await coordinator.cancel(input.id));
|
|
479
800
|
if (endpoint === "logout") return ok(await coordinator.logout({ signal }));
|
|
801
|
+
if (endpoint === "account/select") return ok(await coordinator.selectAccount(input.id));
|
|
802
|
+
if (endpoint === "account/remove") return ok(await coordinator.removeAccount(input.id));
|
|
480
803
|
return badRequest(`unknown Codex auth endpoint: ${endpoint}`);
|
|
481
804
|
} catch (error) {
|
|
482
805
|
if (signal.aborted) throw error;
|
|
@@ -942,7 +1265,7 @@ function openaiCodexSubscriptionProvider({ resolveSpeedMode = () => void 0, reso
|
|
|
942
1265
|
}
|
|
943
1266
|
//#endregion
|
|
944
1267
|
//#region src/version.js
|
|
945
|
-
const PACKAGE_VERSION = "1.
|
|
1268
|
+
const PACKAGE_VERSION = "1.12.1-beta.0";
|
|
946
1269
|
const USER_AGENT = `dsh-codex-subscription/${PACKAGE_VERSION}`;
|
|
947
1270
|
//#endregion
|
|
948
1271
|
//#region src/model-catalog.js
|
|
@@ -2060,18 +2383,33 @@ function createCodexUsageReader(options) {
|
|
|
2060
2383
|
//#region src/quota-forecast.js
|
|
2061
2384
|
const HOUR_MS = 3600 * 1e3;
|
|
2062
2385
|
const HISTORY_MS = 24 * HOUR_MS;
|
|
2063
|
-
const
|
|
2064
|
-
const MIN_CONSUMED_PERCENT = 1;
|
|
2386
|
+
const MIN_SAMPLES = 3;
|
|
2065
2387
|
const PLATEAU_SAMPLE_MS = 900 * 1e3;
|
|
2066
2388
|
const finite = (value) => Number.isFinite(Number(value));
|
|
2067
2389
|
const clampPercent = (value) => Math.max(0, Math.min(100, Number(value)));
|
|
2068
|
-
const
|
|
2069
|
-
|
|
2390
|
+
const cleanSegment = (value) => String(value ?? "default").slice(0, 96);
|
|
2391
|
+
const keyFor = (window, context = {}) => JSON.stringify([
|
|
2392
|
+
cleanSegment(context.scope),
|
|
2393
|
+
cleanSegment(context.limitId ?? "codex"),
|
|
2394
|
+
Number(window.windowSeconds) || "limit"
|
|
2395
|
+
]);
|
|
2396
|
+
const median = (values) => {
|
|
2397
|
+
const ordered = [...values].sort((a, b) => a - b);
|
|
2398
|
+
const middle = Math.floor(ordered.length / 2);
|
|
2399
|
+
return ordered.length % 2 === 0 ? (ordered[middle - 1] + ordered[middle]) / 2 : ordered[middle];
|
|
2400
|
+
};
|
|
2401
|
+
function requiredSpanMs(consumedPercent) {
|
|
2402
|
+
if (consumedPercent >= 2) return 300 * 1e3;
|
|
2403
|
+
if (consumedPercent >= 1) return 600 * 1e3;
|
|
2404
|
+
if (consumedPercent >= .5) return 1200 * 1e3;
|
|
2405
|
+
return 1800 * 1e3;
|
|
2406
|
+
}
|
|
2407
|
+
function observeQuotaForecast(state, windows, now = Date.now(), context = {}) {
|
|
2070
2408
|
const next = { windows: { ...state?.windows ?? {} } };
|
|
2071
2409
|
let changed = false;
|
|
2072
2410
|
for (const window of windows ?? []) {
|
|
2073
2411
|
if (!finite(window?.remainingPercent)) continue;
|
|
2074
|
-
const key = keyFor(window);
|
|
2412
|
+
const key = keyFor(window, context);
|
|
2075
2413
|
const resetsAt = finite(window.resetsAt) ? Number(window.resetsAt) : null;
|
|
2076
2414
|
const remainingPercent = Math.round(clampPercent(window.remainingPercent) * 1e4) / 1e4;
|
|
2077
2415
|
const previous = next.windows[key];
|
|
@@ -2101,14 +2439,14 @@ function observeQuotaForecast(state, windows, now = Date.now()) {
|
|
|
2101
2439
|
changed
|
|
2102
2440
|
};
|
|
2103
2441
|
}
|
|
2104
|
-
function estimateQuotaForecast(state, window, now = Date.now()) {
|
|
2442
|
+
function estimateQuotaForecast(state, window, now = Date.now(), context = {}) {
|
|
2105
2443
|
if (!finite(window?.remainingPercent)) return { status: "calibrating" };
|
|
2106
|
-
const record = state?.windows?.[keyFor(window)];
|
|
2444
|
+
const record = state?.windows?.[keyFor(window, context)];
|
|
2107
2445
|
if (record === void 0) return { status: "calibrating" };
|
|
2108
2446
|
const resetsAt = finite(window.resetsAt) ? Number(window.resetsAt) : null;
|
|
2109
2447
|
if (record.resetsAt === null !== (resetsAt === null) || resetsAt !== null && Math.abs(record.resetsAt - resetsAt) > 300) return { status: "calibrating" };
|
|
2110
2448
|
const samples = record.samples.filter((sample) => sample.at >= now - HISTORY_MS && sample.at <= now + 6e4);
|
|
2111
|
-
if (samples.length <
|
|
2449
|
+
if (samples.length < MIN_SAMPLES) return {
|
|
2112
2450
|
status: "calibrating",
|
|
2113
2451
|
sampleCount: samples.length
|
|
2114
2452
|
};
|
|
@@ -2116,77 +2454,190 @@ function estimateQuotaForecast(state, window, now = Date.now()) {
|
|
|
2116
2454
|
const last = samples.at(-1);
|
|
2117
2455
|
const spanMs = last.at - first.at;
|
|
2118
2456
|
const consumedPercent = Math.max(0, first.remainingPercent - last.remainingPercent);
|
|
2119
|
-
if (spanMs <
|
|
2457
|
+
if (spanMs < requiredSpanMs(consumedPercent)) return {
|
|
2120
2458
|
status: "calibrating",
|
|
2121
2459
|
sampleCount: samples.length,
|
|
2122
2460
|
observedSpanMs: spanMs,
|
|
2123
2461
|
consumedPercent
|
|
2124
2462
|
};
|
|
2125
|
-
const
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
}
|
|
2131
|
-
const
|
|
2132
|
-
const
|
|
2133
|
-
const meanY = weighted.reduce((sum, point) => sum + point.y * point.weight, 0) / totalWeight;
|
|
2134
|
-
const numerator = weighted.reduce((sum, point) => sum + point.weight * (point.x - meanX) * (point.y - meanY), 0);
|
|
2135
|
-
const denominator = weighted.reduce((sum, point) => sum + point.weight * (point.x - meanX) ** 2, 0);
|
|
2136
|
-
const pacePerHour = denominator > 0 ? numerator / denominator : 0;
|
|
2463
|
+
const slopes = [];
|
|
2464
|
+
for (let left = 0; left < samples.length - 1; left += 1) for (let right = left + 1; right < samples.length; right += 1) {
|
|
2465
|
+
const hours = (samples[right].at - samples[left].at) / HOUR_MS;
|
|
2466
|
+
if (hours <= 0) continue;
|
|
2467
|
+
slopes.push((samples[left].remainingPercent - samples[right].remainingPercent) / hours);
|
|
2468
|
+
}
|
|
2469
|
+
const positive = slopes.filter((value) => Number.isFinite(value) && value >= 0);
|
|
2470
|
+
const pacePerHour = positive.length === 0 ? 0 : median(positive);
|
|
2137
2471
|
if (!Number.isFinite(pacePerHour) || pacePerHour < .02) return {
|
|
2138
2472
|
status: "idle",
|
|
2139
|
-
pacePerHour: 0
|
|
2473
|
+
pacePerHour: 0,
|
|
2474
|
+
sampleCount: samples.length,
|
|
2475
|
+
observedSpanMs: spanMs,
|
|
2476
|
+
consumedPercent
|
|
2140
2477
|
};
|
|
2141
|
-
const
|
|
2478
|
+
const deviations = positive.map((value) => Math.abs(value - pacePerHour));
|
|
2479
|
+
const uncertaintyPerHour = deviations.length === 0 ? 0 : median(deviations) * 1.4826;
|
|
2480
|
+
const lowerPacePerHour = Math.max(.02, pacePerHour - uncertaintyPerHour);
|
|
2481
|
+
const upperPacePerHour = pacePerHour + uncertaintyPerHour;
|
|
2482
|
+
const remaining = clampPercent(window.remainingPercent);
|
|
2483
|
+
const runwaySeconds = remaining / pacePerHour * 3600;
|
|
2484
|
+
const runwayMinSeconds = remaining / upperPacePerHour * 3600;
|
|
2485
|
+
const runwayMaxSeconds = remaining / lowerPacePerHour * 3600;
|
|
2142
2486
|
const resetSeconds = resetsAt === null ? null : Math.max(0, resetsAt - now / 1e3);
|
|
2143
2487
|
return {
|
|
2144
2488
|
status: "ready",
|
|
2145
2489
|
pacePerHour,
|
|
2490
|
+
uncertaintyPerHour,
|
|
2146
2491
|
runwaySeconds,
|
|
2147
|
-
|
|
2492
|
+
runwayMinSeconds,
|
|
2493
|
+
runwayMaxSeconds,
|
|
2494
|
+
survivesReset: resetSeconds !== null && runwayMinSeconds >= resetSeconds,
|
|
2148
2495
|
sampleCount: samples.length,
|
|
2149
2496
|
observedSpanMs: spanMs,
|
|
2150
2497
|
consumedPercent
|
|
2151
2498
|
};
|
|
2152
2499
|
}
|
|
2153
|
-
function forecastUsage(usage, state = { windows: {} }, now = Date.now()) {
|
|
2154
|
-
|
|
2500
|
+
function forecastUsage(usage, state = { windows: {} }, now = Date.now(), options = {}) {
|
|
2501
|
+
let nextState = state;
|
|
2502
|
+
let changed = false;
|
|
2503
|
+
const rateLimits = (usage?.rateLimits ?? []).map((limit) => {
|
|
2504
|
+
const context = {
|
|
2505
|
+
scope: options.scope,
|
|
2506
|
+
limitId: limit.id
|
|
2507
|
+
};
|
|
2508
|
+
const observed = observeQuotaForecast(nextState, limit.windows, now, context);
|
|
2509
|
+
nextState = observed.state;
|
|
2510
|
+
changed ||= observed.changed;
|
|
2511
|
+
return {
|
|
2512
|
+
...limit,
|
|
2513
|
+
windows: limit.windows.map((window) => ({
|
|
2514
|
+
...window,
|
|
2515
|
+
forecast: estimateQuotaForecast(nextState, window, now, context)
|
|
2516
|
+
}))
|
|
2517
|
+
};
|
|
2518
|
+
});
|
|
2155
2519
|
return {
|
|
2156
|
-
state:
|
|
2157
|
-
changed
|
|
2520
|
+
state: nextState,
|
|
2521
|
+
changed,
|
|
2158
2522
|
usage: {
|
|
2159
2523
|
...usage,
|
|
2160
|
-
rateLimits
|
|
2161
|
-
...limit,
|
|
2162
|
-
windows: limit.windows.map((window) => ({
|
|
2163
|
-
...window,
|
|
2164
|
-
forecast: estimateQuotaForecast(observed.state, window, now)
|
|
2165
|
-
}))
|
|
2166
|
-
})
|
|
2524
|
+
rateLimits
|
|
2167
2525
|
}
|
|
2168
2526
|
};
|
|
2169
2527
|
}
|
|
2170
|
-
function createQuotaForecastReader({ reader, enabled, now = Date.now }) {
|
|
2528
|
+
function createQuotaForecastReader({ reader, enabled, now = Date.now, scope = () => "default", stateStore }) {
|
|
2171
2529
|
let state = { windows: {} };
|
|
2530
|
+
let loaded = false;
|
|
2531
|
+
const load = async () => {
|
|
2532
|
+
if (loaded) return;
|
|
2533
|
+
loaded = true;
|
|
2534
|
+
const restored = await stateStore?.load?.();
|
|
2535
|
+
if (restored?.windows !== null && typeof restored?.windows === "object") state = restored;
|
|
2536
|
+
};
|
|
2172
2537
|
return Object.freeze({
|
|
2173
2538
|
async read(options) {
|
|
2174
2539
|
const usage = await reader.read(options);
|
|
2540
|
+
await load();
|
|
2175
2541
|
if (!enabled()) {
|
|
2176
2542
|
state = { windows: {} };
|
|
2543
|
+
await stateStore?.clear?.();
|
|
2177
2544
|
return usage;
|
|
2178
2545
|
}
|
|
2179
|
-
const forecast = forecastUsage(usage, state, now());
|
|
2546
|
+
const forecast = forecastUsage(usage, state, now(), { scope: await scope() });
|
|
2180
2547
|
state = forecast.state;
|
|
2548
|
+
if (forecast.changed) await stateStore?.save?.(state);
|
|
2181
2549
|
return forecast.usage;
|
|
2182
2550
|
},
|
|
2183
|
-
clear() {
|
|
2551
|
+
async clear() {
|
|
2184
2552
|
state = { windows: {} };
|
|
2553
|
+
loaded = true;
|
|
2554
|
+
reader.clear();
|
|
2555
|
+
await stateStore?.clear?.();
|
|
2556
|
+
},
|
|
2557
|
+
clearCache() {
|
|
2185
2558
|
reader.clear();
|
|
2559
|
+
},
|
|
2560
|
+
async clearScope(targetScope) {
|
|
2561
|
+
await load();
|
|
2562
|
+
const prefix = `[${JSON.stringify(cleanSegment(targetScope))},`;
|
|
2563
|
+
state = { windows: Object.fromEntries(Object.entries(state.windows).filter(([key]) => !key.startsWith(prefix))) };
|
|
2564
|
+
await stateStore?.save?.(state);
|
|
2186
2565
|
}
|
|
2187
2566
|
});
|
|
2188
2567
|
}
|
|
2189
2568
|
//#endregion
|
|
2569
|
+
//#region src/quota-forecast-store.js
|
|
2570
|
+
const DEFAULT_MAX_BYTES = 256 * 1024;
|
|
2571
|
+
const MAX_WINDOWS = 128;
|
|
2572
|
+
const MAX_SAMPLES = 192;
|
|
2573
|
+
function sanitize(value) {
|
|
2574
|
+
if (value === null || typeof value !== "object" || Array.isArray(value) || value.windows === null || typeof value.windows !== "object" || Array.isArray(value.windows)) return void 0;
|
|
2575
|
+
const entries = Object.entries(value.windows);
|
|
2576
|
+
if (entries.length > MAX_WINDOWS) return void 0;
|
|
2577
|
+
const windows = {};
|
|
2578
|
+
for (const [key, record] of entries) {
|
|
2579
|
+
if (key.length === 0 || key.length > 320 || record === null || typeof record !== "object" || !Array.isArray(record.samples) || record.samples.length > MAX_SAMPLES) return void 0;
|
|
2580
|
+
const resetsAt = record.resetsAt === null ? null : Number(record.resetsAt);
|
|
2581
|
+
if (resetsAt !== null && !Number.isFinite(resetsAt)) return void 0;
|
|
2582
|
+
const samples = [];
|
|
2583
|
+
for (const sample of record.samples) {
|
|
2584
|
+
const at = Number(sample?.at);
|
|
2585
|
+
const remainingPercent = Number(sample?.remainingPercent);
|
|
2586
|
+
if (!Number.isFinite(at) || !Number.isFinite(remainingPercent) || remainingPercent < 0 || remainingPercent > 100) return void 0;
|
|
2587
|
+
samples.push({
|
|
2588
|
+
at,
|
|
2589
|
+
remainingPercent
|
|
2590
|
+
});
|
|
2591
|
+
}
|
|
2592
|
+
windows[key] = {
|
|
2593
|
+
resetsAt,
|
|
2594
|
+
samples
|
|
2595
|
+
};
|
|
2596
|
+
}
|
|
2597
|
+
return { windows };
|
|
2598
|
+
}
|
|
2599
|
+
var QuotaForecastStateStore = class {
|
|
2600
|
+
constructor({ filename, maxBytes = DEFAULT_MAX_BYTES }) {
|
|
2601
|
+
this.filename = filename;
|
|
2602
|
+
this.maxBytes = maxBytes;
|
|
2603
|
+
}
|
|
2604
|
+
async load() {
|
|
2605
|
+
try {
|
|
2606
|
+
if ((await stat(this.filename)).size > this.maxBytes) return void 0;
|
|
2607
|
+
return sanitize(JSON.parse(await readFile(this.filename, "utf8")));
|
|
2608
|
+
} catch (error) {
|
|
2609
|
+
if (error?.code === "ENOENT" || error instanceof SyntaxError) return void 0;
|
|
2610
|
+
throw error;
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
async save(state) {
|
|
2614
|
+
const safe = sanitize(state);
|
|
2615
|
+
if (safe === void 0) throw new Error("Refusing to persist malformed quota forecast state");
|
|
2616
|
+
const data = `${JSON.stringify(safe)}\n`;
|
|
2617
|
+
if (Buffer.byteLength(data) > this.maxBytes) throw new Error("Quota forecast state is too large");
|
|
2618
|
+
await mkdir(dirname(this.filename), {
|
|
2619
|
+
recursive: true,
|
|
2620
|
+
mode: 448
|
|
2621
|
+
});
|
|
2622
|
+
const temporary = `${this.filename}.${process.pid}.${randomUUID()}.tmp`;
|
|
2623
|
+
let handle;
|
|
2624
|
+
try {
|
|
2625
|
+
handle = await open(temporary, "wx", 384);
|
|
2626
|
+
await handle.writeFile(data);
|
|
2627
|
+
await handle.sync();
|
|
2628
|
+
await handle.close();
|
|
2629
|
+
handle = void 0;
|
|
2630
|
+
await rename(temporary, this.filename);
|
|
2631
|
+
} finally {
|
|
2632
|
+
await handle?.close().catch(() => void 0);
|
|
2633
|
+
await rm(temporary, { force: true }).catch(() => void 0);
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
async clear() {
|
|
2637
|
+
await rm(this.filename, { force: true });
|
|
2638
|
+
}
|
|
2639
|
+
};
|
|
2640
|
+
//#endregion
|
|
2190
2641
|
//#region src/reset-credits.js
|
|
2191
2642
|
const CODEX_RESET_CREDITS_URL = "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits";
|
|
2192
2643
|
const CODEX_RESET_CONSUME_URL = `${CODEX_RESET_CREDITS_URL}/consume`;
|
|
@@ -2444,8 +2895,9 @@ const inject = [
|
|
|
2444
2895
|
];
|
|
2445
2896
|
const PROVIDER = "openai-codex";
|
|
2446
2897
|
const OAUTH_EXPIRY_SKEW_MS = 6e4;
|
|
2447
|
-
const CREDENTIAL_REF = credentialRef("OPENAI_CODEX_SUBSCRIPTION_OAUTH");
|
|
2448
|
-
const LEGACY_CREDENTIAL_REF = credentialRef("WSL043_OPENAI_CODEX_OAUTH");
|
|
2898
|
+
const CREDENTIAL_REF = dshCredentials.credentialRef("OPENAI_CODEX_SUBSCRIPTION_OAUTH");
|
|
2899
|
+
const LEGACY_CREDENTIAL_REF = dshCredentials.credentialRef("WSL043_OPENAI_CODEX_OAUTH");
|
|
2900
|
+
const ACCOUNT_VAULT_KEY = typeof dshCredentials.credentialKey === "function" ? dshCredentials.credentialKey("codex-subscription", "accounts") : void 0;
|
|
2449
2901
|
const CHANNEL = "/codex-subscription";
|
|
2450
2902
|
const WEB_ENTRY_ID = "web";
|
|
2451
2903
|
const DSH_SEARCH_PROVIDER_FALLBACK = "deepseek-official";
|
|
@@ -2592,10 +3044,16 @@ function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditSer
|
|
|
2592
3044
|
return publicError("internal", message);
|
|
2593
3045
|
}
|
|
2594
3046
|
const result = await authHandler(endpoint, payload, signal);
|
|
3047
|
+
if (endpoint === "account/remove" && result.ok === true && typeof payload?.id === "string") await usageReader.clearScope(payload.id);
|
|
2595
3048
|
if (endpoint === "logout" && result.ok === true) {
|
|
2596
|
-
usageReader.clear();
|
|
3049
|
+
await usageReader.clear();
|
|
3050
|
+
resetCreditService.clear();
|
|
3051
|
+
modelCatalog?.clear();
|
|
3052
|
+
} else if (result.ok === true && (endpoint === "account/select" || endpoint === "account/remove" || endpoint === "login/status" && result.value?.authenticated === true)) {
|
|
3053
|
+
usageReader.clearCache();
|
|
2597
3054
|
resetCreditService.clear();
|
|
2598
3055
|
modelCatalog?.clear();
|
|
3056
|
+
modelCatalog?.refresh({ signal: void 0 }).catch(() => {});
|
|
2599
3057
|
} else if (result.ok === true && (endpoint === "status" || result.value?.authenticated === true)) modelCatalog?.refresh({ signal: void 0 }).catch(() => {});
|
|
2600
3058
|
return result;
|
|
2601
3059
|
};
|
|
@@ -2656,7 +3114,15 @@ function apply(ctx) {
|
|
|
2656
3114
|
const searchProvider = createSearchProviderSwitcher(ctx.loader);
|
|
2657
3115
|
const network = createCodexNetworkTransport();
|
|
2658
3116
|
const originalImages = new OriginalImageStore();
|
|
2659
|
-
const
|
|
3117
|
+
const accountVault = ACCOUNT_VAULT_KEY !== void 0 && typeof ctx.credentials.readRecord === "function" && typeof ctx.credentials.modifyRecord === "function" && typeof ctx.credentials.deleteRecord === "function" ? new DshOAuthAccountVault(ctx.credentials, {
|
|
3118
|
+
key: ACCOUNT_VAULT_KEY,
|
|
3119
|
+
legacyRef: CREDENTIAL_REF,
|
|
3120
|
+
legacyRefs: [LEGACY_CREDENTIAL_REF]
|
|
3121
|
+
}) : void 0;
|
|
3122
|
+
const store = new DshOAuthCredentialStore(ctx.credentials, CREDENTIAL_REF, [LEGACY_CREDENTIAL_REF], {
|
|
3123
|
+
expirySkewMs: OAUTH_EXPIRY_SKEW_MS,
|
|
3124
|
+
vault: accountVault
|
|
3125
|
+
});
|
|
2660
3126
|
const baseProvider = createOpenAICodexProvider();
|
|
2661
3127
|
let resolveAuth = async () => void 0;
|
|
2662
3128
|
const modelCatalog = createOfficialModelCatalog({
|
|
@@ -2778,7 +3244,15 @@ function apply(ctx) {
|
|
|
2778
3244
|
select(settings.get());
|
|
2779
3245
|
return settings.watch(select);
|
|
2780
3246
|
}, "codex-subscription: search provider selection");
|
|
2781
|
-
const auth = createCodexAuthService(authModels, store, {
|
|
3247
|
+
const auth = createCodexAuthService(authModels, store, {
|
|
3248
|
+
runLogin: (operation) => network.run("login", operation),
|
|
3249
|
+
accountVault,
|
|
3250
|
+
createLoginModels: (credentials) => {
|
|
3251
|
+
const loginModels = createModels({ credentials });
|
|
3252
|
+
loginModels.setProvider(provider);
|
|
3253
|
+
return loginModels;
|
|
3254
|
+
}
|
|
3255
|
+
});
|
|
2782
3256
|
const coordinator = new CodexLoginCoordinator(auth);
|
|
2783
3257
|
const usageReader = createQuotaForecastReader({
|
|
2784
3258
|
reader: createCodexUsageReader({
|
|
@@ -2786,8 +3260,18 @@ function apply(ctx) {
|
|
|
2786
3260
|
readCredential: (options) => store.read(PROVIDER, options),
|
|
2787
3261
|
fetch: (input, init) => network.fetch("quota", input, init)
|
|
2788
3262
|
}),
|
|
2789
|
-
enabled: () => normalizeQuickQuotaMode(settings.get()[QUICK_QUOTA_MODE_FIELD], settings.get()[LEGACY_QUICK_QUOTA_FIELD]) === QUICK_QUOTA_MODE_FORECAST
|
|
3263
|
+
enabled: () => normalizeQuickQuotaMode(settings.get()[QUICK_QUOTA_MODE_FIELD], settings.get()[LEGACY_QUICK_QUOTA_FIELD]) === QUICK_QUOTA_MODE_FORECAST,
|
|
3264
|
+
scope: async () => await accountVault?.activeId() ?? "legacy",
|
|
3265
|
+
stateStore: new QuotaForecastStateStore({ filename: dshHomePath("state", "codex-subscription", "quota-forecast.json") })
|
|
2790
3266
|
});
|
|
3267
|
+
ctx.effect(() => {
|
|
3268
|
+
const warmForecast = (value) => {
|
|
3269
|
+
if (normalizeQuickQuotaMode(value["quickQuotaMode"], value["quickQuotaVisible"]) !== "forecast") return;
|
|
3270
|
+
usageReader.read().catch((error) => ctx.logger?.debug?.("could not warm Codex quota forecast: %s", error.message));
|
|
3271
|
+
};
|
|
3272
|
+
warmForecast(settings.get());
|
|
3273
|
+
return settings.watch(warmForecast);
|
|
3274
|
+
}, "codex-subscription: quota forecast warm-up");
|
|
2791
3275
|
const resetCreditService = createCodexResetCreditService({
|
|
2792
3276
|
getAuth: resolveAuth,
|
|
2793
3277
|
readCredential: (options) => store.read(PROVIDER, options),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-codex-subscription",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.1-beta.0",
|
|
4
4
|
"description": "Use ChatGPT and Codex subscriptions in DeepSeek Harness with OAuth, quota, safe resets, web search, images, and Fast mode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -77,22 +77,22 @@
|
|
|
77
77
|
},
|
|
78
78
|
"peerDependencies": {
|
|
79
79
|
"@deepseek-ai/cordis": "4.0.1 || 4.0.2",
|
|
80
|
-
"@deepseek-ai/dsh-api-remotes": "0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-alpha.2 || 0.1.2-alpha.3",
|
|
81
|
-
"@deepseek-ai/dsh-client-connection": "0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-alpha.2 || 0.1.2-alpha.3",
|
|
82
|
-
"@deepseek-ai/dsh-client-locale": "0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-alpha.2 || 0.1.2-alpha.3",
|
|
83
|
-
"@deepseek-ai/dsh-client-runtime": "0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-alpha.2 || 0.1.2-alpha.3",
|
|
84
|
-
"@deepseek-ai/dsh-client-ui-conversation": "0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-alpha.2 || 0.1.2-alpha.3",
|
|
85
|
-
"@deepseek-ai/dsh-client-ui-model-selection": "0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-alpha.2 || 0.1.2-alpha.3",
|
|
86
|
-
"@deepseek-ai/dsh-client-ui-primitives": "0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-alpha.2 || 0.1.2-alpha.3",
|
|
87
|
-
"@deepseek-ai/dsh-client-ui-settings": "0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-alpha.2 || 0.1.2-alpha.3",
|
|
88
|
-
"@deepseek-ai/dsh-client-ui-slots": "0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-alpha.2 || 0.1.2-alpha.3",
|
|
89
|
-
"@deepseek-ai/dsh-client-ui-tool": "0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-alpha.2 || 0.1.2-alpha.3",
|
|
90
|
-
"@deepseek-ai/dsh-credentials": "0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-alpha.2 || 0.1.2-alpha.3",
|
|
91
|
-
"@deepseek-ai/dsh-llm": "0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-alpha.2 || 0.1.2-alpha.3",
|
|
92
|
-
"@deepseek-ai/dsh-llm-pi-ai": "0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-alpha.2 || 0.1.2-alpha.3",
|
|
93
|
-
"@deepseek-ai/dsh-settings": "0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-alpha.2 || 0.1.2-alpha.3",
|
|
94
|
-
"@deepseek-ai/dsh-tools": "0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-alpha.2 || 0.1.2-alpha.3",
|
|
95
|
-
"@deepseek-ai/dsh-web": "0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-alpha.2 || 0.1.2-alpha.3",
|
|
80
|
+
"@deepseek-ai/dsh-api-remotes": "0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-alpha.2 || 0.1.2-alpha.3 || 0.1.2-alpha.4",
|
|
81
|
+
"@deepseek-ai/dsh-client-connection": "0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-alpha.2 || 0.1.2-alpha.3 || 0.1.2-alpha.4",
|
|
82
|
+
"@deepseek-ai/dsh-client-locale": "0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-alpha.2 || 0.1.2-alpha.3 || 0.1.2-alpha.4",
|
|
83
|
+
"@deepseek-ai/dsh-client-runtime": "0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-alpha.2 || 0.1.2-alpha.3 || 0.1.2-alpha.4",
|
|
84
|
+
"@deepseek-ai/dsh-client-ui-conversation": "0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-alpha.2 || 0.1.2-alpha.3 || 0.1.2-alpha.4",
|
|
85
|
+
"@deepseek-ai/dsh-client-ui-model-selection": "0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-alpha.2 || 0.1.2-alpha.3 || 0.1.2-alpha.4",
|
|
86
|
+
"@deepseek-ai/dsh-client-ui-primitives": "0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-alpha.2 || 0.1.2-alpha.3 || 0.1.2-alpha.4",
|
|
87
|
+
"@deepseek-ai/dsh-client-ui-settings": "0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-alpha.2 || 0.1.2-alpha.3 || 0.1.2-alpha.4",
|
|
88
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-alpha.2 || 0.1.2-alpha.3 || 0.1.2-alpha.4",
|
|
89
|
+
"@deepseek-ai/dsh-client-ui-tool": "0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-alpha.2 || 0.1.2-alpha.3 || 0.1.2-alpha.4",
|
|
90
|
+
"@deepseek-ai/dsh-credentials": "0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-alpha.2 || 0.1.2-alpha.3 || 0.1.2-alpha.4",
|
|
91
|
+
"@deepseek-ai/dsh-llm": "0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-alpha.2 || 0.1.2-alpha.3 || 0.1.2-alpha.4",
|
|
92
|
+
"@deepseek-ai/dsh-llm-pi-ai": "0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-alpha.2 || 0.1.2-alpha.3 || 0.1.2-alpha.4",
|
|
93
|
+
"@deepseek-ai/dsh-settings": "0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-alpha.2 || 0.1.2-alpha.3 || 0.1.2-alpha.4",
|
|
94
|
+
"@deepseek-ai/dsh-tools": "0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-alpha.2 || 0.1.2-alpha.3 || 0.1.2-alpha.4",
|
|
95
|
+
"@deepseek-ai/dsh-web": "0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-alpha.2 || 0.1.2-alpha.3 || 0.1.2-alpha.4",
|
|
96
96
|
"@deepseek-ai/schemastery": "3.18.1",
|
|
97
97
|
"@earendil-works/pi-ai": "0.82.1",
|
|
98
98
|
"react": "^18.2.0",
|