@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
package/src/oauth.ts CHANGED
@@ -16,165 +16,165 @@ const OAUTH_TOKEN_USER_AGENT = "axios/1.13.6";
16
16
 
17
17
  const OAUTH_SCOPES_API_KEY = ["org:create_api_key", "user:profile"];
18
18
  const OAUTH_SCOPES_AUTH = [
19
- "user:profile",
20
- "user:inference",
21
- "user:sessions:claude_code",
22
- "user:mcp_servers",
23
- "user:file_upload",
19
+ "user:profile",
20
+ "user:inference",
21
+ "user:sessions:claude_code",
22
+ "user:mcp_servers",
23
+ "user:file_upload",
24
24
  ];
25
25
  const OAUTH_ALL_SCOPES = [...new Set([...OAUTH_SCOPES_API_KEY, ...OAUTH_SCOPES_AUTH])];
26
26
 
27
27
  function base64url(input: Buffer): string {
28
- return input.toString("base64url");
28
+ return input.toString("base64url");
29
29
  }
30
30
 
31
31
  function generatePKCE(): { verifier: string; challenge: string } {
32
- const verifier = base64url(randomBytes(32));
33
- const challenge = base64url(createHash("sha256").update(verifier).digest());
34
- return { verifier, challenge };
32
+ const verifier = base64url(randomBytes(32));
33
+ const challenge = base64url(createHash("sha256").update(verifier).digest());
34
+ return { verifier, challenge };
35
35
  }
36
36
 
37
37
  export interface AuthorizeOptions {
38
- orgUUID?: string;
39
- loginHint?: string;
40
- loginMethod?: string;
38
+ orgUUID?: string;
39
+ loginHint?: string;
40
+ loginMethod?: string;
41
41
  }
42
42
 
43
43
  /**
44
44
  * Build an OAuth authorization URL with PKCE challenge.
45
45
  */
46
46
  export async function authorize(
47
- mode: "max" | "console",
48
- options: AuthorizeOptions = {},
47
+ mode: "max" | "console",
48
+ options: AuthorizeOptions = {},
49
49
  ): Promise<{ url: string; verifier: string; state: string }> {
50
- const pkce = generatePKCE();
51
- // Use a separate random value for state (not the verifier) to avoid
52
- // leaking the PKCE verifier in browser history / referrer headers.
53
- const state = base64url(randomBytes(32));
50
+ const pkce = generatePKCE();
51
+ // Use a separate random value for state (not the verifier) to avoid
52
+ // leaking the PKCE verifier in browser history / referrer headers.
53
+ const state = base64url(randomBytes(32));
54
54
 
55
- const url = new URL(`https://${mode === "console" ? OAUTH_CONSOLE_HOST : OAUTH_MAX_HOST}/oauth/authorize`);
56
- url.searchParams.set("code", "true");
57
- url.searchParams.set("client_id", CLIENT_ID);
58
- url.searchParams.set("response_type", "code");
59
- url.searchParams.set("redirect_uri", OAUTH_REDIRECT_URI);
60
- url.searchParams.set("scope", OAUTH_ALL_SCOPES.join(" "));
61
- url.searchParams.set("code_challenge", pkce.challenge);
62
- url.searchParams.set("code_challenge_method", "S256");
63
- url.searchParams.set("state", state);
64
- if (options.orgUUID) url.searchParams.set("orgUUID", options.orgUUID);
65
- if (options.loginHint) url.searchParams.set("login_hint", options.loginHint);
66
- if (options.loginMethod) url.searchParams.set("login_method", options.loginMethod);
67
- return {
68
- url: url.toString(),
69
- verifier: pkce.verifier,
70
- state,
71
- };
55
+ const url = new URL(`https://${mode === "console" ? OAUTH_CONSOLE_HOST : OAUTH_MAX_HOST}/oauth/authorize`);
56
+ url.searchParams.set("code", "true");
57
+ url.searchParams.set("client_id", CLIENT_ID);
58
+ url.searchParams.set("response_type", "code");
59
+ url.searchParams.set("redirect_uri", OAUTH_REDIRECT_URI);
60
+ url.searchParams.set("scope", OAUTH_ALL_SCOPES.join(" "));
61
+ url.searchParams.set("code_challenge", pkce.challenge);
62
+ url.searchParams.set("code_challenge_method", "S256");
63
+ url.searchParams.set("state", state);
64
+ if (options.orgUUID) url.searchParams.set("orgUUID", options.orgUUID);
65
+ if (options.loginHint) url.searchParams.set("login_hint", options.loginHint);
66
+ if (options.loginMethod) url.searchParams.set("login_method", options.loginMethod);
67
+ return {
68
+ url: url.toString(),
69
+ verifier: pkce.verifier,
70
+ state,
71
+ };
72
72
  }
