@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.
- package/README.md +88 -88
- package/dist/opencode-anthropic-auth-cli.mjs +804 -507
- package/dist/opencode-anthropic-auth-plugin.js +4751 -4109
- package/package.json +67 -59
- package/src/__tests__/billing-edge-cases.test.ts +59 -59
- package/src/__tests__/bun-proxy.parallel.test.ts +388 -382
- package/src/__tests__/cc-comparison.test.ts +87 -87
- package/src/__tests__/cc-credentials.test.ts +254 -250
- package/src/__tests__/cch-drift-checker.test.ts +51 -51
- package/src/__tests__/cch-native-style.test.ts +56 -56
- package/src/__tests__/debug-gating.test.ts +42 -42
- package/src/__tests__/decomposition-smoke.test.ts +68 -68
- package/src/__tests__/fingerprint-regression.test.ts +575 -566
- package/src/__tests__/helpers/conversation-history.smoke.test.ts +271 -271
- package/src/__tests__/helpers/conversation-history.ts +119 -119
- package/src/__tests__/helpers/deferred.smoke.test.ts +103 -103
- package/src/__tests__/helpers/deferred.ts +69 -69
- package/src/__tests__/helpers/in-memory-storage.smoke.test.ts +155 -155
- package/src/__tests__/helpers/in-memory-storage.ts +88 -88
- package/src/__tests__/helpers/mock-bun-proxy.smoke.test.ts +68 -68
- package/src/__tests__/helpers/mock-bun-proxy.ts +189 -189
- package/src/__tests__/helpers/plugin-fetch-harness.smoke.test.ts +273 -273
- package/src/__tests__/helpers/plugin-fetch-harness.ts +288 -288
- package/src/__tests__/helpers/sse.smoke.test.ts +236 -236
- package/src/__tests__/helpers/sse.ts +209 -209
- package/src/__tests__/index.parallel.test.ts +605 -595
- package/src/__tests__/sanitization-regex.test.ts +112 -112
- package/src/__tests__/state-bounds.test.ts +90 -90
- package/src/account-identity.test.ts +197 -192
- package/src/account-identity.ts +69 -67
- package/src/account-state.test.ts +86 -86
- package/src/account-state.ts +25 -25
- package/src/accounts/matching.test.ts +335 -0
- package/src/accounts/matching.ts +167 -0
- package/src/accounts/persistence.test.ts +345 -0
- package/src/accounts/persistence.ts +432 -0
- package/src/accounts/repair.test.ts +276 -0
- package/src/accounts/repair.ts +407 -0
- package/src/accounts.dedup.test.ts +621 -621
- package/src/accounts.test.ts +933 -929
- package/src/accounts.ts +633 -989
- package/src/backoff.test.ts +345 -345
- package/src/backoff.ts +219 -219
- package/src/betas.ts +124 -124
- package/src/bun-fetch.test.ts +345 -342
- package/src/bun-fetch.ts +424 -424
- package/src/bun-proxy.test.ts +25 -25
- package/src/bun-proxy.ts +209 -209
- package/src/cc-credentials.ts +111 -111
- package/src/circuit-breaker.test.ts +184 -184
- package/src/circuit-breaker.ts +169 -169
- package/src/cli/commands/auth.ts +963 -0
- package/src/cli/commands/config.ts +547 -0
- package/src/cli/formatting.test.ts +406 -0
- package/src/cli/formatting.ts +219 -0
- package/src/cli.ts +255 -2022
- package/src/commands/handlers/betas.ts +100 -0
- package/src/commands/handlers/config.ts +99 -0
- package/src/commands/handlers/files.ts +375 -0
- package/src/commands/oauth-flow.ts +181 -166
- package/src/commands/prompts.ts +61 -61
- package/src/commands/router.test.ts +421 -0
- package/src/commands/router.ts +143 -635
- package/src/config.test.ts +482 -482
- package/src/config.ts +412 -404
- package/src/constants.ts +48 -48
- package/src/drift/cch-constants.ts +95 -95
- package/src/env.ts +111 -105
- package/src/headers/billing.ts +33 -33
- package/src/headers/builder.ts +130 -130
- package/src/headers/cch.ts +75 -75
- package/src/headers/stainless.ts +25 -25
- package/src/headers/user-agent.ts +23 -23
- package/src/index.ts +436 -828
- package/src/models.ts +27 -27
- package/src/oauth.test.ts +102 -102
- package/src/oauth.ts +178 -178
- package/src/parent-pid-watcher.test.ts +148 -148
- package/src/parent-pid-watcher.ts +69 -69
- package/src/plugin-helpers.ts +82 -82
- package/src/refresh-helpers.ts +145 -139
- package/src/refresh-lock.test.ts +94 -94
- package/src/refresh-lock.ts +93 -93
- package/src/request/body.history.test.ts +579 -571
- package/src/request/body.ts +255 -255
- package/src/request/metadata.ts +65 -65
- package/src/request/retry.test.ts +156 -156
- package/src/request/retry.ts +67 -67
- package/src/request/url.ts +21 -21
- package/src/request-orchestration-helpers.ts +648 -0
- package/src/response/index.ts +5 -5
- package/src/response/mcp.ts +58 -58
- package/src/response/streaming.test.ts +313 -311
- package/src/response/streaming.ts +412 -410
- package/src/rotation.test.ts +304 -301
- package/src/rotation.ts +205 -205
- package/src/storage.test.ts +547 -547
- package/src/storage.ts +315 -291
- package/src/system-prompt/builder.ts +38 -38
- package/src/system-prompt/index.ts +5 -5
- package/src/system-prompt/normalize.ts +60 -60
- package/src/system-prompt/sanitize.ts +30 -30
- package/src/thinking.ts +21 -20
- package/src/token-refresh.test.ts +265 -265
- package/src/token-refresh.ts +219 -214
- package/src/types.ts +30 -30
- 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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
28
|
+
return input.toString("base64url");
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
function generatePKCE(): { verifier: string; challenge: string } {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
48
|
-
|
|
47
|
+
mode: "max" | "console",
|
|
48
|
+
options: AuthorizeOptions = {},
|
|
49
49
|
): Promise<{ url: string; verifier: string; state: string }> {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
const fail = (status: number | undefined, rawText = ""): ExchangeResult => {
|
|
95
|
+
let errorCode: string | undefined;
|
|
96
|
+
let reason: string | undefined;
|
|
97
97
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
211
|
+
access_token: string;
|
|
212
|
+
refresh_token: string;
|
|
213
|
+
expires_in: number;
|
|
214
214
|
}
|
|
215
215
|
|
|
216
216
|
interface RefreshError extends Error {
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
227
|
-
|
|
226
|
+
refreshTokenValue: string,
|
|
227
|
+
options: { signal?: AbortSignal } = {},
|
|
228
228
|
): Promise<TokenRefreshResponse> {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
257
|
+
return resp.json() as Promise<TokenRefreshResponse>;
|
|
258
258
|
}
|