@vacbo/opencode-anthropic-fix 0.0.44 → 0.1.1

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.
Files changed (61) hide show
  1. package/README.md +19 -0
  2. package/dist/bun-proxy.mjs +282 -55
  3. package/dist/opencode-anthropic-auth-cli.mjs +194 -55
  4. package/dist/opencode-anthropic-auth-plugin.js +1816 -594
  5. package/package.json +1 -1
  6. package/src/__tests__/billing-edge-cases.test.ts +84 -0
  7. package/src/__tests__/bun-proxy.parallel.test.ts +460 -0
  8. package/src/__tests__/debug-gating.test.ts +76 -0
  9. package/src/__tests__/decomposition-smoke.test.ts +92 -0
  10. package/src/__tests__/fingerprint-regression.test.ts +1 -1
  11. package/src/__tests__/helpers/conversation-history.smoke.test.ts +338 -0
  12. package/src/__tests__/helpers/conversation-history.ts +376 -0
  13. package/src/__tests__/helpers/deferred.smoke.test.ts +161 -0
  14. package/src/__tests__/helpers/deferred.ts +122 -0
  15. package/src/__tests__/helpers/in-memory-storage.smoke.test.ts +166 -0
  16. package/src/__tests__/helpers/in-memory-storage.ts +152 -0
  17. package/src/__tests__/helpers/mock-bun-proxy.smoke.test.ts +92 -0
  18. package/src/__tests__/helpers/mock-bun-proxy.ts +229 -0
  19. package/src/__tests__/helpers/plugin-fetch-harness.smoke.test.ts +337 -0
  20. package/src/__tests__/helpers/plugin-fetch-harness.ts +401 -0
  21. package/src/__tests__/helpers/sse.smoke.test.ts +243 -0
  22. package/src/__tests__/helpers/sse.ts +288 -0
  23. package/src/__tests__/index.parallel.test.ts +711 -0
  24. package/src/__tests__/sanitization-regex.test.ts +65 -0
  25. package/src/__tests__/state-bounds.test.ts +110 -0
  26. package/src/account-identity.test.ts +213 -0
  27. package/src/account-identity.ts +108 -0
  28. package/src/accounts.dedup.test.ts +696 -0
  29. package/src/accounts.test.ts +2 -1
  30. package/src/accounts.ts +485 -191
  31. package/src/bun-fetch.test.ts +379 -0
  32. package/src/bun-fetch.ts +447 -174
  33. package/src/bun-proxy.ts +289 -57
  34. package/src/circuit-breaker.test.ts +274 -0
  35. package/src/circuit-breaker.ts +235 -0
  36. package/src/cli.test.ts +1 -0
  37. package/src/cli.ts +37 -18
  38. package/src/commands/router.ts +25 -5
  39. package/src/env.ts +1 -0
  40. package/src/headers/billing.ts +31 -13
  41. package/src/index.ts +224 -247
  42. package/src/oauth.ts +7 -1
  43. package/src/parent-pid-watcher.test.ts +219 -0
  44. package/src/parent-pid-watcher.ts +99 -0
  45. package/src/plugin-helpers.ts +112 -0
  46. package/src/refresh-helpers.ts +169 -0
  47. package/src/refresh-lock.test.ts +36 -9
  48. package/src/refresh-lock.ts +2 -2
  49. package/src/request/body.history.test.ts +398 -0
  50. package/src/request/body.ts +200 -13
  51. package/src/request/metadata.ts +6 -2
  52. package/src/response/index.ts +1 -1
  53. package/src/response/mcp.ts +60 -31
  54. package/src/response/streaming.test.ts +382 -0
  55. package/src/response/streaming.ts +403 -76
  56. package/src/storage.test.ts +127 -104
  57. package/src/storage.ts +152 -62
  58. package/src/system-prompt/builder.ts +33 -3
  59. package/src/system-prompt/sanitize.ts +12 -2
  60. package/src/token-refresh.test.ts +84 -1
  61. package/src/token-refresh.ts +14 -8