73
73
 
74
74
  export type ExchangeResult =
75
- | {
76
- type: "success";
77
- refresh: string;
78
- access: string;
79
- expires: number;
80
- email?: string;
81
- }
82
- | {
83
- type: "failed";
84
- status?: number;
85
- code?: string;
86
- reason?: string;
87
- details?: string;
88
- };
75
+ | {
76
+ type: "success";
77
+ refresh: string;
78
+ access: string;
79
+ expires: number;
80
+ email?: string;
81
+ }
82
+ | {
83
+ type: "failed";
84
+ status?: number;
85
+ code?: string;
86
+ reason?: string;
87
+ details?: string;
88
+ };
89
89
 
90
90
  /**
91
91
  * Exchange an authorization code for tokens.
92
92
  */
93
93
  export async function exchange(code: string, verifier: string): Promise<ExchangeResult> {
94
- const fail = (status: number | undefined, rawText = ""): ExchangeResult => {
95
- let errorCode: string | undefined;
96
- let reason: string | undefined;
94
+ const fail = (status: number | undefined, rawText = ""): ExchangeResult => {
95
+ let errorCode: string | undefined;
96
+ let reason: string | undefined;
97
97
 
98
- if (rawText) {
99
- try {
100
- const parsed = JSON.parse(rawText);
101
- if (typeof parsed.error === "string" && parsed.error) {
102
- errorCode = parsed.error;
103
- }
104
- if (typeof parsed.error_description === "string" && parsed.error_description) {
105
- reason = parsed.error_description;
106
- } else if (typeof parsed.message === "string" && parsed.message) {
107
- reason = parsed.message;
98
+ if (rawText) {
99
+ try {
100
+ const parsed = JSON.parse(rawText);
101
+ if (typeof parsed.error === "string" && parsed.error) {
102
+ errorCode = parsed.error;
103
+ }
104
+ if (typeof parsed.error_description === "string" && parsed.error_description) {
105
+ reason = parsed.error_description;
106
+ } else if (typeof parsed.message === "string" && parsed.message) {
107
+ reason = parsed.message;
108
+ }
109
+ } catch {
110
+ // Body is not JSON — use raw text as the reason, trimmed to strip whitespace
111
+ reason = rawText.trim() || undefined;
112
+ }
108
113
  }
109
- } catch {
110
- // Body is not JSON — use raw text as the reason, trimmed to strip whitespace
111
- reason = rawText.trim() || undefined;
112
- }
113
- }
114
114
 
115
- const detailsParts: string[] = [];
116
- if (typeof status === "number") detailsParts.push(`HTTP ${status}`);
117
- if (errorCode) detailsParts.push(errorCode);
118
- if (reason) detailsParts.push(reason);
115
+ const detailsParts: string[] = [];
116
+ if (typeof status === "number") detailsParts.push(`HTTP ${status}`);
117
+ if (errorCode) detailsParts.push(errorCode);
118
+ if (reason) detailsParts.push(reason);
119
119
 
120
- return {
121
- type: "failed",
122
- ...(typeof status === "number" ? { status } : {}),
123
- ...(errorCode ? { code: errorCode } : {}),
124
- ...(reason ? { reason } : {}),
125
- ...(detailsParts.length ? { details: detailsParts.join(" · ") } : {}),
120
+ return {
121
+ type: "failed",
122
+ ...(typeof status === "number" ? { status } : {}),
123
+ ...(errorCode ? { code: errorCode } : {}),
124
+ ...(reason ? { reason } : {}),
125
+ ...(detailsParts.length ? { details: detailsParts.join(" · ") } : {}),
126
+ };
126
127
  };
127
- };
128
128
 
129
- const splits = code.split("#");
130
- let result: Response;
131
- try {
132
- result = await fetch(OAUTH_TOKEN_URL, {
133
- method: "POST",
134
- headers: {
135
- "Content-Type": "application/json",
136
- "User-Agent": OAUTH_TOKEN_USER_AGENT,
137
- },
138
- body: JSON.stringify({
139
- code: splits[0],
140
- state: splits[1],
141
- grant_type: "authorization_code",
142
- client_id: CLIENT_ID,
143
- redirect_uri: OAUTH_REDIRECT_URI,
144
- code_verifier: verifier,
145
- }),
146
- signal: AbortSignal.timeout(15000),
147
- });
148
- } catch (err) {
149
- return fail(undefined, err instanceof Error ? err.message : String(err));
150
- }
129
+ const splits = code.split("#");
130
+ let result: Response;
131
+ try {
132
+ result = await fetch(OAUTH_TOKEN_URL, {
133
+ method: "POST",
134
+ headers: {
135
+ "Content-Type": "application/json",
136
+ "User-Agent": OAUTH_TOKEN_USER_AGENT,
137
+ },
138
+ body: JSON.stringify({
139
+ code: splits[0],
140
+ state: splits[1],
141
+ grant_type: "authorization_code",
142
+ client_id: CLIENT_ID,
143
+ redirect_uri: OAUTH_REDIRECT_URI,
144
+ code_verifier: verifier,
145
+ }),
146
+ signal: AbortSignal.timeout(15000),
147
+ });
148
+ } catch (err) {
149
+ return fail(undefined, err instanceof Error ? err.message : String(err));
150
+ }
151
151
 
152
- if (!result.ok) {
153
- const raw =
154
- typeof result.text === "function"
155
- ? await result
156
- .text()
157
- .then((value) => (typeof value === "string" ? value : ""))
158
- // Body may be unreadable on 5xx responses; empty string is the correct fallback
159
- // because we've already captured the HTTP status for the caller.
160
- .catch(() => "")
161
- : "";
162
- return fail(result.status, raw);
163
- }
152
+ if (!result.ok) {
153
+ const raw =
154
+ typeof result.text === "function"
155
+ ? await result
156
+ .text()
157
+ .then((value) => (typeof value === "string" ? value : ""))
158
+ // Body may be unreadable on 5xx responses; empty string is the correct fallback
159
+ // because we've already captured the HTTP status for the caller.
160
+ .catch(() => "")
161
+ : "";
162
+ return fail(result.status, raw);
163
+ }
164
164
 
165
- const json = (await result.json()) as {
166
- refresh_token: string;
167
- access_token: string;
168
- expires_in: number;
169
- account?: { email_address?: string };
170
- };
171
- return {
172
- type: "success",
173
- refresh: json.refresh_token,
174
- access: json.access_token,
175
- expires: Date.now() + json.expires_in * 1000,
176
- email: json.account?.email_address || undefined,
177
- };
165
+ const json = (await result.json()) as {
166
+ refresh_token: string;
167
+ access_token: string;
168
+ expires_in: number;
169
+ account?: { email_address?: string };
170
+ };
171
+ return {
172
+ type: "success",
173
+ refresh: json.refresh_token,
174
+ access: json.access_token,
175
+ expires: Date.now() + json.expires_in * 1000,
176
+ email: json.account?.email_address || undefined,
177
+ };
178
178
  }
179
179
 
180
180
  /**
@@ -185,37 +185,37 @@ export async function exchange(code: string, verifier: string): Promise<Exchange
185
185
  * proceed with local cleanup regardless of the result.
186
186
  */
187
187
  export async function revoke(refreshToken: string): Promise<boolean> {
188
- try {
189
- const resp = await fetch(OAUTH_REVOKE_URL, {
190
- method: "POST",
191
- headers: {
192
- "Content-Type": "application/json",
193
- "User-Agent": OAUTH_TOKEN_USER_AGENT,
194
- },
195
- body: JSON.stringify({
196
- token: refreshToken,
197
- token_type_hint: "refresh_token",
198
- client_id: CLIENT_ID,
199
- }),
200
- signal: AbortSignal.timeout(5000),
201
- });
202
- return resp.ok;
203
- } catch {
204
- // Best-effort revocation — network errors, DNS failures, or endpoint unavailability
205
- // are all expected; callers proceed with local cleanup regardless.
206
- return false;
207
- }
188
+ try {
189
+ const resp = await fetch(OAUTH_REVOKE_URL, {
190
+ method: "POST",
191
+ headers: {
192
+ "Content-Type": "application/json",
193
+ "User-Agent": OAUTH_TOKEN_USER_AGENT,
194
+ },
195
+ body: JSON.stringify({
196
+ token: refreshToken,
197
+ token_type_hint: "refresh_token",
198
+ client_id: CLIENT_ID,
199
+ }),
200
+ signal: AbortSignal.timeout(5000),
201
+ });
202
+ return resp.ok;
203
+ } catch {
204
+ // Best-effort revocation — network errors, DNS failures, or endpoint unavailability
205
+ // are all expected; callers proceed with local cleanup regardless.
206
+ return false;
207
+ }
208
208
  }
209
209
 
210
210
  export interface TokenRefreshResponse {
211
- access_token: string;
212
- refresh_token: string;
213
- expires_in: number;
211
+ access_token: string;
212
+ refresh_token: string;
213
+ expires_in: number;
214
214
  }
215
215
 
216
216
  interface RefreshError extends Error {
217
- status?: number;
218
- code?: string;
217
+ status?: number;
218
+ code?: string;
219
219
  }
220
220
 
221
221
  /**
@@ -223,36 +223,36 @@ interface RefreshError extends Error {
223
223
  * @throws On HTTP errors or network failures
224
224
  */
225
225
  export async function refreshToken(
226
- refreshTokenValue: string,
227
- options: { signal?: AbortSignal } = {},
226
+ refreshTokenValue: string,
227
+ options: { signal?: AbortSignal } = {},
228
228
  ): Promise<TokenRefreshResponse> {
229
- const resp = await fetch(OAUTH_TOKEN_URL, {
230
- method: "POST",
231
- headers: {
232
- "Content-Type": "application/json",
233
- "User-Agent": OAUTH_TOKEN_USER_AGENT,
234
- },
235
- body: JSON.stringify({
236
- grant_type: "refresh_token",
237
- client_id: CLIENT_ID,
238
- refresh_token: refreshTokenValue,
239
- }),
240
- ...(options.signal ? { signal: options.signal } : {}),
241
- });
229
+ const resp = await fetch(OAUTH_TOKEN_URL, {
230
+ method: "POST",
231
+ headers: {
232
+ "Content-Type": "application/json",
233
+ "User-Agent": OAUTH_TOKEN_USER_AGENT,
234
+ },
235
+ body: JSON.stringify({
236
+ grant_type: "refresh_token",
237
+ client_id: CLIENT_ID,
238
+ refresh_token: refreshTokenValue,
239
+ }),
240
+ ...(options.signal ? { signal: options.signal } : {}),
241
+ });
242
242
 
243
- if (!resp.ok) {
244
- // Empty string is the correct fallback — we already have resp.status for the error message.
245
- const text = await resp.text().catch(() => "");
246
- const error: RefreshError = new Error(`Token refresh failed (HTTP ${resp.status}): ${text}`);
247
- error.status = resp.status;
248
- try {
249
- const parsed = JSON.parse(text);
250
- if (parsed.error) error.code = parsed.error;
251
- } catch {
252
- // Body may not be valid JSON — leave error.code unset, the HTTP status is still attached
243
+ if (!resp.ok) {
244
+ // Empty string is the correct fallback — we already have resp.status for the error message.
245
+ const text = await resp.text().catch(() => "");
246
+ const error: RefreshError = new Error(`Token refresh failed (HTTP ${resp.status}): ${text}`);
247
+ error.status = resp.status;
248
+ try {
249
+ const parsed = JSON.parse(text);
250
+ if (parsed.error) error.code = parsed.error;
251
+ } catch {
252
+ // Body may not be valid JSON — leave error.code unset, the HTTP status is still attached
253
+ }
254
+ throw error;
253
255
  }
254
- throw error;
255
- }
256
256
 
257
- return resp.json() as Promise<TokenRefreshResponse>;
257
+ return resp.json() as Promise<TokenRefreshResponse>;
258
258
  }