@ex-machina/opencode-anthropic-auth 2.0.0-next.1 → 2.0.0-next.3
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 +2 -2
- package/dist/auth.d.ts +3 -3
- package/dist/auth.js +161 -82
- package/dist/bounded.d.ts +8 -0
- package/dist/bounded.js +59 -0
- package/dist/config.d.ts +10 -0
- package/dist/config.js +25 -15
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +575 -57
- package/dist/json-response-stream.d.ts +10 -0
- package/dist/json-response-stream.js +638 -0
- package/dist/rate-limit.d.ts +18 -0
- package/dist/rate-limit.js +354 -0
- package/dist/transform.d.ts +41 -19
- package/dist/transform.js +592 -329
- package/dist/version-rejection.d.ts +17 -0
- package/dist/version-rejection.js +75 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -114,9 +114,9 @@ The plugin reads the following environment variables:
|
|
|
114
114
|
- **`ANTHROPIC_INSECURE`** — Skips TLS certificate verification. Behavior differs by OpenCode version:
|
|
115
115
|
- **OpenCode v1** — Set to `1` or `true` to skip verification. Only effective when `ANTHROPIC_BASE_URL` is also set.
|
|
116
116
|
- **OpenCode v2** — Not supported. OpenCode v2 plugin request hooks cannot disable TLS verification. If set, the plugin logs a warning and leaves verification enabled; requests to an untrusted or self-signed `ANTHROPIC_BASE_URL` will fail.
|
|
117
|
-
- **`ANTHROPIC_CLAUDE_CODE_VERSION`** — Overrides the Claude Code version reported to Anthropic for both release lines. Must be `major.minor.patch` (for example, `2.1.
|
|
117
|
+
- **`ANTHROPIC_CLAUDE_CODE_VERSION`** — Overrides the Claude Code version reported to Anthropic for both release lines. Must be `major.minor.patch` (for example, `2.1.280`). Defaults to the bundled version; a malformed value logs an actionable error without echoing its contents, and the bundled version is used instead. A value older than the bundled version is honored but logs a warning, since reporting an older version can make newer models reject the request. Read once when the plugin loads, so restart OpenCode after changing it.
|
|
118
118
|
|
|
119
|
-
Anthropic gates model access on the reported Claude Code version server-side, returning a 400 `claude_code_version_too_old` error for models that require a newer client. `ANTHROPIC_CLAUDE_CODE_VERSION`
|
|
119
|
+
Anthropic gates model access on the reported Claude Code version server-side, returning a 400 `claude_code_version_too_old` error for models that require a newer client. On OpenCode v2, when the exact structured rejection names a newer minimum, the plugin adopts that real version for the rest of the process and retries the initial request once. Other 400 responses and valid explicit `ANTHROPIC_CLAUDE_CODE_VERSION` overrides are never changed automatically. A malformed override is ignored, so its bundled fallback remains eligible for exact-response recovery. The override remains an escape hatch for both release lines.
|
|
120
120
|
|
|
121
121
|
## How It Works
|
|
122
122
|
|
package/dist/auth.d.ts
CHANGED
|
@@ -25,8 +25,8 @@ export type RefreshResult = {
|
|
|
25
25
|
};
|
|
26
26
|
/**
|
|
27
27
|
* Exchange a refresh token for a new access/refresh token pair.
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
28
|
+
* Refresh tokens may rotate after a request reaches the provider. Retrying an
|
|
29
|
+
* ambiguous 5xx, timeout, network failure, or response-body failure can replay
|
|
30
|
+
* an already consumed token, so each call makes exactly one token request.
|
|
31
31
|
*/
|
|
32
32
|
export declare function refreshToken(refreshTokenValue: string): Promise<RefreshResult>;
|
package/dist/auth.js
CHANGED
|
@@ -1,6 +1,37 @@
|
|
|
1
|
+
import { BodyLimitError, contentLength, InvalidUtf8Error, readBoundedText, } from "./bounded.js";
|
|
1
2
|
import { AUTHORIZE_URLS, CLIENT_ID, CODE_CALLBACK_URL, OAUTH_SCOPES, TOKEN_URL, } from "./constants.js";
|
|
2
3
|
import { generatePKCE } from "./pkce.js";
|
|
3
|
-
const
|
|
4
|
+
const TOKEN_TIMEOUT_MS = 30_000;
|
|
5
|
+
const MAX_TOKEN_RESPONSE_BYTES = 64 * 1024;
|
|
6
|
+
const MAX_TOKEN_LENGTH = 8 * 1024;
|
|
7
|
+
const MAX_CALLBACK_INPUT_BYTES = 16 * 1024;
|
|
8
|
+
const MAX_VERIFIER_BYTES = 1024;
|
|
9
|
+
const MAX_REDIRECT_URI_BYTES = 2 * 1024;
|
|
10
|
+
function isWellFormedUtf16(value) {
|
|
11
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
12
|
+
const unit = value.charCodeAt(index);
|
|
13
|
+
if (unit >= 0xd800 && unit <= 0xdbff) {
|
|
14
|
+
if (index + 1 >= value.length)
|
|
15
|
+
return false;
|
|
16
|
+
const next = value.charCodeAt(index + 1);
|
|
17
|
+
if (next < 0xdc00 || next > 0xdfff)
|
|
18
|
+
return false;
|
|
19
|
+
index += 1;
|
|
20
|
+
}
|
|
21
|
+
else if (unit >= 0xdc00 && unit <= 0xdfff) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
function isBoundedUtf8(value, maxBytes) {
|
|
28
|
+
if (value.length === 0 ||
|
|
29
|
+
value.length > maxBytes ||
|
|
30
|
+
!isWellFormedUtf16(value)) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
return new TextEncoder().encode(value).byteLength <= maxBytes;
|
|
34
|
+
}
|
|
4
35
|
function isTokenResponse(value) {
|
|
5
36
|
if (typeof value !== 'object' || value === null)
|
|
6
37
|
return false;
|
|
@@ -9,16 +40,23 @@ function isTokenResponse(value) {
|
|
|
9
40
|
if (!('expires_in' in value))
|
|
10
41
|
return false;
|
|
11
42
|
return (typeof value.refresh_token === 'string' &&
|
|
12
|
-
value.refresh_token
|
|
43
|
+
isBoundedUtf8(value.refresh_token, MAX_TOKEN_LENGTH) &&
|
|
13
44
|
typeof value.access_token === 'string' &&
|
|
14
|
-
value.access_token
|
|
45
|
+
isBoundedUtf8(value.access_token, MAX_TOKEN_LENGTH) &&
|
|
15
46
|
typeof value.expires_in === 'number' &&
|
|
16
47
|
Number.isSafeInteger(value.expires_in) &&
|
|
17
48
|
value.expires_in > 0);
|
|
18
49
|
}
|
|
19
50
|
async function parseTokenResponse(response) {
|
|
51
|
+
const declaredLength = contentLength(response.headers);
|
|
52
|
+
if (declaredLength !== undefined &&
|
|
53
|
+
declaredLength > MAX_TOKEN_RESPONSE_BYTES) {
|
|
54
|
+
await response.body?.cancel().catch(() => { });
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
20
57
|
try {
|
|
21
|
-
const
|
|
58
|
+
const text = await readBoundedText(response.body, MAX_TOKEN_RESPONSE_BYTES, 'Anthropic token response');
|
|
59
|
+
const value = JSON.parse(text);
|
|
22
60
|
if (!isTokenResponse(value))
|
|
23
61
|
return undefined;
|
|
24
62
|
const expires = Date.now() + value.expires_in * 1000;
|
|
@@ -31,11 +69,54 @@ async function parseTokenResponse(response) {
|
|
|
31
69
|
};
|
|
32
70
|
}
|
|
33
71
|
catch (error) {
|
|
34
|
-
if (error instanceof SyntaxError
|
|
72
|
+
if (error instanceof SyntaxError ||
|
|
73
|
+
error instanceof BodyLimitError ||
|
|
74
|
+
error instanceof InvalidUtf8Error) {
|
|
35
75
|
return undefined;
|
|
76
|
+
}
|
|
36
77
|
throw error;
|
|
37
78
|
}
|
|
38
79
|
}
|
|
80
|
+
function isTransientNetworkError(error) {
|
|
81
|
+
const seen = new WeakSet();
|
|
82
|
+
let current = error;
|
|
83
|
+
for (let depth = 0; depth < 8; depth++) {
|
|
84
|
+
if (typeof current !== 'object' || current === null)
|
|
85
|
+
return false;
|
|
86
|
+
if (seen.has(current))
|
|
87
|
+
return false;
|
|
88
|
+
seen.add(current);
|
|
89
|
+
if ('name' in current &&
|
|
90
|
+
(current.name === 'TimeoutError' || current.name === 'AbortError')) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
if ('code' in current) {
|
|
94
|
+
const code = current.code;
|
|
95
|
+
if (code === 'ECONNRESET' ||
|
|
96
|
+
code === 'ECONNREFUSED' ||
|
|
97
|
+
code === 'ETIMEDOUT' ||
|
|
98
|
+
code === 'EPIPE' ||
|
|
99
|
+
code === 'ENETUNREACH' ||
|
|
100
|
+
code === 'EAI_AGAIN' ||
|
|
101
|
+
code === 'UND_ERR_CONNECT_TIMEOUT' ||
|
|
102
|
+
code === 'UND_ERR_SOCKET') {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const message = current instanceof Error ? current.message.toLowerCase() : '';
|
|
107
|
+
if (message === 'fetch failed' ||
|
|
108
|
+
message === 'terminated' ||
|
|
109
|
+
message === 'network error' ||
|
|
110
|
+
message.includes('socket hang up') ||
|
|
111
|
+
message.includes('other side closed')) {
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
if (!('cause' in current))
|
|
115
|
+
return false;
|
|
116
|
+
current = current.cause;
|
|
117
|
+
}
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
39
120
|
function generateState() {
|
|
40
121
|
return crypto.randomUUID().replace(/-/g, '');
|
|
41
122
|
}
|
|
@@ -65,23 +146,34 @@ function parseCallbackInput(input) {
|
|
|
65
146
|
return null;
|
|
66
147
|
}
|
|
67
148
|
async function exchangeCode(callback, verifier, redirectUri) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
149
|
+
let result;
|
|
150
|
+
try {
|
|
151
|
+
result = await fetch(TOKEN_URL, {
|
|
152
|
+
method: 'POST',
|
|
153
|
+
signal: AbortSignal.timeout(TOKEN_TIMEOUT_MS),
|
|
154
|
+
redirect: 'error',
|
|
155
|
+
headers: {
|
|
156
|
+
'Content-Type': 'application/json',
|
|
157
|
+
Accept: 'application/json, text/plain, */*',
|
|
158
|
+
'User-Agent': 'axios/1.13.6',
|
|
159
|
+
},
|
|
160
|
+
body: JSON.stringify({
|
|
161
|
+
code: callback.code,
|
|
162
|
+
state: callback.state,
|
|
163
|
+
grant_type: 'authorization_code',
|
|
164
|
+
client_id: CLIENT_ID,
|
|
165
|
+
redirect_uri: redirectUri,
|
|
166
|
+
code_verifier: verifier,
|
|
167
|
+
}),
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
if (isTransientNetworkError(error))
|
|
172
|
+
return { type: 'failed' };
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
84
175
|
if (!result.ok) {
|
|
176
|
+
await result.body?.cancel().catch(() => { });
|
|
85
177
|
return {
|
|
86
178
|
type: 'failed',
|
|
87
179
|
};
|
|
@@ -114,6 +206,13 @@ export async function authorize(mode) {
|
|
|
114
206
|
};
|
|
115
207
|
}
|
|
116
208
|
export async function exchange(input, verifier, redirectUri, expectedState) {
|
|
209
|
+
if (!isBoundedUtf8(input, MAX_CALLBACK_INPUT_BYTES) ||
|
|
210
|
+
!isBoundedUtf8(verifier, MAX_VERIFIER_BYTES) ||
|
|
211
|
+
!isBoundedUtf8(redirectUri, MAX_REDIRECT_URI_BYTES) ||
|
|
212
|
+
(expectedState !== undefined &&
|
|
213
|
+
!isBoundedUtf8(expectedState, MAX_TOKEN_LENGTH))) {
|
|
214
|
+
return { type: 'failed' };
|
|
215
|
+
}
|
|
117
216
|
const callback = parseCallbackInput(input);
|
|
118
217
|
if (!callback) {
|
|
119
218
|
return {
|
|
@@ -125,73 +224,53 @@ export async function exchange(input, verifier, redirectUri, expectedState) {
|
|
|
125
224
|
type: 'failed',
|
|
126
225
|
};
|
|
127
226
|
}
|
|
227
|
+
if (!isBoundedUtf8(callback.code, MAX_TOKEN_LENGTH) ||
|
|
228
|
+
!isBoundedUtf8(callback.state, MAX_TOKEN_LENGTH)) {
|
|
229
|
+
return { type: 'failed' };
|
|
230
|
+
}
|
|
128
231
|
return exchangeCode(callback, verifier, redirectUri);
|
|
129
232
|
}
|
|
130
233
|
/**
|
|
131
234
|
* Exchange a refresh token for a new access/refresh token pair.
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
235
|
+
* Refresh tokens may rotate after a request reaches the provider. Retrying an
|
|
236
|
+
* ambiguous 5xx, timeout, network failure, or response-body failure can replay
|
|
237
|
+
* an already consumed token, so each call makes exactly one token request.
|
|
135
238
|
*/
|
|
136
239
|
export async function refreshToken(refreshTokenValue) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
if (!response.ok) {
|
|
160
|
-
if (response.status >= 500 && attempt < maxRetries) {
|
|
161
|
-
await response.body?.cancel();
|
|
162
|
-
continue;
|
|
163
|
-
}
|
|
164
|
-
await response.body?.cancel();
|
|
165
|
-
return { type: 'failed', status: response.status };
|
|
166
|
-
}
|
|
167
|
-
const tokens = await parseTokenResponse(response);
|
|
168
|
-
if (!tokens) {
|
|
169
|
-
return { type: 'failed', status: response.status };
|
|
170
|
-
}
|
|
171
|
-
return {
|
|
172
|
-
type: 'success',
|
|
173
|
-
...tokens,
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
catch (error) {
|
|
177
|
-
const isNetworkError = (typeof error === 'object' &&
|
|
178
|
-
error !== null &&
|
|
179
|
-
'name' in error &&
|
|
180
|
-
(error.name === 'TimeoutError' || error.name === 'AbortError')) ||
|
|
181
|
-
(error instanceof Error &&
|
|
182
|
-
(error.message.includes('fetch failed') ||
|
|
183
|
-
('code' in error &&
|
|
184
|
-
(error.code === 'ECONNRESET' ||
|
|
185
|
-
error.code === 'ECONNREFUSED' ||
|
|
186
|
-
error.code === 'ETIMEDOUT' ||
|
|
187
|
-
error.code === 'UND_ERR_CONNECT_TIMEOUT'))));
|
|
188
|
-
if (attempt < maxRetries && isNetworkError) {
|
|
189
|
-
continue;
|
|
190
|
-
}
|
|
191
|
-
throw error;
|
|
240
|
+
if (!isBoundedUtf8(refreshTokenValue, MAX_TOKEN_LENGTH)) {
|
|
241
|
+
return { type: 'failed', status: 400 };
|
|
242
|
+
}
|
|
243
|
+
try {
|
|
244
|
+
const response = await fetch(TOKEN_URL, {
|
|
245
|
+
method: 'POST',
|
|
246
|
+
signal: AbortSignal.timeout(TOKEN_TIMEOUT_MS),
|
|
247
|
+
redirect: 'error',
|
|
248
|
+
headers: {
|
|
249
|
+
'Content-Type': 'application/json',
|
|
250
|
+
Accept: 'application/json, text/plain, */*',
|
|
251
|
+
'User-Agent': 'axios/1.13.6',
|
|
252
|
+
},
|
|
253
|
+
body: JSON.stringify({
|
|
254
|
+
grant_type: 'refresh_token',
|
|
255
|
+
refresh_token: refreshTokenValue,
|
|
256
|
+
client_id: CLIENT_ID,
|
|
257
|
+
}),
|
|
258
|
+
});
|
|
259
|
+
if (!response.ok) {
|
|
260
|
+
await response.body?.cancel().catch(() => { });
|
|
261
|
+
return { type: 'failed', status: response.status };
|
|
192
262
|
}
|
|
263
|
+
const tokens = await parseTokenResponse(response);
|
|
264
|
+
if (!tokens)
|
|
265
|
+
return { type: 'failed', status: response.status };
|
|
266
|
+
return {
|
|
267
|
+
type: 'success',
|
|
268
|
+
...tokens,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
if (isTransientNetworkError(error))
|
|
273
|
+
return { type: 'failed', status: 0 };
|
|
274
|
+
throw error;
|
|
193
275
|
}
|
|
194
|
-
// Unreachable — each iteration either returns or throws.
|
|
195
|
-
// Kept as a TypeScript exhaustiveness guard.
|
|
196
|
-
throw new Error('Token refresh exhausted all retries');
|
|
197
276
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare class BodyLimitError extends Error {
|
|
2
|
+
constructor(label: string, limit: number);
|
|
3
|
+
}
|
|
4
|
+
export declare class InvalidUtf8Error extends Error {
|
|
5
|
+
constructor(label: string);
|
|
6
|
+
}
|
|
7
|
+
export declare function contentLength(headers: Headers): number | undefined;
|
|
8
|
+
export declare function readBoundedText(body: ReadableStream<Uint8Array> | null, limit: number, label: string): Promise<string>;
|
package/dist/bounded.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export class BodyLimitError extends Error {
|
|
2
|
+
constructor(label, limit) {
|
|
3
|
+
super(`${label} exceeds ${limit} byte limit`);
|
|
4
|
+
this.name = 'BodyLimitError';
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export class InvalidUtf8Error extends Error {
|
|
8
|
+
constructor(label) {
|
|
9
|
+
super(`${label} is not valid UTF-8`);
|
|
10
|
+
this.name = 'InvalidUtf8Error';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function contentLength(headers) {
|
|
14
|
+
const raw = headers.get('content-length');
|
|
15
|
+
if (!raw || !/^\d+$/.test(raw))
|
|
16
|
+
return undefined;
|
|
17
|
+
const value = Number(raw);
|
|
18
|
+
return Number.isSafeInteger(value) ? value : undefined;
|
|
19
|
+
}
|
|
20
|
+
export async function readBoundedText(body, limit, label) {
|
|
21
|
+
if (!body)
|
|
22
|
+
return '';
|
|
23
|
+
const reader = body.getReader();
|
|
24
|
+
const decoder = new TextDecoder('utf-8', { fatal: true });
|
|
25
|
+
const parts = [];
|
|
26
|
+
let total = 0;
|
|
27
|
+
try {
|
|
28
|
+
while (true) {
|
|
29
|
+
const { done, value } = await reader.read();
|
|
30
|
+
if (done)
|
|
31
|
+
break;
|
|
32
|
+
total += value.byteLength;
|
|
33
|
+
if (total > limit) {
|
|
34
|
+
await reader.cancel().catch(() => { });
|
|
35
|
+
throw new BodyLimitError(label, limit);
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
parts.push(decoder.decode(value, { stream: true }));
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
throw new InvalidUtf8Error(label);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
parts.push(decoder.decode());
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
throw new InvalidUtf8Error(label);
|
|
49
|
+
}
|
|
50
|
+
return parts.join('');
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
await reader.cancel(error).catch(() => { });
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
reader.releaseLock();
|
|
58
|
+
}
|
|
59
|
+
}
|
package/dist/config.d.ts
CHANGED
|
@@ -7,6 +7,16 @@
|
|
|
7
7
|
* waiting for a published bump.
|
|
8
8
|
*/
|
|
9
9
|
export declare const ANTHROPIC_CLAUDE_CODE_VERSION_ENV_VAR = "ANTHROPIC_CLAUDE_CODE_VERSION";
|
|
10
|
+
export declare function isValidClaudeCodeVersion(candidate: string): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Compare two validated Claude Code releases.
|
|
13
|
+
*
|
|
14
|
+
* Components are compared numerically rather than lexically — `2.1.99` sorts
|
|
15
|
+
* after `2.1.280` as a string but is the older release — and as `BigInt`, so a
|
|
16
|
+
* large bounded component cannot silently lose precision the way `Number`
|
|
17
|
+
* would. Invalid input has no ordering and returns `undefined`.
|
|
18
|
+
*/
|
|
19
|
+
export declare function compareClaudeCodeVersions(candidate: string, baseline: string): -1 | 0 | 1 | undefined;
|
|
10
20
|
/**
|
|
11
21
|
* Outcome of reading the version override.
|
|
12
22
|
*
|
package/dist/config.js
CHANGED
|
@@ -11,19 +11,27 @@ export const ANTHROPIC_CLAUDE_CODE_VERSION_ENV_VAR = 'ANTHROPIC_CLAUDE_CODE_VERS
|
|
|
11
11
|
/**
|
|
12
12
|
* Claude Code releases are `major.minor.patch` with numeric components.
|
|
13
13
|
*
|
|
14
|
-
* Leading zeros are rejected: `02.1.
|
|
14
|
+
* Leading zeros are rejected: `02.1.280` is not a release Anthropic publishes,
|
|
15
15
|
* so accepting it would report a version string no server-side gate expects.
|
|
16
16
|
*/
|
|
17
17
|
const VERSION_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
18
|
+
const MAX_VERSION_LENGTH = 64;
|
|
19
|
+
export function isValidClaudeCodeVersion(candidate) {
|
|
20
|
+
return (candidate.length <= MAX_VERSION_LENGTH && VERSION_PATTERN.test(candidate));
|
|
21
|
+
}
|
|
18
22
|
/**
|
|
19
|
-
*
|
|
23
|
+
* Compare two validated Claude Code releases.
|
|
20
24
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
+
* Components are compared numerically rather than lexically — `2.1.99` sorts
|
|
26
|
+
* after `2.1.280` as a string but is the older release — and as `BigInt`, so a
|
|
27
|
+
* large bounded component cannot silently lose precision the way `Number`
|
|
28
|
+
* would. Invalid input has no ordering and returns `undefined`.
|
|
25
29
|
*/
|
|
26
|
-
function
|
|
30
|
+
export function compareClaudeCodeVersions(candidate, baseline) {
|
|
31
|
+
if (!isValidClaudeCodeVersion(candidate) ||
|
|
32
|
+
!isValidClaudeCodeVersion(baseline)) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
27
35
|
// The `0n` defaults are unreachable — `VERSION_PATTERN` guarantees exactly
|
|
28
36
|
// three components — but they keep the destructuring free of assertions.
|
|
29
37
|
const [major = 0n, minor = 0n, patch = 0n] = candidate
|
|
@@ -33,10 +41,12 @@ function isOlderVersion(candidate, baseline) {
|
|
|
33
41
|
.split('.')
|
|
34
42
|
.map((part) => BigInt(part));
|
|
35
43
|
if (major !== baseMajor)
|
|
36
|
-
return major < baseMajor;
|
|
44
|
+
return major < baseMajor ? -1 : 1;
|
|
37
45
|
if (minor !== baseMinor)
|
|
38
|
-
return minor < baseMinor;
|
|
39
|
-
|
|
46
|
+
return minor < baseMinor ? -1 : 1;
|
|
47
|
+
if (patch !== basePatch)
|
|
48
|
+
return patch < basePatch ? -1 : 1;
|
|
49
|
+
return 0;
|
|
40
50
|
}
|
|
41
51
|
/**
|
|
42
52
|
* Resolve the Claude Code version to report to Anthropic.
|
|
@@ -52,17 +62,17 @@ export function resolveClaudeCodeVersion(raw = process.env[ANTHROPIC_CLAUDE_CODE
|
|
|
52
62
|
if (raw === undefined) {
|
|
53
63
|
return { type: 'success', version: CLAUDE_CODE_VERSION };
|
|
54
64
|
}
|
|
55
|
-
const trimmed = raw.trim();
|
|
56
|
-
if (!
|
|
65
|
+
const trimmed = raw.length <= MAX_VERSION_LENGTH ? raw.trim() : '';
|
|
66
|
+
if (!isValidClaudeCodeVersion(trimmed)) {
|
|
57
67
|
return {
|
|
58
68
|
type: 'invalid',
|
|
59
|
-
error: `${ANTHROPIC_CLAUDE_CODE_VERSION_ENV_VAR} is
|
|
60
|
-
`
|
|
69
|
+
error: `${ANTHROPIC_CLAUDE_CODE_VERSION_ENV_VAR} is not a valid Claude Code version. ` +
|
|
70
|
+
`Expected major.minor.patch (for example, ${CLAUDE_CODE_VERSION}). ` +
|
|
61
71
|
`Reporting the bundled version ${CLAUDE_CODE_VERSION} instead; correct or unset ` +
|
|
62
72
|
`${ANTHROPIC_CLAUDE_CODE_VERSION_ENV_VAR} and restart OpenCode to use the override.`,
|
|
63
73
|
};
|
|
64
74
|
}
|
|
65
|
-
if (
|
|
75
|
+
if (compareClaudeCodeVersions(trimmed, CLAUDE_CODE_VERSION) === -1) {
|
|
66
76
|
return {
|
|
67
77
|
type: 'outdated',
|
|
68
78
|
version: trimmed,
|
package/dist/constants.d.ts
CHANGED
|
@@ -22,7 +22,7 @@ export declare const CCH_POSITIONS: number[];
|
|
|
22
22
|
* newer is required"). Keep this at or above the latest published
|
|
23
23
|
* `@anthropic-ai/claude-code` release, otherwise new models are unreachable.
|
|
24
24
|
*/
|
|
25
|
-
export declare const CLAUDE_CODE_VERSION = "2.1.
|
|
25
|
+
export declare const CLAUDE_CODE_VERSION = "2.1.280";
|
|
26
26
|
export declare const CLAUDE_CODE_ENTRYPOINT = "sdk-cli";
|
|
27
27
|
/**
|
|
28
28
|
* Build the `user-agent` value for a reported Claude Code version.
|
package/dist/constants.js
CHANGED
|
@@ -32,7 +32,7 @@ export const CCH_POSITIONS = [4, 7, 20];
|
|
|
32
32
|
* newer is required"). Keep this at or above the latest published
|
|
33
33
|
* `@anthropic-ai/claude-code` release, otherwise new models are unreachable.
|
|
34
34
|
*/
|
|
35
|
-
export const CLAUDE_CODE_VERSION = '2.1.
|
|
35
|
+
export const CLAUDE_CODE_VERSION = '2.1.280';
|
|
36
36
|
export const CLAUDE_CODE_ENTRYPOINT = 'sdk-cli';
|
|
37
37
|
/**
|
|
38
38
|
* Build the `user-agent` value for a reported Claude Code version.
|
package/dist/index.d.ts
CHANGED