dsh-codex-subscription 1.7.2 → 1.7.4

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 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.7.2` package below; do not install a moving branch.
9
+ - Use the exact `1.7.4` 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.7.2
37
+ npx -y @deepseek-ai/dsh@0.1.1-rc.2 plugin --profile web add dsh-codex-subscription@1.7.4
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.7.2
43
+ dsh plugin --profile web add dsh-codex-subscription@1.7.4
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.7.2` appears exactly once.
61
+ 1. `dsh-codex-subscription` version `1.7.4` 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
 
@@ -8,6 +8,7 @@ This project depends on software distributed under its own terms. The dependency
8
8
  | `@earendil-works/pi-ai` 0.82.1 | OpenAI Codex OAuth, model catalog, Responses transport, and WebSocket continuation | MIT | https://github.com/earendil-works/pi |
9
9
  | React | DSH settings component runtime | MIT | https://github.com/facebook/react |
10
10
  | Heroicons | Composer speed icon | MIT | https://github.com/tailwindlabs/heroicons |
11
+ | `https-proxy-agent` 7.0.6 | HTTPS proxy transport for Codex OAuth token requests | MIT | https://github.com/TooTallNate/proxy-agents |
11
12
  | tsdown | Development-time bundler | MIT | https://github.com/rolldown/tsdown |
12
13
 
13
14
  No third-party project endorses this community plugin. See each installed package for its complete license text and transitive dependency notices.