@@ -0,0 +1,379 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from "vitest";
2
+
3
+ import { createDeferred, createDeferredQueue } from "./__tests__/helpers/deferred.js";
4
+ import { createMockBunProxy } from "./__tests__/helpers/mock-bun-proxy.js";
5
+
6
+ let execFileSyncMock: Mock;
7
+ let spawnMock: Mock;
8
+ let existsSyncMock: Mock;
9
+ let readFileSyncMock: Mock;
10
+ let statSyncMock: Mock;
11
+ let unlinkSyncMock: Mock;
12
+ let writeFileSyncMock: Mock;
13
+
14
+ const originalNodeEnv = process.env.NODE_ENV;
15
+ const originalVitest = process.env.VITEST;
16
+
17
+ vi.mock("node:child_process", () => ({
18
+ execFileSync: (...args: unknown[]) => execFileSyncMock(...args),
19
+ spawn: (...args: unknown[]) => spawnMock(...args),
20
+ }));
21
+
22
+ vi.mock("node:fs", async () => {
23
+ const actual = await vi.importActual<typeof import("node:fs")>("node:fs");
24
+
25
+ return {
26
+ ...actual,
27
+ existsSync: (...args: unknown[]) => existsSyncMock(...args),
28
+ readFileSync: (...args: unknown[]) => readFileSyncMock(...args),
29
+ statSync: (...args: unknown[]) => statSyncMock(...args),
30
+ unlinkSync: (...args: unknown[]) => unlinkSyncMock(...args),
31
+ writeFileSync: (...args: unknown[]) => writeFileSyncMock(...args),
32
+ };
33
+ });
34
+
35
+ type BunFetchModule = Awaited<typeof import("./bun-fetch.js")> & {
36
+ createBunFetch?: (options?: { debug?: boolean; onProxyStatus?: (status: unknown) => void }) => {
37
+ fetch: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
38
+ shutdown: () => Promise<void>;
39
+ getStatus: () => unknown;
40
+ };
41
+ };
42
+
43
+ async function readBunFetchSource(): Promise<string> {
44
+ const fs = await vi.importActual<typeof import("node:fs")>("node:fs");
45
+ return fs.readFileSync(new URL("./bun-fetch.ts", import.meta.url), "utf8");
46
+ }
47
+
48
+ async function loadBunFetchModule(): Promise<BunFetchModule> {
49
+ return import("./bun-fetch.js") as Promise<BunFetchModule>;
50
+ }
51
+
52
+ function installMockFetch(implementation?: Parameters<typeof vi.fn>[0]): ReturnType<typeof vi.fn> {
53
+ const fetchMock = vi.fn(implementation ?? (async () => new Response("native-fallback", { status: 200 })));
54
+ vi.stubGlobal("fetch", fetchMock);
55
+ return fetchMock;
56
+ }
57
+
58
+ function getCreateBunFetch(moduleNs: BunFetchModule): NonNullable<BunFetchModule["createBunFetch"]> {
59
+ const createBunFetch = moduleNs.createBunFetch;
60
+
61
+ expect(createBunFetch, "T20 must export createBunFetch() for per-instance lifecycle ownership").toBeTypeOf(
62
+ "function",
63
+ );
64
+
65
+ if (typeof createBunFetch !== "function") {
66
+ throw new TypeError("createBunFetch is missing");
67
+ }
68
+
69
+ return createBunFetch;
70
+ }
71
+
72
+ beforeEach(async () => {
73
+ const fs = await vi.importActual<typeof import("node:fs")>("node:fs");
74
+
75
+ vi.resetModules();
76
+ vi.useRealTimers();
77
+ vi.unstubAllGlobals();
78
+
79
+ execFileSyncMock = vi.fn().mockReturnValue(undefined);
80
+ spawnMock = vi.fn();
81
+ existsSyncMock = vi.fn((filePath: unknown) => {
82
+ if (typeof filePath === "string" && /bun-proxy\.(mjs|ts)$/.test(filePath)) {
83
+ return true;
84
+ }
85
+
86
+ return fs.existsSync(filePath as Parameters<typeof fs.existsSync>[0]);
87
+ });
88
+ readFileSyncMock = vi.fn((...args: unknown[]) =>
89
+ fs.readFileSync(args[0] as Parameters<typeof fs.readFileSync>[0], args[1] as Parameters<typeof fs.readFileSync>[1]),
90
+ );
91
+ statSyncMock = vi.fn((...args: unknown[]) => fs.statSync(args[0] as Parameters<typeof fs.statSync>[0]));
92
+ unlinkSyncMock = vi.fn();
93
+ writeFileSyncMock = vi.fn();
94
+
95
+ process.env.NODE_ENV = "development";
96
+ delete process.env.VITEST;
97
+ });
98
+
99
+ afterEach(() => {
100
+ vi.useRealTimers();
101
+ vi.unstubAllGlobals();
102
+ vi.restoreAllMocks();
103
+
104
+ if (originalNodeEnv === undefined) {
105
+ delete process.env.NODE_ENV;
106
+ } else {
107
+ process.env.NODE_ENV = originalNodeEnv;
108
+ }
109
+
110
+ if (originalVitest === undefined) {
111
+ delete process.env.VITEST;
112
+ } else {
113
+ process.env.VITEST = originalVitest;
114
+ }
115
+ });
116
+
117
+ describe("bun-fetch source guardrails (RED until T20/T21)", () => {
118
+ it("removes module-level proxy/process/counter state in favor of instance-owned closures", async () => {
119
+ const source = await readBunFetchSource();
120
+
121
+ expect(source).not.toMatch(/^let (proxyPort|proxyProcess|starting|healthCheckFails|exitHandlerRegistered)\b/m);
122
+ });
123
+
124
+ it("does not hard-code a fixed port, pid file, or MAX_HEALTH_FAILS singleton constants", async () => {
125
+ const source = await readBunFetchSource();
126
+
127
+ expect(source).not.toMatch(/\b(FIXED_PORT|PID_FILE|MAX_HEALTH_FAILS|healthCheckFails)\b/);
128
+ expect(source).not.toContain("48372");
129
+ });
130
+
131
+ it("spawns Bun with --parent-pid and without passing a fixed port argument", async () => {
132
+ const source = await readBunFetchSource();
133
+
134
+ expect(source).toContain("--parent-pid");
135
+ expect(source).not.toMatch(/String\(FIXED_PORT\)|48372/);
136
+ });
137
+
138
+ it("parses proxy banners with a line-buffered stdout reader", async () => {
139
+ const source = await readBunFetchSource();
140
+
141
+ expect(source).toMatch(/readline\.createInterface|createInterface\(/);
142
+ });
143
+
144
+ it("never calls stopBunProxy from fetchViaBun catch blocks", async () => {
145
+ const source = await readBunFetchSource();
146
+
147
+ expect(source).not.toMatch(/catch\s*\([^)]*\)\s*\{[\s\S]*stopBunProxy\(/);
148
+ });
149
+
150
+ it("uses a circuit breaker instead of a shared healthCheckFails counter", async () => {
151
+ const source = await readBunFetchSource();
152
+
153
+ expect(source).toMatch(/createCircuitBreaker|CircuitBreaker/);
154
+ expect(source).not.toContain("healthCheckFails");
155
+ });
156
+
157
+ it("installs no global process.on handlers or process.exit calls", async () => {
158
+ const source = await readBunFetchSource();
159
+
160
+ expect(source).not.toMatch(/process\.on\s*\(/);
161
+ expect(source).not.toMatch(/process\.exit\s*\(/);
162
+ });
163
+ });
164
+
165
+ describe("createBunFetch runtime lifecycle (RED until T20)", () => {
166
+ it("exports a createBunFetch factory with fetch/shutdown/getStatus instance API", async () => {
167
+ const proxy = createMockBunProxy();
168
+ spawnMock.mockImplementation(proxy.mockSpawn);
169
+ installMockFetch();
170
+
171
+ const moduleNs = await loadBunFetchModule();
172
+ const createBunFetch = getCreateBunFetch(moduleNs);
173
+ const instance = createBunFetch({ debug: false });
174
+
175
+ expect(instance).toMatchObject({
176
+ fetch: expect.any(Function),
177
+ shutdown: expect.any(Function),
178
+ getStatus: expect.any(Function),
179
+ });
180
+ });
181
+
182
+ it("creates a new proxy per plugin instance instead of sharing module-level state", async () => {
183
+ const proxyA = createMockBunProxy();
184
+ const proxyB = createMockBunProxy();
185
+ spawnMock.mockImplementationOnce(proxyA.mockSpawn).mockImplementationOnce(proxyB.mockSpawn);
186
+ installMockFetch();
187
+
188
+ const moduleNs = await loadBunFetchModule();
189
+ const createBunFetch = getCreateBunFetch(moduleNs);
190
+ const instanceA = createBunFetch({ debug: false });
191
+ const instanceB = createBunFetch({ debug: false });
192
+
193
+ proxyA.simulateStdoutBanner(41001);
194
+ proxyB.simulateStdoutBanner(41002);
195
+
196
+ await Promise.all([
197
+ instanceA.fetch("https://example.com/a", { method: "POST", body: "a" }),
198
+ instanceB.fetch("https://example.com/b", { method: "POST", body: "b" }),
199
+ ]);
200
+
201
+ expect(spawnMock).toHaveBeenCalledTimes(2);
202
+ });
203
+
204
+ it("reuses the same proxy for sequential requests from a single instance", async () => {
205
+ const proxy = createMockBunProxy();
206
+ spawnMock.mockImplementation(proxy.mockSpawn);
207
+ installMockFetch(async () => proxy.child.forwardFetch("https://example.com/reused", { method: "POST" }));
208
+
209
+ const moduleNs = await loadBunFetchModule();
210
+ const createBunFetch = getCreateBunFetch(moduleNs);
211
+ const instance = createBunFetch({ debug: false });
212
+
213
+ proxy.simulateStdoutBanner(41011);
214
+
215
+ await instance.fetch("https://example.com/first", { method: "POST", body: "first" });
216
+ await instance.fetch("https://example.com/second", { method: "POST", body: "second" });
217
+
218
+ expect(spawnMock).toHaveBeenCalledTimes(1);
219
+ });
220
+
221
+ it("parses a split BUN_PROXY_PORT banner line-by-line instead of per-chunk", async () => {
222
+ vi.useFakeTimers();
223
+
224
+ const proxy = createMockBunProxy();
225
+ spawnMock.mockImplementation(proxy.mockSpawn);
226
+ installMockFetch();
227
+
228
+ const moduleNs = await loadBunFetchModule();
229
+ expect(moduleNs.ensureBunProxy).toBeTypeOf("function");
230
+
231
+ const startup = moduleNs.ensureBunProxy(false);
232
+ proxy.child.stdout.write("BUN_PROXY_");
233
+ proxy.child.stdout.write("PORT=43123\n");
234
+
235
+ await vi.advanceTimersByTimeAsync(5001);
236
+ await expect(startup).resolves.toBe(43123);
237
+ });
238
+
239
+ it("keeps 10 concurrent sibling requests on one shared proxy without interference", async () => {
240
+ const responses = createDeferredQueue<Response>();
241
+ const proxy = createMockBunProxy({
242
+ forwardToMockFetch: async () => responses.enqueue().promise,
243
+ });
244
+ spawnMock.mockImplementation(proxy.mockSpawn);
245
+ installMockFetch();
246
+
247
+ const moduleNs = await loadBunFetchModule();
248
+ const createBunFetch = getCreateBunFetch(moduleNs);
249
+ const instance = createBunFetch({ debug: false });
250
+
251
+ proxy.simulateStdoutBanner(41021);
252
+
253
+ const requests = Array.from({ length: 10 }, (_, index) =>
254
+ instance.fetch(`https://example.com/${index}`, {
255
+ method: "POST",
256
+ body: `body-${index}`,
257
+ }),
258
+ );
259
+
260
+ expect(spawnMock).toHaveBeenCalledTimes(1);
261
+
262
+ for (let index = 0; index < 10; index += 1) {
263
+ responses.resolveNext(new Response(`ok-${index}`, { status: 200 }));
264
+ }
265
+
266
+ await expect(Promise.all(requests)).resolves.toHaveLength(10);
267
+ });
268
+
269
+ it("does not kill sibling streams or the proxy when one concurrent request fails", async () => {
270
+ const slowSuccess = createDeferred<Response>();
271
+ const proxy = createMockBunProxy({
272
+ forwardToMockFetch: async (input) => {
273
+ if (String(input).includes("/fail")) {
274
+ throw new Error("upstream exploded");
275
+ }
276
+
277
+ return slowSuccess.promise;
278
+ },
279
+ });
280
+ spawnMock.mockImplementation(proxy.mockSpawn);
281
+ installMockFetch();
282
+
283
+ const moduleNs = await loadBunFetchModule();
284
+ const createBunFetch = getCreateBunFetch(moduleNs);
285
+ const instance = createBunFetch({ debug: false });
286
+
287
+ proxy.simulateStdoutBanner(41031);
288
+
289
+ const goodRequest = instance.fetch("https://example.com/stream-ok", {
290
+ method: "POST",
291
+ body: "ok",
292
+ });
293
+ const badRequest = instance.fetch("https://example.com/fail", {
294
+ method: "POST",
295
+ body: "fail",
296
+ });
297
+
298
+ slowSuccess.resolve(new Response("still-open", { status: 200 }));
299
+
300
+ await expect(goodRequest).resolves.toBeInstanceOf(Response);
301
+ await expect(badRequest).rejects.toThrow("upstream exploded");
302
+ expect(proxy.child.killSignals).toEqual([]);
303
+ });
304
+
305
+ it("falls back gracefully when Bun spawn fails without calling process.exit", async () => {
306
+ const nativeFetch = installMockFetch(async () => new Response("native", { status: 200 }));
307
+ spawnMock.mockImplementation(() => {
308
+ throw new Error("spawn failed");
309
+ });
310
+
311
+ const exitSpy = vi.spyOn(process, "exit").mockImplementation(((code?: string | number | null) => {
312
+ throw new Error(`unexpected process.exit(${code ?? ""})`);
313
+ }) as typeof process.exit);
314
+
315
+ const moduleNs = await loadBunFetchModule();
316
+ const createBunFetch = getCreateBunFetch(moduleNs);
317
+ const onProxyStatus = vi.fn();
318
+ const instance = createBunFetch({ debug: false, onProxyStatus });
319
+
320
+ const response = await instance.fetch("https://example.com/native", { method: "POST", body: "native" });
321
+
322
+ expect(await response.text()).toBe("native");
323
+ expect(nativeFetch).toHaveBeenCalledTimes(1);
324
+ expect(onProxyStatus).toHaveBeenCalled();
325
+ expect(exitSpy).not.toHaveBeenCalled();
326
+ });
327
+
328
+ it("keeps an old instance alive for in-flight work when a new hot-reload instance is created", async () => {
329
+ const firstResponse = createDeferred<Response>();
330
+ const proxyA = createMockBunProxy({ forwardToMockFetch: async () => firstResponse.promise });
331
+ const proxyB = createMockBunProxy({
332
+ forwardToMockFetch: async () => new Response("fresh-instance", { status: 200 }),
333
+ });
334
+ spawnMock.mockImplementationOnce(proxyA.mockSpawn).mockImplementationOnce(proxyB.mockSpawn);
335
+ installMockFetch();
336
+
337
+ const moduleNs = await loadBunFetchModule();
338
+ const createBunFetch = getCreateBunFetch(moduleNs);
339
+ const oldInstance = createBunFetch({ debug: false });
340
+ const oldRequest = oldInstance.fetch("https://example.com/old", { method: "POST", body: "old" });
341
+
342
+ proxyA.simulateStdoutBanner(41041);
343
+
344
+ const newInstance = createBunFetch({ debug: false });
345
+ proxyB.simulateStdoutBanner(41042);
346
+
347
+ const newRequest = newInstance.fetch("https://example.com/new", { method: "POST", body: "new" });
348
+
349
+ expect(proxyA.getInFlightCount()).toBe(1);
350
+ firstResponse.resolve(new Response("old-instance-still-streaming", { status: 200 }));
351
+
352
+ await expect(oldRequest).resolves.toBeInstanceOf(Response);
353
+ await expect(newRequest).resolves.toBeInstanceOf(Response);
354
+ });
355
+
356
+ it("cleans up the current child on shutdown without clearing state for an older child exit", async () => {
357
+ const proxyA = createMockBunProxy();
358
+ const proxyB = createMockBunProxy();
359
+ spawnMock.mockImplementationOnce(proxyA.mockSpawn).mockImplementationOnce(proxyB.mockSpawn);
360
+ installMockFetch(async () => new Response("ok", { status: 200 }));
361
+
362
+ const moduleNs = await loadBunFetchModule();
363
+ const createBunFetch = getCreateBunFetch(moduleNs);
364
+ const instanceA = createBunFetch({ debug: false });
365
+ const instanceB = createBunFetch({ debug: false });
366
+
367
+ proxyA.simulateStdoutBanner(41051);
368
+ proxyB.simulateStdoutBanner(41052);
369
+
370
+ await instanceA.fetch("https://example.com/a", { method: "POST", body: "a" });
371
+ await instanceB.fetch("https://example.com/b", { method: "POST", body: "b" });
372
+
373
+ proxyA.simulateExit(0, null);
374
+ await instanceB.shutdown();
375
+
376
+ expect(proxyB.child.killSignals).toContain("SIGTERM");
377
+ expect(proxyA.child.killSignals).toEqual([]);
378
+ });
379
+ });