@sunshaobei/dsh-codex-auth 3.0.8 → 3.1.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/lib/client.js CHANGED
@@ -337,6 +337,7 @@ window.__ModuleLoader__.load({
337
337
  return () => { observer.disconnect(); marked.forEach(button => button.classList.remove("codex-auth-nav")); };
338
338
  }, "dsh-codex-auth: ChatGPT navigation icon");
339
339
  // ---- plugin detail page (plugins.bundle.config, same as dsh-context) ----
340
+ const CFG_KEY = "@sunshaobei/dsh-codex-auth"; // 必须等于包名:页面按 configured = bundles.has(pkg.name) 判断
340
341
  const settingsStore = (() => {
341
342
  let form; let state = {}; const listeners = new Set();
342
343
  const notify = () => listeners.forEach((fn) => fn());
package/lib/index.js CHANGED
@@ -29,6 +29,7 @@
29
29
  import http from 'node:http';
30
30
  import crypto from 'node:crypto';
31
31
  import { execFile } from 'node:child_process';
32
+ import { proxyFetch } from './proxy-fetch.mjs';
32
33
  import z from './vendor/schemastery.mjs';
33
34
  const RPC_CHANNEL = '/dsh-codex-auth';
34
35
  const CRED_KEY = 'llm-pi-ai/openai-codex';
@@ -97,7 +98,7 @@ function openBrowser(url) {
97
98
  });
98
99
  }
