@vacbo/opencode-anthropic-fix 0.0.43 → 0.0.44

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.
@@ -1,5 +1,5 @@
1
1
  // src/bun-proxy.ts
2
- var PORT = parseInt(process.argv[2] || "0", 10);
2
+ var PORT = parseInt(process.argv[2] || "48372", 10);
3
3
  var server = Bun.serve({
4
4
  port: PORT,
5
5
  async fetch(req) {
@@ -6375,12 +6375,28 @@ function formatSwitchReason(status, reason) {
6375
6375
 
6376
6376
  // src/bun-fetch.ts
6377
6377
  import { execFileSync, spawn } from "node:child_process";
6378
- import { existsSync as existsSync5 } from "node:fs";
6378
+ import { existsSync as existsSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync5, unlinkSync } from "node:fs";
6379
6379
  import { dirname as dirname4, join as join6 } from "node:path";
6380
+ import { tmpdir } from "node:os";
6380
6381
  import { fileURLToPath } from "node:url";
6381
6382
  var proxyPort = null;
6382
6383
  var proxyProcess = null;
6383
6384
  var starting = null;
6385
+ var healthCheckFails = 0;
6386
+ var FIXED_PORT = 48372;
6387
+ var PID_FILE = join6(tmpdir(), "opencode-bun-proxy.pid");
6388
+ var MAX_HEALTH_FAILS = 2;
6389
+ var exitHandlerRegistered = false;
6390
+ function registerExitHandler() {
6391
+ if (exitHandlerRegistered) return;
6392
+ exitHandlerRegistered = true;
6393
+ const cleanup = () => {
6394
+ stopBunProxy();
6395
+ };
6396
+ process.on("exit", cleanup);
6397
+ process.on("SIGINT", cleanup);
6398
+ process.on("SIGTERM", cleanup);
6399
+ }
6384
6400
  function findProxyScript() {
6385
6401
  const dir = typeof __dirname !== "undefined" ? __dirname : dirname4(fileURLToPath(import.meta.url));
6386
6402
  for (const candidate of [
@@ -6392,45 +6408,78 @@ function findProxyScript() {
6392
6408
  }
6393
6409
  return null;
6394
6410
  }
6411
+ var _hasBun = null;
6395
6412
  function hasBun() {
6413
+ if (_hasBun !== null) return _hasBun;
6396
6414
  try {
6397
6415
  execFileSync("which", ["bun"], { stdio: "ignore" });
6398
- return true;
6416
+ _hasBun = true;
6417
+ } catch {
6418
+ _hasBun = false;
6419
+ }
6420
+ return _hasBun;
6421
+ }
6422
+ function killStaleProxy() {
6423
+ try {
6424
+ const raw = readFileSync6(PID_FILE, "utf-8").trim();
6425
+ const pid = parseInt(raw, 10);
6426
+ if (pid > 0) {
6427
+ try {
6428
+ process.kill(pid, "SIGTERM");
6429
+ } catch {
6430
+ }
6431
+ }
6432
+ unlinkSync(PID_FILE);
6433
+ } catch {
6434
+ }
6435
+ }
6436
+ async function isProxyHealthy(port) {
6437
+ try {
6438
+ const resp = await fetch(`http://127.0.0.1:${port}/__health`, {
6439
+ signal: AbortSignal.timeout(2e3)
6440
+ });
6441
+ return resp.ok;
6399
6442
  } catch {
6400
6443
  return false;
6401
6444
  }
6402
6445
  }
6403
- async function ensureBunProxy() {
6404
- if (process.env.VITEST || process.env.NODE_ENV === "test") return null;
6405
- if (proxyPort) return proxyPort;
6406
- if (starting) return starting;
6407
- starting = new Promise((resolve2) => {
6446
+ function spawnProxy() {
6447
+ return new Promise((resolve2) => {
6408
6448
  const script = findProxyScript();
6409
6449
  if (!script || !hasBun()) {
6410
6450
  resolve2(null);
6411
- starting = null;
6412
6451
  return;
6413
6452
  }
6453
+ killStaleProxy();
6414
6454
  try {
6415
- const child = spawn("bun", ["run", script, "0"], {
6416
- stdio: ["ignore", "pipe", "ignore"],
6455
+ const child = spawn("bun", ["run", script, String(FIXED_PORT)], {
6456
+ stdio: ["ignore", "pipe", "pipe"],
6417
6457
  detached: false
6418
6458
  });
6419
6459
  proxyProcess = child;
6460
+ registerExitHandler();
6420
6461
  let done = false;
6462
+ const finish = (port) => {
6463
+ if (done) return;
6464
+ done = true;
6465
+ if (port && child.pid) {
6466
+ try {
6467
+ writeFileSync5(PID_FILE, String(child.pid));
6468
+ } catch {
6469
+ }
6470
+ }
6471
+ resolve2(port);
6472
+ };
6421
6473
  child.stdout?.on("data", (chunk) => {
6422
6474
  const m = chunk.toString().match(/BUN_PROXY_PORT=(\d+)/);
6423
- if (m && !done) {
6424
- done = true;
6475
+ if (m) {
6425
6476
  proxyPort = parseInt(m[1], 10);
6426
- resolve2(proxyPort);
6477
+ healthCheckFails = 0;
6478
+ finish(proxyPort);
6427
6479
  }
6428
6480
  });
6429
6481
  child.on("error", () => {
6430
- if (!done) {
6431
- done = true;
6432
- resolve2(null);
6433
- }
6482
+ finish(null);
6434
6483
  proxyPort = null;
6435
6484
  proxyProcess = null;
6436
6485
  starting = null;
@@ -6439,23 +6488,48 @@ async function ensureBunProxy() {
6439
6488
  proxyPort = null;
6440
6489
  proxyProcess = null;
6441
6490
  starting = null;
6442
- if (!done) {
6443
- done = true;
6444
- resolve2(null);
6445
- }
6491
+ finish(null);
6446
6492
  });
6447
- setTimeout(() => {
6448
- if (!done) {
6449
- done = true;
6450
- resolve2(null);
6451
- }
6452
- }, 5e3);
6493
+ setTimeout(() => finish(null), 5e3);
6453
6494
  } catch {
6454
6495
  resolve2(null);
6455
- starting = null;
6456
6496
  }
6457
6497
  });
6458
- return starting;
6498
+ }
6499
+ async function ensureBunProxy() {
6500
+ if (process.env.VITEST || process.env.NODE_ENV === "test") return null;
6501
+ if (proxyPort && proxyProcess && !proxyProcess.killed) {
6502
+ return proxyPort;
6503
+ }
6504
+ if (!proxyPort && await isProxyHealthy(FIXED_PORT)) {
6505
+ proxyPort = FIXED_PORT;
6506
+ console.error("[bun-fetch] Reusing existing Bun proxy on port", FIXED_PORT);
6507
+ return proxyPort;
6508
+ }
6509
+ if (proxyPort && (!proxyProcess || proxyProcess.killed)) {
6510
+ proxyPort = null;
6511
+ proxyProcess = null;
6512
+ starting = null;
6513
+ }
6514
+ if (starting) return starting;
6515
+ starting = spawnProxy();
6516
+ const port = await starting;
6517
+ starting = null;
6518
+ if (port) console.error("[bun-fetch] Bun proxy started on port", port);
6519
+ else console.error("[bun-fetch] Failed to start Bun proxy, falling back to Node.js fetch");
6520
+ return port;
6521
+ }
6522
+ function stopBunProxy() {
6523
+ if (proxyProcess) {
6524
+ try {
6525
+ proxyProcess.kill();
6526
+ } catch {
6527
+ }
6528
+ proxyProcess = null;
6529
+ }
6530
+ proxyPort = null;
6531
+ starting = null;
6532
+ killStaleProxy();
6459
6533
  }
6460
6534
  async function fetchViaBun(input, init) {
6461
6535
  const port = await ensureBunProxy();
@@ -6463,11 +6537,36 @@ async function fetchViaBun(input, init) {
6463
6537
  if (!port) return fetch(input, init);
6464
6538
  const headers = new Headers(init.headers);
6465
6539
  headers.set("x-proxy-url", url);
6466
- return fetch(`http://127.0.0.1:${port}/`, {
6467
- method: init.method || "POST",
6468
- headers,
6469
- body: init.body
6470
- });
6540
+ try {
6541
+ const resp = await fetch(`http://127.0.0.1:${port}/`, {
6542
+ method: init.method || "POST",
6543
+ headers,
6544
+ body: init.body
6545
+ });
6546
+ if (resp.status === 502) {
6547
+ const errText = await resp.text();
6548
+ throw new Error(`Bun proxy upstream error: ${errText}`);
6549
+ }
6550
+ healthCheckFails = 0;
6551
+ return resp;
6552
+ } catch (err) {
6553
+ healthCheckFails++;
6554
+ if (healthCheckFails >= MAX_HEALTH_FAILS) {
6555
+ stopBunProxy();
6556
+ const newPort = await ensureBunProxy();
6557
+ if (newPort) {
6558
+ healthCheckFails = 0;
6559
+ const retryHeaders = new Headers(init.headers);
6560
+ retryHeaders.set("x-proxy-url", url);
6561
+ return fetch(`http://127.0.0.1:${newPort}/`, {
6562
+ method: init.method || "POST",
6563
+ headers: retryHeaders,
6564
+ body: init.body
6565
+ });
6566
+ }
6567
+ }
6568
+ throw err;
6569
+ }
6471
6570
  }
6472
6571
 
6473
6572
  // src/index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vacbo/opencode-anthropic-fix",
3
- "version": "0.0.43",
3
+ "version": "0.0.44",
4
4
  "main": "./dist/opencode-anthropic-auth-plugin.js",
5
5
  "bin": {
6
6
  "opencode-anthropic-auth": "./dist/opencode-anthropic-auth-cli.mjs",
package/src/bun-fetch.ts CHANGED
@@ -1,15 +1,33 @@
1
1
  // ---------------------------------------------------------------------------
2
- // Bun TLS proxy manager — spawns a Bun subprocess for BoringSSL TLS.
2
+ // Bun TLS proxy manager — spawns a single Bun subprocess for BoringSSL TLS.
3
+ // Hardened: health checks, auto-restart, single-instance guarantee.
3
4
  // ---------------------------------------------------------------------------
4
5
 
5
6
  import { execFileSync, spawn } from "node:child_process";
6
- import { existsSync } from "node:fs";
7
+ import { existsSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
7
8
  import { dirname, join } from "node:path";
9
+ import { tmpdir } from "node:os";
8
10
  import { fileURLToPath } from "node:url";
9
11
 
10
12
  let proxyPort: number | null = null;
11
13
  let proxyProcess: ReturnType<typeof spawn> | null = null;
12
14
  let starting: Promise<number | null> | null = null;
15
+ let healthCheckFails = 0;
16
+
17
+ const FIXED_PORT = 48372;
18
+ const PID_FILE = join(tmpdir(), "opencode-bun-proxy.pid");
19
+ const MAX_HEALTH_FAILS = 2;
20
+
21
+ // Kill proxy when parent process exits
22
+ let exitHandlerRegistered = false;
23
+ function registerExitHandler(): void {
24
+ if (exitHandlerRegistered) return;
25
+ exitHandlerRegistered = true;
26
+ const cleanup = () => { stopBunProxy(); };
27
+ process.on("exit", cleanup);
28
+ process.on("SIGINT", cleanup);
29
+ process.on("SIGTERM", cleanup);
30
+ }
13
31
 
14
32
  function findProxyScript(): string | null {
15
33
  const dir = typeof __dirname !== "undefined" ? __dirname : dirname(fileURLToPath(import.meta.url));
@@ -23,75 +41,147 @@ function findProxyScript(): string | null {
23
41
  return null;
24
42
  }
25
43
 
44
+ let _hasBun: boolean | null = null;
26
45
  function hasBun(): boolean {
46
+ if (_hasBun !== null) return _hasBun;
27
47
  try {
28
48
  execFileSync("which", ["bun"], { stdio: "ignore" });
29
- return true;
49
+ _hasBun = true;
30
50
  } catch {
31
- return false;
51
+ _hasBun = false;
32
52
  }
53
+ return _hasBun;
33
54
  }
34
55
 
35
- export async function ensureBunProxy(): Promise<number | null> {
36
- // Skip proxy in test environments
37
- if (process.env.VITEST || process.env.NODE_ENV === "test") return null;
38
- if (proxyPort) return proxyPort;
39
- if (starting) return starting;
56
+ function killStaleProxy(): void {
57
+ try {
58
+ const raw = readFileSync(PID_FILE, "utf-8").trim();
59
+ const pid = parseInt(raw, 10);
60
+ if (pid > 0) {
61
+ try { process.kill(pid, "SIGTERM"); } catch { /* already dead */ }
62
+ }
63
+ unlinkSync(PID_FILE);
64
+ } catch {
65
+ // No PID file or already cleaned
66
+ }
67
+ }
40
68
 
41
- starting = new Promise<number | null>((resolve) => {
69
+ async function isProxyHealthy(port: number): Promise<boolean> {
70
+ try {
71
+ const resp = await fetch(`http://127.0.0.1:${port}/__health`, {
72
+ signal: AbortSignal.timeout(2000),
73
+ });
74
+ return resp.ok;
75
+ } catch {
76
+ return false;
77
+ }
78
+ }
79
+
80
+ function spawnProxy(): Promise<number | null> {
81
+ return new Promise<number | null>((resolve) => {
42
82
  const script = findProxyScript();
43
83
  if (!script || !hasBun()) {
44
84
  resolve(null);
45
- starting = null;
46
85
  return;
47
86
  }
48
87
 
88
+ // Kill any stale instance first
89
+ killStaleProxy();
90
+
49
91
  try {
50
- const child = spawn("bun", ["run", script, "0"], {
51
- stdio: ["ignore", "pipe", "ignore"],
92
+ const child = spawn("bun", ["run", script, String(FIXED_PORT)], {
93
+ stdio: ["ignore", "pipe", "pipe"],
52
94
  detached: false,
53
95
  });
54
96
  proxyProcess = child;
97
+ registerExitHandler();
55
98
 
56
99
  let done = false;
100
+ const finish = (port: number | null) => {
101
+ if (done) return;
102
+ done = true;
103
+ if (port && child.pid) {
104
+ try { writeFileSync(PID_FILE, String(child.pid)); } catch { /* ok */ }
105
+ }
106
+ resolve(port);
107
+ };
108
+
57
109
  child.stdout?.on("data", (chunk: Buffer) => {
58
110
  const m = chunk.toString().match(/BUN_PROXY_PORT=(\d+)/);
59
- if (m && !done) {
60
- done = true;
111
+ if (m) {
61
112
  proxyPort = parseInt(m[1], 10);
62
- resolve(proxyPort);
113
+ healthCheckFails = 0;
114
+ finish(proxyPort);
63
115
  }
64
116
  });
117
+
65
118
  child.on("error", () => {
66
- if (!done) { done = true; resolve(null); }
67
- proxyPort = null; proxyProcess = null; starting = null;
119
+ finish(null);
120
+ proxyPort = null;
121
+ proxyProcess = null;
122
+ starting = null;
68
123
  });
124
+
69
125
  child.on("exit", () => {
70
- proxyPort = null; proxyProcess = null; starting = null;
71
- if (!done) { done = true; resolve(null); }
126
+ proxyPort = null;
127
+ proxyProcess = null;
128
+ starting = null;
129
+ finish(null);
72
130
  });
73
- setTimeout(() => {
74
- if (!done) { done = true; resolve(null); }
75
- }, 5000);
131
+
132
+ // Timeout
133
+ setTimeout(() => finish(null), 5000);
76
134
  } catch {
77
135
  resolve(null);
78
- starting = null;
79
136
  }
80
137
  });
138
+ }
139
+
140
+ export async function ensureBunProxy(): Promise<number | null> {
141
+ if (process.env.VITEST || process.env.NODE_ENV === "test") return null;
142
+
143
+ // Fast path: proxy already running and healthy
144
+ if (proxyPort && proxyProcess && !proxyProcess.killed) {
145
+ return proxyPort;
146
+ }
147
+
148
+ // Check if a proxy is already running on the fixed port (from previous session)
149
+ if (!proxyPort && await isProxyHealthy(FIXED_PORT)) {
150
+ proxyPort = FIXED_PORT;
151
+ console.error("[bun-fetch] Reusing existing Bun proxy on port", FIXED_PORT);
152
+ return proxyPort;
153
+ }
154
+
155
+ // Restart if previous instance died
156
+ if (proxyPort && (!proxyProcess || proxyProcess.killed)) {
157
+ proxyPort = null;
158
+ proxyProcess = null;
159
+ starting = null;
160
+ }
81
161
 
82
- return starting;
162
+ if (starting) return starting;
163
+
164
+ starting = spawnProxy();
165
+ const port = await starting;
166
+ starting = null;
167
+ if (port) console.error("[bun-fetch] Bun proxy started on port", port);
168
+ else console.error("[bun-fetch] Failed to start Bun proxy, falling back to Node.js fetch");
169
+ return port;
83
170
  }
84
171
 
85
172
  export function stopBunProxy(): void {
86
173
  if (proxyProcess) {
87
- try { proxyProcess.kill(); } catch { /* already dead */ }
88
- proxyProcess = null; proxyPort = null; starting = null;
174
+ try { proxyProcess.kill(); } catch { /* */ }
175
+ proxyProcess = null;
89
176
  }
177
+ proxyPort = null;
178
+ starting = null;
179
+ killStaleProxy();
90
180
  }
91
181
 
92
182
  /**
93
183
  * Fetch via Bun proxy for BoringSSL TLS fingerprint.
94
- * Falls back to native Node fetch if Bun is unavailable.
184
+ * Auto-restarts proxy on failure. Falls back to native fetch only if Bun is unavailable.
95
185
  */
96
186
  export async function fetchViaBun(
97
187
  input: string | URL | Request,
@@ -105,9 +195,41 @@ export async function fetchViaBun(
105
195
  const headers = new Headers(init.headers);
106
196
  headers.set("x-proxy-url", url);
107
197
 
108
- return fetch(`http://127.0.0.1:${port}/`, {
109
- method: init.method || "POST",
110
- headers,
111
- body: init.body,
112
- });
198
+ try {
199
+ const resp = await fetch(`http://127.0.0.1:${port}/`, {
200
+ method: init.method || "POST",
201
+ headers,
202
+ body: init.body,
203
+ });
204
+
205
+ // Proxy returned a 502 — Bun proxy couldn't reach Anthropic
206
+ if (resp.status === 502) {
207
+ const errText = await resp.text();
208
+ throw new Error(`Bun proxy upstream error: ${errText}`);
209
+ }
210
+
211
+ healthCheckFails = 0;
212
+ return resp;
213
+ } catch (err) {
214
+ healthCheckFails++;
215
+
216
+ // If proxy seems dead, restart it and retry once
217
+ if (healthCheckFails >= MAX_HEALTH_FAILS) {
218
+ stopBunProxy();
219
+ const newPort = await ensureBunProxy();
220
+ if (newPort) {
221
+ healthCheckFails = 0;
222
+ const retryHeaders = new Headers(init.headers);
223
+ retryHeaders.set("x-proxy-url", url);
224
+ return fetch(`http://127.0.0.1:${newPort}/`, {
225
+ method: init.method || "POST",
226
+ headers: retryHeaders,
227
+ body: init.body,
228
+ });
229
+ }
230
+ }
231
+
232
+ // Final fallback: native fetch (will use Node TLS — not ideal but better than failing)
233
+ throw err;
234
+ }
113
235
  }
package/src/bun-proxy.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  // Standalone Bun TLS proxy — run with: bun dist/bun-proxy.mjs [port]
2
2
  // Forwards requests using Bun's native fetch (BoringSSL TLS fingerprint).
3
3
 
4
- const PORT = parseInt(process.argv[2] || "0", 10);
4
+ const PORT = parseInt(process.argv[2] || "48372", 10);
5
5
 
6
6
  const server = Bun.serve({
7
7
  port: PORT,