@ex-machina/opencode-anthropic-auth 1.8.4 → 2.0.0-next.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.
- package/dist/auth.d.ts +16 -0
- package/dist/auth.js +108 -4
- package/dist/config.d.ts +7 -7
- package/dist/config.js +9 -4
- package/dist/index.d.ts +3 -2
- package/dist/index.js +152 -200
- package/package.json +7 -5
package/dist/auth.d.ts
CHANGED
|
@@ -14,3 +14,19 @@ export type ExchangeResult = {
|
|
|
14
14
|
type: 'failed';
|
|
15
15
|
};
|
|
16
16
|
export declare function exchange(input: string, verifier: string, redirectUri: string, expectedState?: string): Promise<ExchangeResult>;
|
|
17
|
+
export type RefreshResult = {
|
|
18
|
+
type: 'success';
|
|
19
|
+
refresh: string;
|
|
20
|
+
access: string;
|
|
21
|
+
expires: number;
|
|
22
|
+
} | {
|
|
23
|
+
type: 'failed';
|
|
24
|
+
status: number;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Exchange a refresh token for a new access/refresh token pair.
|
|
28
|
+
* Retries transient (5xx, network) failures with exponential backoff;
|
|
29
|
+
* non-transient failures (e.g. 403 on a revoked/rotated-away token)
|
|
30
|
+
* are returned immediately as `{ type: 'failed' }`.
|
|
31
|
+
*/
|
|
32
|
+
export declare function refreshToken(refreshTokenValue: string): Promise<RefreshResult>;
|
package/dist/auth.js
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
import { AUTHORIZE_URLS, CLIENT_ID, CODE_CALLBACK_URL, OAUTH_SCOPES, TOKEN_URL, } from "./constants.js";
|
|
2
2
|
import { generatePKCE } from "./pkce.js";
|
|
3
|
+
const REFRESH_TIMEOUT_MS = 30_000;
|
|
4
|
+
function isTokenResponse(value) {
|
|
5
|
+
if (typeof value !== 'object' || value === null)
|
|
6
|
+
return false;
|
|
7
|
+
if (!('refresh_token' in value) || !('access_token' in value))
|
|
8
|
+
return false;
|
|
9
|
+
if (!('expires_in' in value))
|
|
10
|
+
return false;
|
|
11
|
+
return (typeof value.refresh_token === 'string' &&
|
|
12
|
+
value.refresh_token.length > 0 &&
|
|
13
|
+
typeof value.access_token === 'string' &&
|
|
14
|
+
value.access_token.length > 0 &&
|
|
15
|
+
typeof value.expires_in === 'number' &&
|
|
16
|
+
Number.isSafeInteger(value.expires_in) &&
|
|
17
|
+
value.expires_in > 0);
|
|
18
|
+
}
|
|
19
|
+
async function parseTokenResponse(response) {
|
|
20
|
+
try {
|
|
21
|
+
const value = await response.json();
|
|
22
|
+
if (!isTokenResponse(value))
|
|
23
|
+
return undefined;
|
|
24
|
+
const expires = Date.now() + value.expires_in * 1000;
|
|
25
|
+
if (!Number.isSafeInteger(expires))
|
|
26
|
+
return undefined;
|
|
27
|
+
return {
|
|
28
|
+
refresh: value.refresh_token,
|
|
29
|
+
access: value.access_token,
|
|
30
|
+
expires,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (error instanceof SyntaxError)
|
|
35
|
+
return undefined;
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
3
39
|
function generateState() {
|
|
4
40
|
return crypto.randomUUID().replace(/-/g, '');
|
|
5
41
|
}
|
|
@@ -50,12 +86,12 @@ async function exchangeCode(callback, verifier, redirectUri) {
|
|
|
50
86
|
type: 'failed',
|
|
51
87
|
};
|
|
52
88
|
}
|
|
53
|
-
const
|
|
89
|
+
const tokens = await parseTokenResponse(result);
|
|
90
|
+
if (!tokens)
|
|
91
|
+
return { type: 'failed' };
|
|
54
92
|
return {
|
|
55
93
|
type: 'success',
|
|
56
|
-
|
|
57
|
-
access: json.access_token,
|
|
58
|
-
expires: Date.now() + json.expires_in * 1000,
|
|
94
|
+
...tokens,
|
|
59
95
|
};
|
|
60
96
|
}
|
|
61
97
|
export async function authorize(mode) {
|
|
@@ -91,3 +127,71 @@ export async function exchange(input, verifier, redirectUri, expectedState) {
|
|
|
91
127
|
}
|
|
92
128
|
return exchangeCode(callback, verifier, redirectUri);
|
|
93
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* Exchange a refresh token for a new access/refresh token pair.
|
|
132
|
+
* Retries transient (5xx, network) failures with exponential backoff;
|
|
133
|
+
* non-transient failures (e.g. 403 on a revoked/rotated-away token)
|
|
134
|
+
* are returned immediately as `{ type: 'failed' }`.
|
|
135
|
+
*/
|
|
136
|
+
export async function refreshToken(refreshTokenValue) {
|
|
137
|
+
const maxRetries = 2;
|
|
138
|
+
const baseDelayMs = 500;
|
|
139
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
140
|
+
try {
|
|
141
|
+
if (attempt > 0) {
|
|
142
|
+
const delay = baseDelayMs * 2 ** (attempt - 1);
|
|
143
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
144
|
+
}
|
|
145
|
+
const response = await fetch(TOKEN_URL, {
|
|
146
|
+
method: 'POST',
|
|
147
|
+
signal: AbortSignal.timeout(REFRESH_TIMEOUT_MS),
|
|
148
|
+
headers: {
|
|
149
|
+
'Content-Type': 'application/json',
|
|
150
|
+
Accept: 'application/json, text/plain, */*',
|
|
151
|
+
'User-Agent': 'axios/1.13.6',
|
|
152
|
+
},
|
|
153
|
+
body: JSON.stringify({
|
|
154
|
+
grant_type: 'refresh_token',
|
|
155
|
+
refresh_token: refreshTokenValue,
|
|
156
|
+
client_id: CLIENT_ID,
|
|
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;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
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
|
+
}
|
package/dist/config.d.ts
CHANGED
|
@@ -16,15 +16,15 @@ export declare const ANTHROPIC_CLAUDE_CODE_VERSION_ENV_VAR = "ANTHROPIC_CLAUDE_C
|
|
|
16
16
|
* it with the warning explaining why reporting it is risky.
|
|
17
17
|
*/
|
|
18
18
|
export type ClaudeCodeVersionResolution = {
|
|
19
|
-
type: 'success';
|
|
20
|
-
version: string;
|
|
19
|
+
readonly type: 'success';
|
|
20
|
+
readonly version: string;
|
|
21
21
|
} | {
|
|
22
|
-
type: 'outdated';
|
|
23
|
-
version: string;
|
|
24
|
-
warning: string;
|
|
22
|
+
readonly type: 'outdated';
|
|
23
|
+
readonly version: string;
|
|
24
|
+
readonly warning: string;
|
|
25
25
|
} | {
|
|
26
|
-
type: 'invalid';
|
|
27
|
-
error: string;
|
|
26
|
+
readonly type: 'invalid';
|
|
27
|
+
readonly error: string;
|
|
28
28
|
};
|
|
29
29
|
/**
|
|
30
30
|
* Resolve the Claude Code version to report to Anthropic.
|
package/dist/config.js
CHANGED
|
@@ -8,8 +8,13 @@ import { CLAUDE_CODE_VERSION } from "./constants.js";
|
|
|
8
8
|
* waiting for a published bump.
|
|
9
9
|
*/
|
|
10
10
|
export const ANTHROPIC_CLAUDE_CODE_VERSION_ENV_VAR = 'ANTHROPIC_CLAUDE_CODE_VERSION';
|
|
11
|
-
/**
|
|
12
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Claude Code releases are `major.minor.patch` with numeric components.
|
|
13
|
+
*
|
|
14
|
+
* Leading zeros are rejected: `02.1.258` is not a release Anthropic publishes,
|
|
15
|
+
* so accepting it would report a version string no server-side gate expects.
|
|
16
|
+
*/
|
|
17
|
+
const VERSION_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
13
18
|
/**
|
|
14
19
|
* Is `candidate` an older Claude Code release than `baseline`?
|
|
15
20
|
*
|
|
@@ -52,8 +57,8 @@ export function resolveClaudeCodeVersion(raw = process.env[ANTHROPIC_CLAUDE_CODE
|
|
|
52
57
|
return {
|
|
53
58
|
type: 'invalid',
|
|
54
59
|
error: `${ANTHROPIC_CLAUDE_CODE_VERSION_ENV_VAR} is set to ${JSON.stringify(raw)}, which is not a ` +
|
|
55
|
-
`Claude Code version. Expected major.minor.patch (
|
|
56
|
-
`Reporting the bundled version ${CLAUDE_CODE_VERSION} instead
|
|
60
|
+
`Claude Code version. Expected major.minor.patch (for example, ${CLAUDE_CODE_VERSION}). ` +
|
|
61
|
+
`Reporting the bundled version ${CLAUDE_CODE_VERSION} instead; correct or unset ` +
|
|
57
62
|
`${ANTHROPIC_CLAUDE_CODE_VERSION_ENV_VAR} and restart OpenCode to use the override.`,
|
|
58
63
|
};
|
|
59
64
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import { Plugin } from '@opencode-ai/plugin';
|
|
2
|
+
declare const _default: Plugin.Plugin;
|
|
3
|
+
export default _default;
|
package/dist/index.js
CHANGED
|
@@ -1,210 +1,162 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Plugin } from '@opencode-ai/plugin';
|
|
2
|
+
import { authorize, exchange, refreshToken } from "./auth.js";
|
|
2
3
|
import { resolveClaudeCodeVersion } from "./config.js";
|
|
3
|
-
import { CLAUDE_CODE_VERSION,
|
|
4
|
+
import { CLAUDE_CODE_VERSION, REQUIRED_BETAS } from "./constants.js";
|
|
4
5
|
import { createStrippedStream, isInsecure, mergeHeaders, rewriteRequestBody, rewriteUrl, setOAuthHeaders, } from "./transform.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
},
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
/* Logging is best-effort; the resolved version still applies. */
|
|
25
|
-
}
|
|
6
|
+
const PLUGIN_ID = 'ex-machina.anthropic-auth';
|
|
7
|
+
const INTEGRATION_ID = 'anthropic';
|
|
8
|
+
const REFRESH_CACHE_GRACE_MS = 30_000;
|
|
9
|
+
// `methodID` is a branded `Integration.MethodID` at the type level (a
|
|
10
|
+
// compile-time-only tag — there's no runtime representation), so a plain
|
|
11
|
+
// string literal needs a cast to satisfy the branded field.
|
|
12
|
+
const METHOD_ID = 'claude-max';
|
|
13
|
+
function toCredential(exchanged) {
|
|
14
|
+
return {
|
|
15
|
+
type: 'oauth',
|
|
16
|
+
methodID: METHOD_ID,
|
|
17
|
+
refresh: exchanged.refresh,
|
|
18
|
+
access: exchanged.access,
|
|
19
|
+
expires: exchanged.expires,
|
|
20
|
+
};
|
|
26
21
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
else if (resolution.type === 'outdated') {
|
|
35
|
-
await logVersionOverrideIssue(client, 'warn', resolution.warning);
|
|
22
|
+
async function resolveActiveOAuth(ctx) {
|
|
23
|
+
const connection = await ctx.integration.connection.active(INTEGRATION_ID);
|
|
24
|
+
if (!connection)
|
|
25
|
+
return undefined;
|
|
26
|
+
const credential = await ctx.integration.connection.resolve(connection);
|
|
27
|
+
if (credential?.type === 'oauth' && credential.methodID === METHOD_ID) {
|
|
28
|
+
return credential;
|
|
36
29
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
function warnIfInsecureUnsupported() {
|
|
33
|
+
if (!isInsecure())
|
|
34
|
+
return;
|
|
35
|
+
console.warn('[ex-machina.anthropic-auth] ANTHROPIC_INSECURE is set, but OpenCode v2 ' +
|
|
36
|
+
'plugin request hooks cannot disable TLS verification for a custom ' +
|
|
37
|
+
'ANTHROPIC_BASE_URL endpoint. TLS verification remains enabled — ' +
|
|
38
|
+
'requests to an untrusted/self-signed endpoint will fail.');
|
|
39
|
+
}
|
|
40
|
+
function isTransformedOAuthRequest(request) {
|
|
41
|
+
const betas = new Set((request.headers.get('anthropic-beta') ?? '')
|
|
42
|
+
.split(',')
|
|
43
|
+
.map((beta) => beta.trim()));
|
|
44
|
+
return (request.headers.get('authorization')?.startsWith('Bearer ') === true &&
|
|
45
|
+
REQUIRED_BETAS.every((beta) => betas.has(beta)) &&
|
|
46
|
+
new URL(request.url).searchParams.get('beta') === 'true');
|
|
47
|
+
}
|
|
48
|
+
export default Plugin.define({
|
|
49
|
+
id: PLUGIN_ID,
|
|
50
|
+
setup: async (ctx) => {
|
|
51
|
+
warnIfInsecureUnsupported();
|
|
52
|
+
// Resolved once per plugin instance so every request reports the same
|
|
53
|
+
// version in both the user-agent and the billing header.
|
|
54
|
+
const versionResolution = resolveClaudeCodeVersion();
|
|
55
|
+
if (versionResolution.type === 'invalid') {
|
|
56
|
+
console.error(`[ex-machina.anthropic-auth] ${versionResolution.error}`);
|
|
57
|
+
}
|
|
58
|
+
else if (versionResolution.type === 'outdated') {
|
|
59
|
+
console.warn(`[ex-machina.anthropic-auth] ${versionResolution.warning}`);
|
|
60
|
+
}
|
|
61
|
+
// Only a malformed override lacks a usable version; an outdated one was
|
|
62
|
+
// set deliberately, so it is reported as configured.
|
|
63
|
+
const claudeCodeVersion = versionResolution.type === 'invalid'
|
|
64
|
+
? CLAUDE_CODE_VERSION
|
|
65
|
+
: versionResolution.version;
|
|
66
|
+
// Retain successful refreshes for this plugin generation so a host call
|
|
67
|
+
// holding the rotated token cannot submit it again before persistence.
|
|
68
|
+
const refreshInFlight = new Map();
|
|
69
|
+
const refreshCredential = async (credential) => {
|
|
70
|
+
const existing = refreshInFlight.get(credential.refresh);
|
|
71
|
+
if (existing)
|
|
72
|
+
return existing;
|
|
73
|
+
const pending = (async () => {
|
|
74
|
+
const result = await refreshToken(credential.refresh);
|
|
75
|
+
if (result.type === 'failed') {
|
|
76
|
+
throw new Error(`Anthropic token refresh failed: ${result.status}`);
|
|
77
|
+
}
|
|
78
|
+
return toCredential(result);
|
|
79
|
+
})();
|
|
80
|
+
refreshInFlight.set(credential.refresh, pending);
|
|
81
|
+
try {
|
|
82
|
+
const rotated = await pending;
|
|
83
|
+
const timer = setTimeout(() => {
|
|
84
|
+
if (refreshInFlight.get(credential.refresh) === pending) {
|
|
85
|
+
refreshInFlight.delete(credential.refresh);
|
|
56
86
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
87
|
+
}, REFRESH_CACHE_GRACE_MS);
|
|
88
|
+
timer.unref?.();
|
|
89
|
+
return rotated;
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
if (refreshInFlight.get(credential.refresh) === pending) {
|
|
93
|
+
refreshInFlight.delete(credential.refresh);
|
|
94
|
+
}
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
await ctx.integration.transform((draft) => {
|
|
99
|
+
draft.method.update({
|
|
100
|
+
integrationID: INTEGRATION_ID,
|
|
101
|
+
method: {
|
|
102
|
+
id: METHOD_ID,
|
|
103
|
+
type: 'oauth',
|
|
104
|
+
label: 'Claude Pro/Max',
|
|
105
|
+
},
|
|
106
|
+
authorize: async () => {
|
|
107
|
+
const result = await authorize('max');
|
|
60
108
|
return {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const maxRetries = 2;
|
|
70
|
-
const baseDelayMs = 500;
|
|
71
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
72
|
-
try {
|
|
73
|
-
if (attempt > 0) {
|
|
74
|
-
const delay = baseDelayMs * 2 ** (attempt - 1);
|
|
75
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
76
|
-
}
|
|
77
|
-
// Re-read auth to get the latest refresh token.
|
|
78
|
-
// The outer `auth` snapshot may be stale if tokens
|
|
79
|
-
// were rotated since the fetch() call was made.
|
|
80
|
-
const freshAuth = await getAuth();
|
|
81
|
-
const response = await fetch(TOKEN_URL, {
|
|
82
|
-
method: 'POST',
|
|
83
|
-
headers: {
|
|
84
|
-
'Content-Type': 'application/json',
|
|
85
|
-
Accept: 'application/json, text/plain, */*',
|
|
86
|
-
'User-Agent': 'axios/1.13.6',
|
|
87
|
-
},
|
|
88
|
-
body: JSON.stringify({
|
|
89
|
-
grant_type: 'refresh_token',
|
|
90
|
-
refresh_token: freshAuth.refresh,
|
|
91
|
-
client_id: CLIENT_ID,
|
|
92
|
-
}),
|
|
93
|
-
});
|
|
94
|
-
if (!response.ok) {
|
|
95
|
-
if (response.status >= 500 && attempt < maxRetries) {
|
|
96
|
-
await response.body?.cancel();
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
const body = await response.text().catch(() => '');
|
|
100
|
-
throw new Error(`Token refresh failed: ${response.status} — ${body}`);
|
|
101
|
-
}
|
|
102
|
-
const json = (await response.json());
|
|
103
|
-
// biome-ignore lint/suspicious/noExplicitAny: SDK types don't expose auth.set
|
|
104
|
-
await client.auth.set({
|
|
105
|
-
path: {
|
|
106
|
-
id: 'anthropic',
|
|
107
|
-
},
|
|
108
|
-
body: {
|
|
109
|
-
type: 'oauth',
|
|
110
|
-
refresh: json.refresh_token,
|
|
111
|
-
access: json.access_token,
|
|
112
|
-
expires: Date.now() + json.expires_in * 1000,
|
|
113
|
-
},
|
|
114
|
-
});
|
|
115
|
-
return json.access_token;
|
|
116
|
-
}
|
|
117
|
-
catch (error) {
|
|
118
|
-
const isNetworkError = error instanceof Error &&
|
|
119
|
-
(error.message.includes('fetch failed') ||
|
|
120
|
-
('code' in error &&
|
|
121
|
-
(error.code === 'ECONNRESET' ||
|
|
122
|
-
error.code === 'ECONNREFUSED' ||
|
|
123
|
-
error.code === 'ETIMEDOUT' ||
|
|
124
|
-
error.code === 'UND_ERR_CONNECT_TIMEOUT')));
|
|
125
|
-
if (attempt < maxRetries && isNetworkError) {
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
throw error;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
// Unreachable — each iteration either returns or throws.
|
|
132
|
-
// Kept as a TypeScript exhaustiveness guard.
|
|
133
|
-
throw new Error('Token refresh exhausted all retries');
|
|
134
|
-
})().finally(() => {
|
|
135
|
-
refreshPromise = null;
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
auth.access = await refreshPromise;
|
|
109
|
+
url: result.url,
|
|
110
|
+
instructions: 'Paste the authorization code here:',
|
|
111
|
+
mode: 'code',
|
|
112
|
+
callback: async (code) => {
|
|
113
|
+
const exchanged = await exchange(code, result.verifier, result.redirectUri, result.state);
|
|
114
|
+
if (exchanged.type === 'failed') {
|
|
115
|
+
throw new Error('Failed to exchange the Claude Pro/Max authorization code. ' +
|
|
116
|
+
'Double-check that you pasted the full code and try again.');
|
|
139
117
|
}
|
|
140
|
-
|
|
141
|
-
// biome-ignore lint/style/noNonNullAssertion: access is guaranteed set above
|
|
142
|
-
setOAuthHeaders(requestHeaders, auth.access, claudeCodeVersion);
|
|
143
|
-
let body = init?.body;
|
|
144
|
-
if (body && typeof body === 'string') {
|
|
145
|
-
body = rewriteRequestBody(body, claudeCodeVersion);
|
|
146
|
-
}
|
|
147
|
-
const rewritten = rewriteUrl(input);
|
|
148
|
-
const response = await fetch(rewritten.input, {
|
|
149
|
-
...init,
|
|
150
|
-
body,
|
|
151
|
-
headers: requestHeaders,
|
|
152
|
-
...(isInsecure() && { tls: { rejectUnauthorized: false } }),
|
|
153
|
-
});
|
|
154
|
-
return createStrippedStream(response);
|
|
118
|
+
return toCredential(exchanged);
|
|
155
119
|
},
|
|
156
120
|
};
|
|
157
|
-
}
|
|
158
|
-
return {};
|
|
159
|
-
},
|
|
160
|
-
methods: [
|
|
161
|
-
{
|
|
162
|
-
label: 'Claude Pro/Max',
|
|
163
|
-
type: 'oauth',
|
|
164
|
-
authorize: async () => {
|
|
165
|
-
const result = await authorize('max');
|
|
166
|
-
return {
|
|
167
|
-
url: result.url,
|
|
168
|
-
instructions: 'Paste the authorization code here:',
|
|
169
|
-
method: 'code',
|
|
170
|
-
callback: async (code) => {
|
|
171
|
-
return exchange(code, result.verifier, result.redirectUri, result.state);
|
|
172
|
-
},
|
|
173
|
-
};
|
|
174
|
-
},
|
|
175
121
|
},
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
122
|
+
refresh: refreshCredential,
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
await ctx.session.hook('http.request', async (event) => {
|
|
126
|
+
if (event.model.providerID !== INTEGRATION_ID)
|
|
127
|
+
return;
|
|
128
|
+
const credential = await resolveActiveOAuth(ctx);
|
|
129
|
+
if (!credential)
|
|
130
|
+
return;
|
|
131
|
+
const request = event.request;
|
|
132
|
+
const hasBody = request.method !== 'GET' && request.method !== 'HEAD';
|
|
133
|
+
const bodyText = hasBody ? await request.clone().text() : undefined;
|
|
134
|
+
const rewrittenBody = bodyText !== undefined
|
|
135
|
+
? rewriteRequestBody(bodyText, claudeCodeVersion)
|
|
136
|
+
: undefined;
|
|
137
|
+
const headers = mergeHeaders(request);
|
|
138
|
+
setOAuthHeaders(headers, credential.access, claudeCodeVersion);
|
|
139
|
+
if (rewrittenBody !== undefined)
|
|
140
|
+
headers.delete('content-length');
|
|
141
|
+
const { input: rewrittenInput } = rewriteUrl(request.url);
|
|
142
|
+
const url = typeof rewrittenInput === 'string'
|
|
143
|
+
? rewrittenInput
|
|
144
|
+
: rewrittenInput instanceof Request
|
|
145
|
+
? rewrittenInput.url
|
|
146
|
+
: rewrittenInput.toString();
|
|
147
|
+
event.request = new Request(url, {
|
|
148
|
+
method: request.method,
|
|
149
|
+
headers,
|
|
150
|
+
body: rewrittenBody,
|
|
151
|
+
signal: request.signal,
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
await ctx.session.hook('http.response', (event) => {
|
|
155
|
+
if (event.model.providerID !== INTEGRATION_ID)
|
|
156
|
+
return;
|
|
157
|
+
if (!isTransformedOAuthRequest(event.request))
|
|
158
|
+
return;
|
|
159
|
+
event.response = createStrippedStream(event.response);
|
|
160
|
+
});
|
|
161
|
+
},
|
|
162
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ex-machina/opencode-anthropic-auth",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0-next.1",
|
|
4
|
+
"type": "module",
|
|
4
5
|
"repository": {
|
|
5
6
|
"type": "git",
|
|
6
7
|
"url": "https://github.com/ex-machina-co/opencode-anthropic-auth"
|
|
@@ -23,20 +24,21 @@
|
|
|
23
24
|
"check": "bun turbo check:all",
|
|
24
25
|
"test": "bun test",
|
|
25
26
|
"types": "tsc",
|
|
27
|
+
"check:package": "bun run build && bun scripts/check-package.ts",
|
|
26
28
|
"format": "biome check --write --unsafe",
|
|
27
29
|
"format:check": "biome format .",
|
|
28
30
|
"lint": "biome lint --error-on-warnings .",
|
|
29
31
|
"change": "changeset",
|
|
30
|
-
"release": "bun run build && bun change publish"
|
|
32
|
+
"release": "bun run build && bun change publish",
|
|
33
|
+
"release:next": "bun scripts/validate-next-release.ts && bun run release"
|
|
31
34
|
},
|
|
32
|
-
"
|
|
33
|
-
"@opencode-ai/plugin": "
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@opencode-ai/plugin": "0.0.0-next-17444"
|
|
34
37
|
},
|
|
35
38
|
"devDependencies": {
|
|
36
39
|
"@biomejs/biome": "2.5.2",
|
|
37
40
|
"@changesets/changelog-github": "^0.7.0",
|
|
38
41
|
"@changesets/cli": "^2.31.0",
|
|
39
|
-
"@opencode-ai/plugin": "1.17.13",
|
|
40
42
|
"@tsconfig/bun": "1.0.10",
|
|
41
43
|
"@types/bun": "1.3.14",
|
|
42
44
|
"dedent": "^1.7.2",
|