99
100
  function exchangeCode(code, verifier) {
100
- return fetch(TOKEN_URL, {
101
+ return proxyFetch(TOKEN_URL, {
101
102
  method: 'POST',
102
103
  signal: AbortSignal.timeout(30000),
103
104
  headers: { 'content-type': 'application/x-www-form-urlencoded' },
@@ -394,7 +395,7 @@ async function silentRefresh(credentials) {
394
395
  const payload = record && record.kind === 'grant' ? record.payload : undefined;
395
396
  if (!payload || payload.type !== 'oauth' || !payload.refresh)
396
397
  return;
397
- const res = await fetch(TOKEN_URL, {
398
+ const res = await proxyFetch(TOKEN_URL, {
398
399
  method: 'POST',
399
400
  signal: AbortSignal.timeout(30000),
400
401
  headers: { 'content-type': 'application/x-www-form-urlencoded' },
@@ -0,0 +1,157 @@
1
+ /**
2
+ * dsh-codex-auth — proxy-aware fetch for the OpenAI token endpoints.
3
+ *
4
+ * Why: Node's built-in fetch never reads the OS/system proxy configuration,
5
+ * so in regions where OpenAI is blocked the token exchange fails with 403
6
+ * unsupported_country_region_territory even when a system proxy (Clash 等)
7
+ * is running and the browser works fine.
8
+ *
9
+ * Strategy (cached after the first successful attempt):
10
+ * 1. explicit env proxies (HTTPS_PROXY / HTTP_PROXY / ALL_PROXY)
11
+ * 2. well-known local proxy ports (Clash 7897/7890, V2Ray 1087 …)
12
+ * 3. direct connection (no proxy)
13
+ *
14
+ * The request itself is issued as raw HTTP/1.1 over a TLS socket whose
15
+ * underlying connection tunnels through the proxy with HTTP CONNECT.
16
+ * Response parsing supports Content-Length, chunked and close-delimited
17
+ * bodies — enough for the small JSON payloads these endpoints return.
18
+ *
19
+ * Zero runtime dependencies.
20
+ */
21
+ import http from 'node:http';
22
+ import tls from 'node:tls';
23
+
24
+ const WELL_KNOWN_PORTS = [7897, 7890, 1087, 8118, 6152, 1080];
25
+ const PROBE_TIMEOUT_MS = 2500;
26
+ const REQUEST_TIMEOUT_MS = 30000;
27
+
28
+ const cache = { proxy: undefined }; // undefined = 未探测;null = 直连可用;string = 可用代理
29
+
30
+ function candidateProxies() {
31
+ const list = [];
32
+ for (const key of ['HTTPS_PROXY', 'https_proxy', 'HTTP_PROXY', 'http_proxy', 'ALL_PROXY', 'all_proxy']) {
33
+ const v = process.env[key];
34
+ if (v && /^https?:\/\//i.test(v)) list.push(v.replace(/\/+$/, ''));
35
+ }
36
+ for (const port of WELL_KNOWN_PORTS) list.push(`http://127.0.0.1:${port}`);
37
+ return [...new Set(list)];
38
+ }
39
+
40
+ /** 通过代理发起 CONNECT,返回到目标 host:port 的原始 TCP socket。 */
41
+ function connectViaProxy(proxyUrl, host, port, timeoutMs) {
42
+ return new Promise((resolve, reject) => {
43
+ let u;
44
+ try { u = new URL(proxyUrl); } catch (e) { reject(new Error(`bad proxy url: ${proxyUrl}`)); return; }
45
+ const req = http.request({
46
+ host: u.hostname,
47
+ port: Number(u.port || 80),
48
+ method: 'CONNECT',
49
+ path: `${host}:${port}`,
50
+ headers: { host: `${host}:${port}` },
51
+ timeout: timeoutMs,
52
+ });
53
+ req.on('connect', (res, socket) => {
54
+ if (res.statusCode === 200) resolve(socket);
55
+ else { socket.destroy(); reject(new Error(`proxy CONNECT ${res.statusCode}`)); }
56
+ });
57
+ req.on('timeout', () => { req.destroy(new Error('proxy connect timeout')); });
58
+ req.on('error', reject);
59
+ req.end();
60
+ });
61
+ }
62
+
63
+ /** 在已建立的 TLS socket 上发送请求并收集完整响应。 */
64
+ function requestOverSocket(socket, url, { method = 'POST', headers = {}, body = '' } = {}) {
65
+ return new Promise((resolve, reject) => {
66
+ const timeout = setTimeout(() => {
67
+ socket.destroy();
68
+ reject(new Error('request timeout'));
69
+ }, REQUEST_TIMEOUT_MS);
70
+ const head = [
71
+ `${method} ${url.pathname}${url.search} HTTP/1.1`,
72
+ `host: ${url.host}`,
73
+ 'connection: close',
74
+ ...Object.entries(headers).map(([k, v]) => `${k}: ${v}`),
75
+ ];
76
+ if (body) head.push(`content-length: ${Buffer.byteLength(body)}`);
77
+ socket.write(head.join('\r\n') + '\r\n\r\n' + (body || ''));
78
+ const chunks = [];
79
+ socket.on('data', (c) => chunks.push(c));
80
+ socket.on('error', (e) => { clearTimeout(timeout); reject(e); });
81
+ socket.on('close', () => {
82
+ clearTimeout(timeout);
83
+ try {
84
+ const raw = Buffer.concat(chunks).toString('utf8');
85
+ const sep = raw.indexOf('\r\n\r\n');
86
+ if (sep === -1) { reject(new Error('malformed proxy response')); return; }
87
+ const headText = raw.slice(0, sep);
88
+ let bodyText = raw.slice(sep + 4);
89
+ const statusLine = headText.split('\r\n')[0] || '';
90
+ const status = Number(statusLine.split(' ')[1]) || 0;
91
+ if (/transfer-encoding:\s*chunked/i.test(headText)) bodyText = dechunk(bodyText);
92
+ resolve({
93
+ status,
94
+ ok: status >= 200 && status < 300,
95
+ text: async () => bodyText,
96
+ json: async () => JSON.parse(bodyText),
97
+ });
98
+ } catch (e) { reject(e); }
99
+ });
100
+ });
101
+ }
102
+
103
+ function dechunk(text) {
104
+ let out = '';
105
+ let rest = text;
106
+ for (;;) {
107
+ const idx = rest.indexOf('\r\n');
108
+ if (idx === -1) break;
109
+ const size = parseInt(rest.slice(0, idx), 16);
110
+ if (!Number.isFinite(size) || size === 0) break;
111
+ out += rest.slice(idx + 2, idx + 2 + size);
112
+ rest = rest.slice(idx + 2 + size + 2);
113
+ }
114
+ return out;
115
+ }
116
+
117
+ /**
118
+ * 代理感知的 fetch(覆盖本插件用到的 Response 子集:ok / status / text / json)。
119
+ * 首次调用时按候选顺序探测并缓存可用的出口(代理或直连)。
120
+ */
121
+ export async function proxyFetch(url, options = {}) {
122
+ const u = new URL(url);
123
+ if (u.protocol !== 'https:') throw new Error('proxyFetch only supports https');
124
+ const port = Number(u.port || 443);
125
+ const candidates = [];
126
+ if (cache.proxy !== undefined) candidates.push(cache.proxy); // 已知可用出口优先
127
+ else candidates.push(...candidateProxies(), null);
128
+ let lastError;
129
+ for (const proxy of candidates) {
130
+ try {
131
+ let socket;
132
+ if (proxy) {
133
+ const raw = await connectViaProxy(proxy, u.hostname, port, PROBE_TIMEOUT_MS);
134
+ // 隧道 socket 上再包一层 TLS(目标是 https)
135
+ socket = tls.connect({ socket: raw, host: u.hostname, port, servername: u.hostname });
136
+ await new Promise((resolve, reject) => {
137
+ socket.once('secureConnect', resolve);
138
+ socket.once('error', reject);
139
+ setTimeout(() => { socket.destroy(); reject(new Error('tls handshake timeout')); }, PROBE_TIMEOUT_MS);
140
+ });
141
+ } else {
142
+ socket = tls.connect({ host: u.hostname, port, servername: u.hostname });
143
+ await new Promise((resolve, reject) => {
144
+ socket.once('secureConnect', resolve);
145
+ socket.once('error', reject);
146
+ setTimeout(() => { socket.destroy(); reject(new Error('direct connect timeout')); }, PROBE_TIMEOUT_MS);
147
+ });
148
+ }
149
+ const res = await requestOverSocket(socket, u, options);
150
+ cache.proxy = proxy; // 记住可用出口
151
+ return res;
152
+ } catch (e) {
153
+ lastError = e;
154
+ }
155
+ }
156
+ throw lastError instanceof Error ? lastError : new Error(String(lastError ?? 'all connection attempts failed'));
157
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sunshaobei/dsh-codex-auth",
3
3
  "description": "Codex 订阅授权管理 for the dsh web surface: a dedicated settings page (authorization status, validity/expiry, one-click browser OAuth) plus an expired-credential modal when a GPT model is selected. Host half runs the full OpenAI OAuth flow (PKCE S256, localhost:1455 callback) and writes the llm-pi-ai/openai-codex grant record through ctx.credentials.",
4
- "version": "3.0.8",
4
+ "version": "3.1.0",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/index.d.ts",