@sunshaobei/dsh-balance-panel 3.0.5 → 3.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -95,9 +95,12 @@ window.__ModuleLoader__.load({
95
95
  }
96
96
  if (family.length === 0) {
97
97
  var prov = String(sel.provider || "").toLowerCase();
98
- // DOM-fallback selection (no provider) matching no family:
99
- // show nothing — an empty prov would substring-match EVERY row.
100
- if (!prov) return null;
98
+ // A DOM-fallback selection has no provider: the model name alone
99
+ // matched no known family, so this model has no known quota
100
+ // source. Show nothing rather than a wrong provider — an empty
101
+ // prov would otherwise substring-match EVERY row.
102
+ if (!prov)
103
+ return null;
101
104
  for (var j = 0; j < specs.rows.length; j++) {
102
105
  var id = String(specs.rows[j].id || "").toLowerCase();
103
106
  if (prov.indexOf(id) > -1 || id.indexOf(prov) > -1)
package/lib/index.js CHANGED
@@ -264,6 +264,35 @@ async function chatgptAuthExists() {
264
264
  }
265
265
  /** In-memory token cache; refreshed on expiry or on auth failure. */
266
266
  let chatgptTokenCache = null;
267
+ // Live handle onto the credentials service — set in apply(). Lets the
268
+ // ChatGPT/Codex token resolver read the shared grant record that
269
+ // dsh-codex-auth writes (llm-pi-ai/openai-codex).
270
+ let codexCredentialsRef = null;
271
+ // Auto-detected local proxy for ChatGPT/Codex upstream calls. The usage and
272
+ // token endpoints are region-blocked on direct connections, and Node's fetch
273
+ // never reads the OS proxy — so when the panel row has no explicit proxy we
274
+ // probe the well-known local proxy ports once and remember the answer.
275
+ let localProxyCache = null; // null = not probed yet
276
+ async function detectLocalProxy() {
277
+ if (localProxyCache !== null)
278
+ return localProxyCache || undefined;
279
+ for (const port of [7897, 7890, 1087, 8118, 6152]) {
280
+ const ok = await new Promise((resolve) => {
281
+ const req = http.request({ host: '127.0.0.1', port, method: 'CONNECT', path: 'chatgpt.com:443', timeout: 2000 });
282
+ req.on('connect', (res, socket) => { socket.destroy(); resolve(res.statusCode === 200); });
283
+ req.on('error', () => resolve(false));
284
+ req.on('timeout', () => { req.destroy(); resolve(false); });
285
+ req.end();
286
+ });
287
+ if (ok) {
288
+ localProxyCache = 'http://127.0.0.1:' + port;
289
+ break;
290
+ }
291
+ }
292
+ if (localProxyCache === null)
293
+ localProxyCache = '';
294
+ return localProxyCache || undefined;
295
+ }
267
296
  async function readChatGPTAuthFile() {
268
297
  const authJson = chatgptAuthJsonPath();
269
298
  let raw;
@@ -632,6 +661,31 @@ function chatgptLoginStatus() {
632
661
  async function resolveChatGPTToken(timeoutMs, proxyUrl) {
633
662
  const now = Date.now();
634
663
  const cached = chatgptTokenCache?.bundle;
664
+ // Source 0: the shared grant record written by dsh-codex-auth
665
+ // (llm-pi-ai/openai-codex). Preferred when present and fresh: the user
666
+ // authorized through the Codex subscription settings page, so the panel
667
+ // should reflect THAT credential instead of ~/.codex/auth.json.
668
+ try {
669
+ if (codexCredentialsRef?.readRecord) {
670
+ const rec = await codexCredentialsRef.readRecord('llm-pi-ai/openai-codex');
671
+ const p = rec && rec.kind === 'grant' ? rec.payload : undefined;
672
+ if (p && p.access) {
673
+ if (!p.expires || p.expires - now > 30000) {
674
+ chatgptTokenCache = { bundle: { access_token: p.access, refresh_token: p.refresh, expires_at_ms: p.expires, account_id: p.accountId } };
675
+ return { accessToken: p.access, accountId: p.accountId };
676
+ }
677
+ if (p.refresh) {
678
+ const refreshed = await refreshChatGPTToken(p.refresh, timeoutMs, proxyUrl);
679
+ if (!refreshed.account_id)
680
+ refreshed.account_id = p.accountId;
681
+ await codexCredentialsRef.modifyRecord('llm-pi-ai/openai-codex', async () => ({ kind: 'grant', payload: { ...p, access: refreshed.access_token, refresh: refreshed.refresh_token || p.refresh, expires: refreshed.expires_at_ms } })).catch(() => { });
682
+ chatgptTokenCache = { bundle: refreshed };
683
+ return { accessToken: refreshed.access_token, accountId: refreshed.account_id };
684
+ }
685
+ }
686
+ }
687
+ }
688
+ catch { /* record absent or unreadable — fall through to the other sources */ }
635
689
  if (cached && cached.access_token && (!cached.expires_at_ms || cached.expires_at_ms - now > 30000)) {
636
690
  return { accessToken: cached.access_token, accountId: cached.account_id };
637
691
  }
@@ -1640,7 +1694,7 @@ async function fetchRow(ctx, provider, proxies, clientProxyUrl) {
1640
1694
  // query AND token refresh — same region constraints apply.
1641
1695
  const cgProxy = clientProxyUrl !== undefined
1642
1696
  ? clientProxyUrl
1643
- : (provider.proxy !== undefined ? proxies[provider.proxy] : undefined);
1697
+ : (provider.proxy !== undefined ? proxies[provider.proxy] : await detectLocalProxy());
1644
1698
  let { accessToken, accountId } = await resolveChatGPTToken(UPSTREAM_TIMEOUT_MS, cgProxy);
1645
1699
  let body;
1646
1700
  try {
@@ -1880,6 +1934,7 @@ function registerQuotaRpc(ctx, handler) {
1880
1934
  * @param config - raw plugin config (schema-processed by the loader).
1881
1935
  */
1882
1936
  export function apply(ctx, config = {}) {
1937
+ codexCredentialsRef = ctx.credentials;
1883
1938
  const raw = config && typeof config === 'object' ? config : {};
1884
1939
  const refreshMs = numOf(raw.refreshMs, 60000);
1885
1940
  if (!(refreshMs >= 5000))
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sunshaobei/dsh-balance-panel",
3
3
  "description": "Provider quota/balance widget for the dsh web surface: an inline sidebar card above the Settings row (collapsed glanceable row expanding into a Harness-native card with a settings panel), fed by a loopback Connection RPC channel whose host half proxies each provider with credentials that never reach the browser.",
4
- "version": "3.0.5",
4
+ "version": "3.0.7",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/index.d.ts",
package/src/client.ts CHANGED
@@ -25,7 +25,7 @@
25
25
  * follows the product theme (light/dark) instead of carrying its own palette.
26
26
  */
27
27
  (window as any).__ModuleLoader__.load({
28
- id: "dsh-balance-panel",
28
+ id: "@sunshaobei/dsh-balance-panel",
29
29
  factory: (require) => {
30
30
  var module = { exports: {} as any };
31
31
  var exports = module.exports;
package/src/index.ts CHANGED
@@ -287,6 +287,31 @@ interface ChatGPTTokenBundle {
287
287
 
288
288
  /** In-memory token cache; refreshed on expiry or on auth failure. */
289
289
  let chatgptTokenCache: { bundle: ChatGPTTokenBundle } | null = null;
290
+ // Live handle onto the credentials service — set in apply(). Lets the
291
+ // ChatGPT/Codex token resolver read the shared grant record that
292
+ // dsh-codex-auth writes (llm-pi-ai/openai-codex).
293
+ let codexCredentialsRef: any = null;
294
+
295
+ // Auto-detected local proxy for ChatGPT/Codex upstream calls. The usage and
296
+ // token endpoints are region-blocked on direct connections, and Node's fetch
297
+ // never reads the OS proxy — so when the panel row has no explicit proxy we
298
+ // probe the well-known local proxy ports once and remember the answer.
299
+ let localProxyCache: string | undefined | null = null; // null = not probed yet
300
+ async function detectLocalProxy(): Promise<string | undefined> {
301
+ if (localProxyCache !== null) return localProxyCache || undefined;
302
+ for (const port of [7897, 7890, 1087, 8118, 6152]) {
303
+ const ok = await new Promise<boolean>((resolve) => {
304
+ const req = http.request({ host: '127.0.0.1', port, method: 'CONNECT', path: 'chatgpt.com:443', timeout: 2000 });
305
+ req.on('connect', (res, socket) => { socket.destroy(); resolve(res.statusCode === 200); });
306
+ req.on('error', () => resolve(false));
307
+ req.on('timeout', () => { req.destroy(); resolve(false); });
308
+ req.end();
309
+ });
310
+ if (ok) { localProxyCache = 'http://127.0.0.1:' + port; break; }
311
+ }
312
+ if (localProxyCache === null) localProxyCache = '';
313
+ return localProxyCache || undefined;
314
+ }
290
315
 
291
316
  async function readChatGPTAuthFile(): Promise<ChatGPTTokenBundle> {
292
317
  const authJson = chatgptAuthJsonPath();
@@ -653,6 +678,29 @@ function chatgptLoginStatus(): { active: boolean; verificationUrl?: string; user
653
678
  async function resolveChatGPTToken(timeoutMs: number, proxyUrl?: string): Promise<{ accessToken: string; accountId?: string }> {
654
679
  const now = Date.now();
655
680
  const cached = chatgptTokenCache?.bundle;
681
+ // Source 0: the shared grant record written by dsh-codex-auth
682
+ // (llm-pi-ai/openai-codex). Preferred when present and fresh: the user
683
+ // authorized through the Codex subscription settings page, so the panel
684
+ // should reflect THAT credential instead of ~/.codex/auth.json.
685
+ try {
686
+ if (codexCredentialsRef?.readRecord) {
687
+ const rec: any = await codexCredentialsRef.readRecord('llm-pi-ai/openai-codex');
688
+ const p = rec && rec.kind === 'grant' ? rec.payload : undefined;
689
+ if (p && p.access) {
690
+ if (!p.expires || p.expires - now > 30_000) {
691
+ chatgptTokenCache = { bundle: { access_token: p.access, refresh_token: p.refresh, expires_at_ms: p.expires, account_id: p.accountId } };
692
+ return { accessToken: p.access, accountId: p.accountId };
693
+ }
694
+ if (p.refresh) {
695
+ const refreshed = await refreshChatGPTToken(p.refresh, timeoutMs, proxyUrl);
696
+ if (!refreshed.account_id) refreshed.account_id = p.accountId;
697
+ await codexCredentialsRef.modifyRecord('llm-pi-ai/openai-codex', async () => ({ kind: 'grant', payload: { ...p, access: refreshed.access_token, refresh: refreshed.refresh_token || p.refresh, expires: refreshed.expires_at_ms } })).catch(() => {});
698
+ chatgptTokenCache = { bundle: refreshed };
699
+ return { accessToken: refreshed.access_token, accountId: refreshed.account_id };
700
+ }
701
+ }
702
+ }
703
+ } catch { /* record absent or unreadable — fall through to the other sources */ }
656
704
  if (cached && cached.access_token && (!cached.expires_at_ms || cached.expires_at_ms - now > 30_000)) {
657
705
  return { accessToken: cached.access_token, accountId: cached.account_id };
658
706
  }
@@ -1601,7 +1649,7 @@ async function fetchRow(ctx, provider, proxies, clientProxyUrl) {
1601
1649
  // query AND token refresh — same region constraints apply.
1602
1650
  const cgProxy = clientProxyUrl !== undefined
1603
1651
  ? clientProxyUrl
1604
- : (provider.proxy !== undefined ? proxies[provider.proxy] : undefined);
1652
+ : (provider.proxy !== undefined ? proxies[provider.proxy] : await detectLocalProxy());
1605
1653
  let { accessToken, accountId } = await resolveChatGPTToken(UPSTREAM_TIMEOUT_MS, cgProxy);
1606
1654
  let body: any;
1607
1655
  try {
@@ -1830,6 +1878,7 @@ async function fetchRow(ctx, provider, proxies, clientProxyUrl) {
1830
1878
  * @param config - raw plugin config (schema-processed by the loader).
1831
1879
  */
1832
1880
  export function apply(ctx, config: Record<string, any> = {}) {
1881
+ codexCredentialsRef = ctx.credentials;
1833
1882
  const raw: Record<string, any> = config && typeof config === 'object' ? config : {};
1834
1883
  const refreshMs = numOf(raw.refreshMs, 60000);
1835
1884
  if (!(refreshMs >= 5000)) throw new Error('quota-panel: config.refreshMs must be >= 5000');