@vacbo/opencode-anthropic-fix 0.1.7 → 0.1.9

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 (107) hide show
  1. package/README.md +88 -88
  2. package/dist/opencode-anthropic-auth-cli.mjs +804 -507
  3. package/dist/opencode-anthropic-auth-plugin.js +4751 -4109
  4. package/package.json +67 -59
  5. package/src/__tests__/billing-edge-cases.test.ts +59 -59
  6. package/src/__tests__/bun-proxy.parallel.test.ts +388 -382
  7. package/src/__tests__/cc-comparison.test.ts +87 -87
  8. package/src/__tests__/cc-credentials.test.ts +254 -250
  9. package/src/__tests__/cch-drift-checker.test.ts +51 -51
  10. package/src/__tests__/cch-native-style.test.ts +56 -56
  11. package/src/__tests__/debug-gating.test.ts +42 -42
  12. package/src/__tests__/decomposition-smoke.test.ts +68 -68
  13. package/src/__tests__/fingerprint-regression.test.ts +575 -566
  14. package/src/__tests__/helpers/conversation-history.smoke.test.ts +271 -271
  15. package/src/__tests__/helpers/conversation-history.ts +119 -119
  16. package/src/__tests__/helpers/deferred.smoke.test.ts +103 -103
  17. package/src/__tests__/helpers/deferred.ts +69 -69
  18. package/src/__tests__/helpers/in-memory-storage.smoke.test.ts +155 -155
  19. package/src/__tests__/helpers/in-memory-storage.ts +88 -88
  20. package/src/__tests__/helpers/mock-bun-proxy.smoke.test.ts +68 -68
  21. package/src/__tests__/helpers/mock-bun-proxy.ts +189 -189
  22. package/src/__tests__/helpers/plugin-fetch-harness.smoke.test.ts +273 -273
  23. package/src/__tests__/helpers/plugin-fetch-harness.ts +288 -288
  24. package/src/__tests__/helpers/sse.smoke.test.ts +236 -236
  25. package/src/__tests__/helpers/sse.ts +209 -209
  26. package/src/__tests__/index.parallel.test.ts +605 -595
  27. package/src/__tests__/sanitization-regex.test.ts +112 -112
  28. package/src/__tests__/state-bounds.test.ts +90 -90
  29. package/src/account-identity.test.ts +197 -192
  30. package/src/account-identity.ts +69 -67
  31. package/src/account-state.test.ts +86 -86
  32. package/src/account-state.ts +25 -25
  33. package/src/accounts/matching.test.ts +335 -0
  34. package/src/accounts/matching.ts +167 -0
  35. package/src/accounts/persistence.test.ts +345 -0
  36. package/src/accounts/persistence.ts +432 -0
  37. package/src/accounts/repair.test.ts +276 -0
  38. package/src/accounts/repair.ts +407 -0
  39. package/src/accounts.dedup.test.ts +621 -621
  40. package/src/accounts.test.ts +933 -929
  41. package/src/accounts.ts +633 -989
  42. package/src/backoff.test.ts +345 -345
  43. package/src/backoff.ts +219 -219
  44. package/src/betas.ts +124 -124
  45. package/src/bun-fetch.test.ts +345 -342
  46. package/src/bun-fetch.ts +424 -424
  47. package/src/bun-proxy.test.ts +25 -25
  48. package/src/bun-proxy.ts +209 -209
  49. package/src/cc-credentials.ts +111 -111
  50. package/src/circuit-breaker.test.ts +184 -184
  51. package/src/circuit-breaker.ts +169 -169
  52. package/src/cli/commands/auth.ts +963 -0
  53. package/src/cli/commands/config.ts +547 -0
  54. package/src/cli/formatting.test.ts +406 -0
  55. package/src/cli/formatting.ts +219 -0
  56. package/src/cli.ts +255 -2022
  57. package/src/commands/handlers/betas.ts +100 -0
  58. package/src/commands/handlers/config.ts +99 -0
  59. package/src/commands/handlers/files.ts +375 -0
  60. package/src/commands/oauth-flow.ts +181 -166
  61. package/src/commands/prompts.ts +61 -61
  62. package/src/commands/router.test.ts +421 -0
  63. package/src/commands/router.ts +143 -635
  64. package/src/config.test.ts +482 -482
  65. package/src/config.ts +412 -404
  66. package/src/constants.ts +48 -48
  67. package/src/drift/cch-constants.ts +95 -95
  68. package/src/env.ts +111 -105
  69. package/src/headers/billing.ts +33 -33
  70. package/src/headers/builder.ts +130 -130
  71. package/src/headers/cch.ts +75 -75
  72. package/src/headers/stainless.ts +25 -25
  73. package/src/headers/user-agent.ts +23 -23
  74. package/src/index.ts +436 -828
  75. package/src/models.ts +27 -27
  76. package/src/oauth.test.ts +102 -102
  77. package/src/oauth.ts +178 -178
  78. package/src/parent-pid-watcher.test.ts +148 -148
  79. package/src/parent-pid-watcher.ts +69 -69
  80. package/src/plugin-helpers.ts +82 -82
  81. package/src/refresh-helpers.ts +145 -139
  82. package/src/refresh-lock.test.ts +94 -94
  83. package/src/refresh-lock.ts +93 -93
  84. package/src/request/body.history.test.ts +579 -571
  85. package/src/request/body.ts +255 -255
  86. package/src/request/metadata.ts +65 -65
  87. package/src/request/retry.test.ts +156 -156
  88. package/src/request/retry.ts +67 -67
  89. package/src/request/url.ts +21 -21
  90. package/src/request-orchestration-helpers.ts +648 -0
  91. package/src/response/index.ts +5 -5
  92. package/src/response/mcp.ts +58 -58
  93. package/src/response/streaming.test.ts +313 -311
  94. package/src/response/streaming.ts +412 -410
  95. package/src/rotation.test.ts +304 -301
  96. package/src/rotation.ts +205 -205
  97. package/src/storage.test.ts +547 -547
  98. package/src/storage.ts +315 -291
  99. package/src/system-prompt/builder.ts +38 -38
  100. package/src/system-prompt/index.ts +5 -5
  101. package/src/system-prompt/normalize.ts +60 -60
  102. package/src/system-prompt/sanitize.ts +30 -30
  103. package/src/thinking.ts +21 -20
  104. package/src/token-refresh.test.ts +265 -265
  105. package/src/token-refresh.ts +219 -214
  106. package/src/types.ts +30 -30
  107. package/dist/bun-proxy.mjs +0 -291