package/lib/client.js CHANGED
@@ -436,6 +436,7 @@ window.__ModuleLoader__.load({
436
436
  .codexSubscriptionCreditSection{display:flex;flex-direction:column;gap:7px}.codexSubscriptionCreditNote{font-size:11px;line-height:17px;color:var(--dsw-alias-label-tertiary)}.codexSubscriptionCreditRows{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px}.codexSubscriptionCreditBalance,.codexSubscriptionSpendLimit{min-width:0;border-radius:10px;padding:12px 14px;background:var(--dsw-alias-bg-module-platform)}
437
437
  .codexSubscriptionCreditBalance{display:flex;flex-direction:column;gap:6px}.codexSubscriptionCreditBalance span,.codexSubscriptionCreditLabel{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}.codexSubscriptionCreditBalance strong{font:600 18px/24px ui-monospace,SFMono-Regular,Consolas,monospace;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}
438
438
  .codexSubscriptionResetSummary{display:flex;align-items:center;justify-content:space-between;gap:10px}.codexSubscriptionResetMeta{display:flex;min-width:0;flex-direction:column;gap:1px}.codexSubscriptionResetBalance{display:flex;flex-direction:column;gap:8px}.codexSubscriptionResetBalance .codexSubscriptionActions{justify-content:flex-start}.codexSubscriptionResetFlow{display:flex;flex-direction:column;gap:10px;border-top:1px solid var(--dsw-alias-border-l2);padding-top:10px}.codexSubscriptionResetFlow h4{margin:0;font-size:13px;line-height:20px;font-weight:500}.codexSubscriptionResetWarning{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}.codexSubscriptionResetExpiry{font-size:11px;line-height:17px;color:var(--dsw-alias-label-tertiary)}.codexSubscriptionResetCheck{display:flex;align-items:flex-start;gap:8px;padding:9px 10px;border-radius:8px;background:var(--dsw-alias-bg-module-platform);font-size:12px;line-height:18px;color:var(--dsw-alias-label-primary);cursor:pointer}.codexSubscriptionResetCheck input{margin:3px 0 0;accent-color:var(--dsw-alias-label-primary)}.codexSubscriptionResetFinal{border-color:var(--dsw-alias-state-error-primary)!important;color:var(--dsw-alias-state-error-primary)!important}.codexSubscriptionResetResult{font-size:12px;line-height:18px;color:var(--dsw-alias-state-success-primary)}
439
+ .codexSubscriptionResetUse:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.codexSubscriptionResetUse:focus-visible{outline:2px solid var(--dsw-alias-border-l3);outline-offset:1px}
439
440
  .codexSubscriptionSpendLimit{display:flex;flex-direction:column;gap:8px}.codexSubscriptionSpendTop{display:flex;align-items:baseline;justify-content:space-between;gap:12px}.codexSubscriptionSpendTop strong{font:600 16px/22px ui-monospace,SFMono-Regular,Consolas,monospace;font-variant-numeric:tabular-nums}.codexSubscriptionSpendLimit progress{width:100%;height:6px;border:0;border-radius:999px;overflow:hidden;background:var(--dsw-alias-border-l3);accent-color:var(--dsw-alias-brand-primary,#3964fe);-webkit-appearance:none;appearance:none}.codexSubscriptionSpendLimit progress::-webkit-progress-bar{background:var(--dsw-alias-border-l3);border-radius:999px}.codexSubscriptionSpendLimit progress::-webkit-progress-value{background:var(--dsw-alias-brand-primary,#3964fe);border-radius:999px}.codexSubscriptionSpendLimit progress::-moz-progress-bar{background:var(--dsw-alias-brand-primary,#3964fe);border-radius:999px}
440
441
  .codexComposerQuota{display:inline-flex;align-items:center;flex:0 0 auto;height:28px;box-sizing:border-box;padding:0;color:var(--dsw-alias-label-secondary);font-family:inherit;font-size:12px;line-height:20px;font-weight:500;font-variant-numeric:tabular-nums;white-space:nowrap;user-select:none}.codexComposerQuotaBar{display:block;width:40px;height:4px;border:0;border-radius:999px;overflow:hidden;background:var(--dsw-alias-border-l3);accent-color:var(--dsw-alias-label-secondary);-webkit-appearance:none;appearance:none}.codexComposerQuotaBar::-webkit-progress-bar{background:var(--dsw-alias-border-l3);border-radius:999px}.codexComposerQuotaBar::-webkit-progress-value{background:var(--dsw-alias-label-secondary);border-radius:999px}.codexComposerQuotaBar::-moz-progress-bar{background:var(--dsw-alias-label-secondary);border-radius:999px}
441
442
  .codexModelSelect{position:relative;min-width:0}.codexModelSelectTrigger{display:flex;align-items:center;gap:4px;min-width:0;max-width:min(360px,45cqw);height:28px;padding:0 4px 0 8px;border:0;border-radius:24px;outline:0;background:transparent;color:var(--dsw-alias-label-secondary);font-size:13px;font-weight:500;line-height:20px;cursor:pointer}.codexModelSelectTrigger:hover:not(:disabled),.codexModelSelectTrigger[aria-expanded=true]{background:var(--dsw-alias-interactive-bg-hover)}.codexModelSelectTrigger:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-border-l3)}.codexModelSelectTrigger:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}.codexModelSelectBolt{display:block;flex:none;width:14px;height:14px;color:var(--dsw-alias-label-primary)}.codexModelSelectLabel{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.codexModelSelectEffort{flex:none;color:var(--dsw-alias-label-caption)}.codexModelSelectChevron{flex:none;color:var(--dsw-alias-label-caption);transition:transform 120ms}.codexModelSelectTrigger[aria-expanded=true] .codexModelSelectChevron{transform:rotate(180deg)}
@@ -1385,6 +1386,7 @@ window.__ModuleLoader__.load({
1385
1386
  return () => window.clearInterval(timer);
1386
1387
  }, [flow?.id, flow?.phase]);
1387
1388
  const begin = (method) => {
1389
+ setFlow(void 0);
1388
1390
  setBusy(true);
1389
1391
  setError(void 0);
1390
1392
  call("login/start", {
@@ -1744,6 +1746,7 @@ window.__ModuleLoader__.load({
1744
1746
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1745
1747
  className: "codexSubscriptionActions",
1746
1748
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1749
+ className: "codexSubscriptionResetUse",
1747
1750
  type: "button",
1748
1751
  variant: "outline",
1749
1752
  disabled: resetBusy,
package/lib/index.js CHANGED
@@ -3,7 +3,10 @@ import { LlmError, createUserMessage } from "@deepseek-ai/dsh-llm";
3
3
  import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
4
4
  import { settingsNamespace } from "@deepseek-ai/dsh-settings";
5
5
  import z from "@deepseek-ai/schemastery";
6
- import { spawn } from "node:child_process";
6
+ import { execFile, spawn } from "node:child_process";
7
+ import { request } from "node:https";
8
+ import { promisify } from "node:util";
9
+ import { HttpsProxyAgent } from "https-proxy-agent";
7
10
  import { openaiCodexProvider as createOpenAICodexProvider } from "@earendil-works/pi-ai/providers/openai-codex";
8
11
  import { createModels } from "@earendil-works/pi-ai";
9
12
  import { randomUUID } from "node:crypto";
@@ -104,7 +107,8 @@ var DshOAuthCredentialStore = class {
104
107
  }
105
108
  };
106
109
  /** Return only account state that is safe to expose to the browser client. */
107
- function createCodexAuthService(models, store) {
110
+ function createCodexAuthService(models, store, options = {}) {
111
+ const runLogin = options.runLogin ?? ((run) => run());
108
112
  return Object.freeze({
109
113
  async status(options) {
110
114
  const current = await store.read(PROVIDER$1, options);
@@ -120,7 +124,7 @@ function createCodexAuthService(models, store) {
120
124
  };
121
125
  },
122
126
  login(interaction) {
123
- return models.login(PROVIDER$1, "oauth", interaction);
127
+ return runLogin(() => models.login(PROVIDER$1, "oauth", interaction));
124
128
  },
125
129
  logout(options) {
126
130
  return models.logout(PROVIDER$1, options);
@@ -218,6 +222,15 @@ const publicPrompt = (prompt) => ({
218
222
  message: String(prompt.message ?? ""),
219
223
  ...typeof prompt.placeholder === "string" ? { placeholder: prompt.placeholder } : {}
220
224
  });
225
+ function classifyLoginFailure(error) {
226
+ const message = error instanceof Error ? error.message : "";
227
+ if (/token exchange failed/iu.test(message)) return "token-exchange";
228
+ if (/fetch failed|\b(?:ECONN|ENOTFOUND|ETIMEDOUT|CERT_|socket|network)\b/iu.test(message)) return "network";
229
+ if (/extract accountId|account[_ -]?id/iu.test(message)) return "account-claim";
230
+ if (/credential|credentials-local|OAuth JSON/iu.test(message)) return "credential-store";
231
+ if (/Missing authorization code|State mismatch|callback/iu.test(message)) return "callback";
232
+ return "provider";
233
+ }
221
234
  /** Own one host-side login without exposing tokens to the browser client. */
222
235
  var CodexLoginCoordinator = class {
223
236
  #sessions = /* @__PURE__ */ new Map();
@@ -234,13 +247,23 @@ var CodexLoginCoordinator = class {
234
247
  if (active === void 0) return { phase: "idle" };
235
248
  return {
236
249
  method: active.view.method,
237
- phase: active.view.phase
250
+ phase: active.view.phase,
251
+ ...active.view.phase === "failed" ? { failure: classifyLoginFailure(active.hostError) } : {}
238
252
  };
239
253
  }
240
254
  async start({ method }) {
241
255
  if (!LOGIN_METHODS.has(method)) throw new Error(`unsupported Codex login method: ${String(method)}`);
242
256
  const active = this.#activeId === void 0 ? void 0 : this.#sessions.get(this.#activeId);
243
- if (active !== void 0 && !TERMINAL_PHASES.has(active.view.phase)) throw new Error("a Codex login is already active");
257
+ if (active !== void 0 && !TERMINAL_PHASES.has(active.view.phase)) {
258
+ active.view = {
259
+ id: active.view.id,
260
+ provider: "openai-codex",
261
+ method: active.view.method,
262
+ phase: "cancelled",
263
+ authenticated: false
264
+ };
265
+ active.controller.abort(/* @__PURE__ */ new Error("Codex login replaced by a new attempt"));
266
+ }
244
267
  if (active !== void 0) this.#sessions.delete(active.view.id);
245
268
  const id = this.createId();
246
269
  const ready = deferred();
@@ -259,7 +282,7 @@ var CodexLoginCoordinator = class {
259
282
  };
260
283
  this.#sessions.set(id, session);
261
284
  this.#activeId = id;
262
- const publishReady = () => ready.resolve(this.read(id));
285
+ const publishReady = () => ready.resolve(publicClone(session.view));
263
286
  const interaction = {
264
287
  signal: controller.signal,
265
288
  prompt: async (prompt) => {
@@ -451,6 +474,158 @@ function createCodexRpcHandler(coordinator, options = {}) {
451
474
  };
452
475
  }
453
476
  //#endregion
477
+ //#region src/oauth-network.js
478
+ const execFileAsync = promisify(execFile);
479
+ const CODEX_AUTH_HOST = "auth.openai.com";
480
+ const MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
481
+ let patchQueue = Promise.resolve();
482
+ function normalizeProxy(raw) {
483
+ if (typeof raw !== "string" || raw.trim() === "") return void 0;
484
+ const value = raw.trim().includes("://") ? raw.trim() : `http://${raw.trim()}`;
485
+ try {
486
+ const url = new URL(value);
487
+ if (!["http:", "https:"].includes(url.protocol) || url.hostname === "") return void 0;
488
+ return url.toString();
489
+ } catch {
490
+ return;
491
+ }
492
+ }
493
+ function bypassesProxy(hostname, port, rawNoProxy) {
494
+ if (typeof rawNoProxy !== "string" || rawNoProxy.trim() === "") return false;
495
+ return rawNoProxy.split(/[\s,]+/u).some((raw) => {
496
+ const entry = raw.trim().toLowerCase();
497
+ if (entry === "*") return true;
498
+ if (entry === "") return false;
499
+ const match = /^(.*?)(?::(\d+))?$/u.exec(entry);
500
+ const host = match?.[1]?.replace(/^\./u, "");
501
+ const entryPort = match?.[2];
502
+ if (!host || entryPort && entryPort !== port) return false;
503
+ return hostname === host || hostname.endsWith(`.${host}`);
504
+ });
505
+ }
506
+ function proxyFromEnvironment(env = process.env, target = new URL(`https://${CODEX_AUTH_HOST}/`)) {
507
+ if (bypassesProxy(target.hostname.toLowerCase(), target.port || "443", env.NO_PROXY ?? env.no_proxy)) return void 0;
508
+ return normalizeProxy(env.HTTPS_PROXY ?? env.https_proxy ?? env.ALL_PROXY ?? env.all_proxy);
509
+ }
510
+ function selectWindowsProxy(value) {
511
+ if (typeof value !== "string") return void 0;
512
+ const entries = value.split(";").map((item) => item.trim()).filter(Boolean);
513
+ const https = entries.find((item) => /^https=/iu.test(item));
514
+ const http = entries.find((item) => /^http=/iu.test(item));
515
+ const selected = (https ?? http ?? entries.find((item) => !item.includes("=")))?.replace(/^[^=]+=/u, "");
516
+ return normalizeProxy(selected);
517
+ }
518
+ async function windowsSystemProxy(options = {}) {
519
+ const run = options.execFile ?? execFileAsync;
520
+ const reg = `${process.env.SystemRoot ?? "C:\\Windows"}\\System32\\reg.exe`;
521
+ const key = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings";
522
+ try {
523
+ const enabled = await run(reg, [
524
+ "query",
525
+ key,
526
+ "/v",
527
+ "ProxyEnable"
528
+ ], {
529
+ windowsHide: true,
530
+ encoding: "utf8"
531
+ });
532
+ if (!/REG_DWORD\s+0x1\b/iu.test(enabled.stdout)) return void 0;
533
+ const configured = await run(reg, [
534
+ "query",
535
+ key,
536
+ "/v",
537
+ "ProxyServer"
538
+ ], {
539
+ windowsHide: true,
540
+ encoding: "utf8"
541
+ });
542
+ return selectWindowsProxy(/^\s*ProxyServer\s+REG_\w+\s+(.+)$/imu.exec(configured.stdout)?.[1]);
543
+ } catch {
544
+ return;
545
+ }
546
+ }
547
+ async function macSystemProxy(options = {}) {
548
+ const run = options.execFile ?? execFileAsync;
549
+ try {
550
+ const result = await run("/usr/sbin/scutil", ["--proxy"], { encoding: "utf8" });
551
+ if (!/^\s*HTTPSEnable\s*:\s*1\s*$/imu.test(result.stdout)) return void 0;
552
+ const host = /^\s*HTTPSProxy\s*:\s*(\S+)\s*$/imu.exec(result.stdout)?.[1];
553
+ const port = /^\s*HTTPSPort\s*:\s*(\d+)\s*$/imu.exec(result.stdout)?.[1];
554
+ return normalizeProxy(host && port ? `${host}:${port}` : void 0);
555
+ } catch {
556
+ return;
557
+ }
558
+ }
559
+ async function resolveCodexOAuthProxy(options = {}) {
560
+ const envProxy = proxyFromEnvironment(options.env);
561
+ if (envProxy) return envProxy;
562
+ const platform = options.platform ?? process.platform;
563
+ if (platform === "win32") return windowsSystemProxy(options);
564
+ if (platform === "darwin") return macSystemProxy(options);
565
+ }
566
+ function bodyBytes(body) {
567
+ if (body === void 0 || body === null) return void 0;
568
+ if (typeof body === "string") return Buffer.from(body);
569
+ if (body instanceof URLSearchParams) return Buffer.from(body.toString());
570
+ if (body instanceof Uint8Array) return Buffer.from(body);
571
+ throw new TypeError("Unsupported Codex OAuth request body");
572
+ }
573
+ function fetchThroughProxy(input, init, proxyUrl) {
574
+ const target = new URL(typeof input === "string" || input instanceof URL ? input : input.url);
575
+ const body = bodyBytes(init?.body);
576
+ const headers = new Headers(init?.headers);
577
+ if (body && !headers.has("content-length")) headers.set("content-length", String(body.byteLength));
578
+ return new Promise((resolve, reject) => {
579
+ const request$1 = request(target, {
580
+ method: init?.method ?? "GET",
581
+ headers: Object.fromEntries(headers.entries()),
582
+ agent: new HttpsProxyAgent(proxyUrl),
583
+ signal: init?.signal
584
+ }, (response) => {
585
+ const chunks = [];
586
+ let total = 0;
587
+ response.on("data", (chunk) => {
588
+ total += chunk.length;
589
+ if (total > MAX_RESPONSE_BYTES) {
590
+ request$1.destroy(/* @__PURE__ */ new Error("Codex OAuth response exceeded the safety limit"));
591
+ return;
592
+ }
593
+ chunks.push(chunk);
594
+ });
595
+ response.on("end", () => resolve(new Response(Buffer.concat(chunks), {
596
+ status: response.statusCode ?? 500,
597
+ statusText: response.statusMessage,
598
+ headers: response.headers
599
+ })));
600
+ });
601
+ request$1.on("error", reject);
602
+ if (body) request$1.write(body);
603
+ request$1.end();
604
+ });
605
+ }
606
+ async function withCodexOAuthNetwork(run, options = {}) {
607
+ const previous = patchQueue;
608
+ let release;
609
+ patchQueue = new Promise((resolve) => {
610
+ release = resolve;
611
+ });
612
+ await previous;
613
+ const proxyUrl = await resolveCodexOAuthProxy(options);
614
+ const original = globalThis.fetch;
615
+ const proxyFetch = options.fetchThroughProxy ?? fetchThroughProxy;
616
+ const wrapper = proxyUrl ? (input, init) => {
617
+ const target = new URL(typeof input === "string" || input instanceof URL ? input : input.url);
618
+ return target.protocol === "https:" && target.hostname === CODEX_AUTH_HOST ? proxyFetch(input, init, proxyUrl) : original(input, init);
619
+ } : original;
620
+ globalThis.fetch = wrapper;
621
+ try {
622
+ return await run();
623
+ } finally {
624
+ if (globalThis.fetch === wrapper) globalThis.fetch = original;
625
+ release();
626
+ }
627
+ }
628
+ //#endregion
454
629
  //#region src/settings-contract.js
455
630
  const SETTINGS_NAMESPACE = "codex-subscription";
456
631
  const QUICK_QUOTA_MODE_FIELD = "quickQuotaMode";
@@ -526,7 +701,7 @@ function openaiCodexSubscriptionProvider({ resolveSpeedMode = () => void 0 } = {
526
701
  }
527
702
  //#endregion
528
703
  //#region src/version.js
529
- const PACKAGE_VERSION = "1.7.2";
704
+ const PACKAGE_VERSION = "1.7.4";
530
705
  const USER_AGENT = `dsh-codex-subscription/${PACKAGE_VERSION}`;
531
706
  //#endregion
532
707
  //#region src/codex-search.js
@@ -1567,7 +1742,7 @@ function apply(ctx) {
1567
1742
  select(settings.get());
1568
1743
  return settings.watch(select);
1569
1744
  }, "codex-subscription: search provider selection");
1570
- const auth = createCodexAuthService(authModels, store);
1745
+ const auth = createCodexAuthService(authModels, store, { runLogin: withCodexOAuthNetwork });
1571
1746
  const coordinator = new CodexLoginCoordinator(auth);
1572
1747
  const usageReader = createCodexUsageReader({
1573
1748
  getAuth: resolveAuth,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-codex-subscription",
3
- "version": "1.7.2",
3
+ "version": "1.7.4",
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",
@@ -122,6 +122,9 @@
122
122
  "react-dom": "18.3.1",
123
123
  "tsdown": "0.22.2"
124
124
  },
125
+ "dependencies": {
126
+ "https-proxy-agent": "7.0.6"
127
+ },
125
128
  "scripts": {
126
129
  "build": "tsdown --config tsdown.config.mjs",
127
130
  "test": "node --test tests/*.test.mjs",