@@ -9,119 +9,119 @@ const DEFAULT_LOCK_TIMEOUT_MS = 15_000;
9
9
  const DEFAULT_STALE_LOCK_MS = 90_000;
10
10
 
11
11
  vi.mock("./storage.js", () => ({
12
- getStoragePath: () => storagePath,
12
+ getStoragePath: () => storagePath,
13
13
  }));
14
14
 
15
15
  import { acquireRefreshLock, releaseRefreshLock } from "./refresh-lock.js";
16
16
 
17
17
  describe("refresh-lock", () => {
18
- beforeEach(async () => {
19
- await fs.rm(baseDir, { recursive: true, force: true });
20
- await fs.mkdir(baseDir, { recursive: true });
21
- });
22
-
23
- afterEach(async () => {
24
- await fs.rm(baseDir, { recursive: true, force: true });
25
- });
26
-
27
- it("does not release lock with mismatched owner", async () => {
28
- const lock = await acquireRefreshLock("acc-1");
29
- expect(lock.acquired).toBe(true);
30
- expect(lock.lockPath).toBeTruthy();
31
- const lockPath = lock.lockPath!;
32
-
33
- await releaseRefreshLock({ lockPath, owner: "wrong-owner" });
34
-
35
- await expect(fs.stat(lockPath)).resolves.toBeTruthy();
36
-
37
- await releaseRefreshLock(lock);
38
- await expect(fs.stat(lockPath)).rejects.toMatchObject({
39
- code: "ENOENT",
18
+ beforeEach(async () => {
19
+ await fs.rm(baseDir, { recursive: true, force: true });
20
+ await fs.mkdir(baseDir, { recursive: true });
40
21
  });
41
- });
42
22
 
43
- it("acquires a new lock after stale lock timeout", async () => {
44
- const first = await acquireRefreshLock("acc-2", {
45
- timeoutMs: 50,
46
- staleMs: 10_000,
23
+ afterEach(async () => {
24
+ await fs.rm(baseDir, { recursive: true, force: true });
47
25
  });
48
- expect(first.acquired).toBe(true);
49
- const firstLockPath = first.lockPath!;
50
-
51
- const old = Date.now() / 1000 - 120;
52
- await fs.utimes(firstLockPath, old, old);
53
26
 
54
- const second = await acquireRefreshLock("acc-2", {
55
- timeoutMs: 200,
56
- backoffMs: 5,
57
- staleMs: 20,
58
- });
59
- expect(second.acquired).toBe(true);
60
- expect(second.owner).not.toBe(first.owner);
27
+ it("does not release lock with mismatched owner", async () => {
28
+ const lock = await acquireRefreshLock("acc-1");
29
+ expect(lock.acquired).toBe(true);
30
+ expect(lock.lockPath).toBeTruthy();
31
+ const lockPath = lock.lockPath!;
61
32
 
62
- await releaseRefreshLock(second);
63
- });
33
+ await releaseRefreshLock({ lockPath, owner: "wrong-owner" });
64
34
 
65
- it("returns not acquired when lock remains busy", async () => {
66
- const first = await acquireRefreshLock("acc-3", { timeoutMs: 50 });
67
- expect(first.acquired).toBe(true);
35
+ await expect(fs.stat(lockPath)).resolves.toBeTruthy();
68
36
 
69
- const second = await acquireRefreshLock("acc-3", {
70
- timeoutMs: 30,
71
- backoffMs: 5,
72
- staleMs: DEFAULT_STALE_LOCK_MS,
37
+ await releaseRefreshLock(lock);
38
+ await expect(fs.stat(lockPath)).rejects.toMatchObject({
39
+ code: "ENOENT",
40
+ });
73
41
  });
74
- expect(second.acquired).toBe(false);
75
-
76
- await releaseRefreshLock(first);
77
- });
78
42
 
79
- it("stale reaper does NOT steal a lock held for 60s", async () => {
80
- const first = await acquireRefreshLock("acc-stable-refresh", {
81
- timeoutMs: DEFAULT_LOCK_TIMEOUT_MS,
43
+ it("acquires a new lock after stale lock timeout", async () => {
44
+ const first = await acquireRefreshLock("acc-2", {
45
+ timeoutMs: 50,
46
+ staleMs: 10_000,
47
+ });
48
+ expect(first.acquired).toBe(true);
49
+ const firstLockPath = first.lockPath!;
50
+
51
+ const old = Date.now() / 1000 - 120;
52
+ await fs.utimes(firstLockPath, old, old);
53
+
54
+ const second = await acquireRefreshLock("acc-2", {
55
+ timeoutMs: 200,
56
+ backoffMs: 5,
57
+ staleMs: 20,
58
+ });
59
+ expect(second.acquired).toBe(true);
60
+ expect(second.owner).not.toBe(first.owner);
61
+
62
+ await releaseRefreshLock(second);
82
63
  });
83
- expect(first.acquired).toBe(true);
84
- expect(first.lockPath).toBeTruthy();
85
- const firstLockPath = first.lockPath!;
86
64
 
87
- const sixtySecondsAgo = Date.now() / 1000 - 60;
88
- await fs.utimes(firstLockPath, sixtySecondsAgo, sixtySecondsAgo);
65
+ it("returns not acquired when lock remains busy", async () => {
66
+ const first = await acquireRefreshLock("acc-3", { timeoutMs: 50 });
67
+ expect(first.acquired).toBe(true);
89
68
 
90
- const second = await acquireRefreshLock("acc-stable-refresh", {
91
- timeoutMs: 30,
92
- backoffMs: 5,
93
- staleMs: DEFAULT_STALE_LOCK_MS,
94
- });
95
- expect(second.acquired).toBe(false);
96
-
97
- await expect(fs.stat(firstLockPath)).resolves.toBeTruthy();
98
- await releaseRefreshLock(first);
99
- });
100
-
101
- it("does not release when inode changed even if owner matches", async () => {
102
- const first = await acquireRefreshLock("acc-4");
103
- expect(first.acquired).toBe(true);
104
- const firstLockPath = first.lockPath!;
105
- const originalLockInode = first.lockInode;
106
- expect(originalLockInode).not.toBeNull();
107
-
108
- // Keep the same owner text but pass a deliberately mismatched inode. This
109
- // tests the safety check directly without relying on filesystem-specific
110
- // inode allocation behavior, which can aggressively recycle inode numbers
111
- // on Linux CI runners.
112
- await fs.writeFile(firstLockPath, JSON.stringify({ owner: first.owner, createdAt: Date.now() }), {
113
- encoding: "utf-8",
114
- mode: 0o600,
115
- });
69
+ const second = await acquireRefreshLock("acc-3", {
70
+ timeoutMs: 30,
71
+ backoffMs: 5,
72
+ staleMs: DEFAULT_STALE_LOCK_MS,
73
+ });
74
+ expect(second.acquired).toBe(false);
116
75
 
117
- await releaseRefreshLock({
118
- lockPath: firstLockPath,
119
- owner: first.owner,
120
- lockInode: originalLockInode! + 1n,
76
+ await releaseRefreshLock(first);
121
77
  });
122
78
 
123
- await expect(fs.stat(firstLockPath)).resolves.toBeTruthy();
79
+ it("stale reaper does NOT steal a lock held for 60s", async () => {
80
+ const first = await acquireRefreshLock("acc-stable-refresh", {
81
+ timeoutMs: DEFAULT_LOCK_TIMEOUT_MS,
82
+ });
83
+ expect(first.acquired).toBe(true);
84
+ expect(first.lockPath).toBeTruthy();
85
+ const firstLockPath = first.lockPath!;
86
+
87
+ const sixtySecondsAgo = Date.now() / 1000 - 60;
88
+ await fs.utimes(firstLockPath, sixtySecondsAgo, sixtySecondsAgo);
89
+
90
+ const second = await acquireRefreshLock("acc-stable-refresh", {
91
+ timeoutMs: 30,
92
+ backoffMs: 5,
93
+ staleMs: DEFAULT_STALE_LOCK_MS,
94
+ });
95
+ expect(second.acquired).toBe(false);
96
+
97
+ await expect(fs.stat(firstLockPath)).resolves.toBeTruthy();
98
+ await releaseRefreshLock(first);
99
+ });
124
100
 
125
- await fs.unlink(firstLockPath);
126
- });
101
+ it("does not release when inode changed even if owner matches", async () => {
102
+ const first = await acquireRefreshLock("acc-4");
103
+ expect(first.acquired).toBe(true);
104
+ const firstLockPath = first.lockPath!;
105
+ const originalLockInode = first.lockInode;
106
+ expect(originalLockInode).not.toBeNull();
107
+
108
+ // Keep the same owner text but pass a deliberately mismatched inode. This
109
+ // tests the safety check directly without relying on filesystem-specific
110
+ // inode allocation behavior, which can aggressively recycle inode numbers
111
+ // on Linux CI runners.
112
+ await fs.writeFile(firstLockPath, JSON.stringify({ owner: first.owner, createdAt: Date.now() }), {
113
+ encoding: "utf-8",
114
+ mode: 0o600,
115
+ });
116
+
117
+ await releaseRefreshLock({
118
+ lockPath: firstLockPath,
119
+ owner: first.owner,
120
+ lockInode: originalLockInode! + 1n,
121
+ });
122
+
123
+ await expect(fs.stat(firstLockPath)).resolves.toBeTruthy();
124
+
125
+ await fs.unlink(firstLockPath);
126
+ });
127
127
  });
@@ -8,128 +8,128 @@ const DEFAULT_LOCK_BACKOFF_MS = 50;
8
8
  const DEFAULT_STALE_LOCK_MS = 90_000;
9
9
 
10
10
  function delay(ms: number): Promise<void> {
11
- return new Promise((resolve) => setTimeout(resolve, ms));
11
+ return new Promise((resolve) => setTimeout(resolve, ms));
12
12
  }
13
13
 
14
14
  function getLockPath(accountId: string): string {
15
- const hash = createHash("sha1").update(accountId).digest("hex").slice(0, 24);
16
- return join(dirname(getStoragePath()), "locks", `refresh-${hash}.lock`);
15
+ const hash = createHash("sha1").update(accountId).digest("hex").slice(0, 24);
16
+ return join(dirname(getStoragePath()), "locks", `refresh-${hash}.lock`);
17
17
  }
18
18
 
19
19
  export interface RefreshLockResult {
20
- acquired: boolean;
21
- lockPath: string | null;
22
- owner: string | null;
23
- lockInode: bigint | null;
20
+ acquired: boolean;
21
+ lockPath: string | null;
22
+ owner: string | null;
23
+ lockInode: bigint | null;
24
24
  }
25
25
 
26
26
  export interface AcquireLockOptions {
27
- timeoutMs?: number;
28
- backoffMs?: number;
29
- staleMs?: number;
27
+ timeoutMs?: number;
28
+ backoffMs?: number;
29
+ staleMs?: number;
30
30
  }
31
31
 
32
32
  /**
33
33
  * Try to acquire a per-account cross-process lock.
34
34
  */
35
35
  export async function acquireRefreshLock(
36
- accountId: string,
37
- options: AcquireLockOptions = {},
36
+ accountId: string,
37
+ options: AcquireLockOptions = {},
38
38
  ): Promise<RefreshLockResult> {
39
- const timeoutMs = options.timeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS;
40
- const backoffMs = options.backoffMs ?? DEFAULT_LOCK_BACKOFF_MS;
41
- const staleMs = options.staleMs ?? DEFAULT_STALE_LOCK_MS;
42
- const lockPath = getLockPath(accountId);
43
- const lockDir = dirname(lockPath);
44
- const deadline = Date.now() + Math.max(0, timeoutMs);
45
- const owner = randomBytes(12).toString("hex");
46
-
47
- await fs.mkdir(lockDir, { recursive: true });
48
-
49
- while (Date.now() <= deadline) {
50
- try {
51
- const handle = await fs.open(lockPath, "wx", 0o600);
52
- try {
53
- await handle.writeFile(JSON.stringify({ pid: process.pid, createdAt: Date.now(), owner }), "utf-8");
54
- const stat = await handle.stat({ bigint: true });
55
- return { acquired: true, lockPath, owner, lockInode: stat.ino };
56
- } finally {
57
- await handle.close();
58
- }
59
- } catch (error) {
60
- const code = (error as NodeJS.ErrnoException).code;
61
- if (code !== "EEXIST") {
62
- throw error;
63
- }
64
-
65
- try {
66
- const stat = await fs.stat(lockPath, { bigint: true });
67
- if (Date.now() - Number(stat.mtimeMs) > staleMs) {
68
- await fs.unlink(lockPath);
69
- continue;
39
+ const timeoutMs = options.timeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS;
40
+ const backoffMs = options.backoffMs ?? DEFAULT_LOCK_BACKOFF_MS;
41
+ const staleMs = options.staleMs ?? DEFAULT_STALE_LOCK_MS;
42
+ const lockPath = getLockPath(accountId);
43
+ const lockDir = dirname(lockPath);
44
+ const deadline = Date.now() + Math.max(0, timeoutMs);
45
+ const owner = randomBytes(12).toString("hex");
46
+
47
+ await fs.mkdir(lockDir, { recursive: true });
48
+
49
+ while (Date.now() <= deadline) {
50
+ try {
51
+ const handle = await fs.open(lockPath, "wx", 0o600);
52
+ try {
53
+ await handle.writeFile(JSON.stringify({ pid: process.pid, createdAt: Date.now(), owner }), "utf-8");
54
+ const stat = await handle.stat({ bigint: true });
55
+ return { acquired: true, lockPath, owner, lockInode: stat.ino };
56
+ } finally {
57
+ await handle.close();
58
+ }
59
+ } catch (error) {
60
+ const code = (error as NodeJS.ErrnoException).code;
61
+ if (code !== "EEXIST") {
62
+ throw error;
63
+ }
64
+
65
+ try {
66
+ const stat = await fs.stat(lockPath, { bigint: true });
67
+ if (Date.now() - Number(stat.mtimeMs) > staleMs) {
68
+ await fs.unlink(lockPath);
69
+ continue;
70
+ }
71
+ } catch {
72
+ // Lock may have been released concurrently; retry.
73
+ }
74
+
75
+ const remaining = deadline - Date.now();
76
+ if (remaining <= 0) break;
77
+ const jitter = Math.floor(Math.random() * 25);
78
+ await delay(Math.min(remaining, backoffMs + jitter));
70
79
  }
71
- } catch {
72
- // Lock may have been released concurrently; retry.
73
- }
74
-
75
- const remaining = deadline - Date.now();
76
- if (remaining <= 0) break;
77
- const jitter = Math.floor(Math.random() * 25);
78
- await delay(Math.min(remaining, backoffMs + jitter));
79
80
  }
80
- }
81
81
 
82
- return { acquired: false, lockPath: null, owner: null, lockInode: null };
82
+ return { acquired: false, lockPath: null, owner: null, lockInode: null };
83
83
  }
84
84
 
85
85
  export type ReleaseLockInput =
86
- | {
87
- lockPath: string | null;
88
- owner?: string | null;
89
- lockInode?: bigint | null;
90
- }
91
- | string
92
- | null;
86
+ | {
87
+ lockPath: string | null;
88
+ owner?: string | null;
89
+ lockInode?: bigint | null;
90
+ }
91
+ | string
92
+ | null;
93
93
 
94
94
  /**
95
95
  * Release a lock acquired by acquireRefreshLock.
96
96
  */
97
97
  export async function releaseRefreshLock(lock: ReleaseLockInput): Promise<void> {
98
- const lockPath = typeof lock === "string" || lock === null ? lock : lock.lockPath;
99
- const owner = typeof lock === "object" && lock ? lock.owner || null : null;
100
- const lockInode = typeof lock === "object" && lock ? (lock.lockInode ?? null) : null;
101
-
102
- if (!lockPath) return;
98
+ const lockPath = typeof lock === "string" || lock === null ? lock : lock.lockPath;
99
+ const owner = typeof lock === "object" && lock ? lock.owner || null : null;
100
+ const lockInode = typeof lock === "object" && lock ? (lock.lockInode ?? null) : null;
101
+
102
+ if (!lockPath) return;
103
+
104
+ // Ownership-safe release: avoid deleting a lock that another process
105
+ // acquired after ours became stale.
106
+ if (owner) {
107
+ try {
108
+ const content = await fs.readFile(lockPath, "utf-8");
109
+ const parsed = JSON.parse(content);
110
+ if (!parsed || typeof parsed !== "object" || parsed.owner !== owner) {
111
+ return;
112
+ }
113
+
114
+ if (lockInode) {
115
+ const stat = await fs.stat(lockPath, { bigint: true });
116
+ if (stat.ino !== lockInode) {
117
+ return;
118
+ }
119
+ }
120
+ } catch (error) {
121
+ const code = (error as NodeJS.ErrnoException).code;
122
+ if (code === "ENOENT") return;
123
+ // If unreadable/corrupt, fail closed to avoid deleting another
124
+ // process's lock when ownership cannot be verified.
125
+ return;
126
+ }
127
+ }
103
128
 
104
- // Ownership-safe release: avoid deleting a lock that another process
105
- // acquired after ours became stale.
106
- if (owner) {
107
129
  try {
108
- const content = await fs.readFile(lockPath, "utf-8");
109
- const parsed = JSON.parse(content);
110
- if (!parsed || typeof parsed !== "object" || parsed.owner !== owner) {
111
- return;
112
- }
113
-
114
- if (lockInode) {
115
- const stat = await fs.stat(lockPath, { bigint: true });
116
- if (stat.ino !== lockInode) {
117
- return;
118
- }
119
- }
130
+ await fs.unlink(lockPath);
120
131
  } catch (error) {
121
- const code = (error as NodeJS.ErrnoException).code;
122
- if (code === "ENOENT") return;
123
- // If unreadable/corrupt, fail closed to avoid deleting another
124
- // process's lock when ownership cannot be verified.
125
- return;
132
+ const code = (error as NodeJS.ErrnoException).code;
133
+ if (code !== "ENOENT") throw error;
126
134
  }
127
- }
128
-
129
- try {
130
- await fs.unlink(lockPath);
131
- } catch (error) {
132
- const code = (error as NodeJS.ErrnoException).code;
133
- if (code !== "ENOENT") throw error;
134
- }
135
135
  }