@ex-machina/opencode-anthropic-auth 0.2.1 → 1.0.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 CHANGED
@@ -1,8 +1,8 @@
1
- type AuthorizationResult = {
1
+ export type AuthorizationResult = {
2
2
  url: string;
3
3
  redirectUri: string;
4
4
  state: string;
5
- callback: () => Promise<ExchangeResult>;
5
+ verifier: string;
6
6
  };
7
7
  export declare function authorize(mode: 'max' | 'console'): Promise<AuthorizationResult>;
8
8
  export type ExchangeResult = {
@@ -14,4 +14,3 @@ 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 {};
package/dist/auth.js CHANGED
@@ -1,26 +1,5 @@
1
- import { createServer } from 'node:http';
2
- import { generatePKCE } from '@openauthjs/openauth/pkce';
3
- import { AUTHORIZE_URLS, CLIENT_ID, OAUTH_SCOPES, TOKEN_URL } from './constants';
4
- const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000;
5
- async function listen(server) {
6
- await new Promise((resolve, reject) => {
7
- server.once('error', reject);
8
- server.listen(0, '127.0.0.1', () => resolve());
9
- });
10
- }
11
- async function close(server) {
12
- if (!server.listening)
13
- return;
14
- await new Promise((resolve, reject) => {
15
- server.close((error) => {
16
- if (error) {
17
- reject(error);
18
- return;
19
- }
20
- resolve();
21
- });
22
- });
23
- }
1
+ import { AUTHORIZE_URLS, CLIENT_ID, CODE_CALLBACK_URL, OAUTH_SCOPES, TOKEN_URL, } from './constants';
2
+ import { generatePKCE } from './pkce';
24
3
  function generateState() {
25
4
  return crypto.randomUUID().replace(/-/g, '');
26
5
  }
@@ -49,19 +28,6 @@ function parseCallbackInput(input) {
49
28
  }
50
29
  return null;
51
30
  }
52
- function successPage() {
53
- return `<!doctype html>
54
- <html>
55
- <head>
56
- <meta charset="utf-8" />
57
- <title>Authorization complete</title>
58
- </head>
59
- <body>
60
- <h1>Authorization complete</h1>
61
- <p>You can close this window and return to OpenCode.</p>
62
- </body>
63
- </html>`;
64
- }
65
31
  async function exchangeCode(callback, verifier, redirectUri) {
66
32
  const result = await fetch(TOKEN_URL, {
67
33
  method: 'POST',
@@ -92,98 +58,23 @@ async function exchangeCode(callback, verifier, redirectUri) {
92
58
  expires: Date.now() + json.expires_in * 1000,
93
59
  };
94
60
  }
95
- async function createCallbackServer(expectedState) {
96
- let settled = false;
97
- let cleanupTimer;
98
- let resolveResult;
99
- let rejectResult;
100
- const server = createServer((req, res) => {
101
- const requestUrl = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
102
- if (requestUrl.pathname !== '/callback') {
103
- res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
104
- res.end('Not found');
105
- return;
106
- }
107
- const code = requestUrl.searchParams.get('code');
108
- const state = requestUrl.searchParams.get('state');
109
- if (!code || !state) {
110
- res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' });
111
- res.end('Missing code or state');
112
- return;
113
- }
114
- if (state !== expectedState) {
115
- res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' });
116
- res.end('Invalid state');
117
- if (!settled) {
118
- settled = true;
119
- rejectResult?.(new Error('OAuth state mismatch'));
120
- }
121
- return;
122
- }
123
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
124
- res.end(successPage());
125
- if (!settled) {
126
- settled = true;
127
- resolveResult?.(requestUrl.toString());
128
- }
129
- });
130
- const callbackUrl = new Promise((resolve, reject) => {
131
- resolveResult = resolve;
132
- rejectResult = reject;
133
- });
134
- await listen(server);
135
- cleanupTimer = setTimeout(() => {
136
- if (settled)
137
- return;
138
- settled = true;
139
- rejectResult?.(new Error('Timed out waiting for OAuth callback'));
140
- }, CALLBACK_TIMEOUT_MS);
141
- const address = server.address();
142
- if (!address || typeof address === 'string') {
143
- clearTimeout(cleanupTimer);
144
- await close(server);
145
- throw new Error('Failed to allocate localhost redirect port');
146
- }
147
- return {
148
- redirectUri: `http://localhost:${address.port}/callback`,
149
- waitForCallback: async () => {
150
- try {
151
- return await callbackUrl;
152
- }
153
- finally {
154
- if (cleanupTimer)
155
- clearTimeout(cleanupTimer);
156
- await close(server);
157
- }
158
- },
159
- };
160
- }
161
61
  export async function authorize(mode) {
162
62
  const pkce = await generatePKCE();
163
63
  const state = generateState();
164
- const callbackServer = await createCallbackServer(state);
165
64
  const url = new URL(AUTHORIZE_URLS[mode], import.meta.url);
166
65
  url.searchParams.set('code', 'true');
167
66
  url.searchParams.set('client_id', CLIENT_ID);
168
67
  url.searchParams.set('response_type', 'code');
169
- url.searchParams.set('redirect_uri', callbackServer.redirectUri);
68
+ url.searchParams.set('redirect_uri', CODE_CALLBACK_URL);
170
69
  url.searchParams.set('scope', OAUTH_SCOPES.join(' '));
171
70
  url.searchParams.set('code_challenge', pkce.challenge);
172
71
  url.searchParams.set('code_challenge_method', 'S256');
173
72
  url.searchParams.set('state', state);
174
73
  return {
175
74
  url: url.toString(),
176
- redirectUri: callbackServer.redirectUri,
75
+ redirectUri: CODE_CALLBACK_URL,
177
76
  state,
178
- callback: async () => {
179
- try {
180
- const callbackUrl = await callbackServer.waitForCallback();
181
- return await exchange(callbackUrl, pkce.verifier, callbackServer.redirectUri, state);
182
- }
183
- catch {
184
- return { type: 'failed' };
185
- }
186
- },
77
+ verifier: pkce.verifier,
187
78
  };
188
79
  }
189
80
  export async function exchange(input, verifier, redirectUri, expectedState) {
@@ -3,6 +3,7 @@ export declare const AUTHORIZE_URLS: {
3
3
  readonly console: "https://platform.claude.com/oauth/authorize";
4
4
  readonly max: "https://claude.ai/oauth/authorize";
5
5
  };
6
+ export declare const CODE_CALLBACK_URL = "https://platform.claude.com/oauth/code/callback";
6
7
  export declare const TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
7
8
  export declare const OAUTH_SCOPES: string[];
8
9
  export declare const TOOL_PREFIX = "mcp_";
package/dist/constants.js CHANGED
@@ -3,6 +3,7 @@ export const AUTHORIZE_URLS = {
3
3
  console: 'https://platform.claude.com/oauth/authorize',
4
4
  max: 'https://claude.ai/oauth/authorize',
5
5
  };
6
+ export const CODE_CALLBACK_URL = 'https://platform.claude.com/oauth/code/callback';
6
7
  export const TOKEN_URL = 'https://platform.claude.com/v1/oauth/token';
7
8
  export const OAUTH_SCOPES = [
8
9
  'org:create_api_key',
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { authorize } from './auth';
1
+ import { authorize, exchange } from './auth';
2
2
  import { CLIENT_ID, TOKEN_URL } from './constants';
3
3
  import { createStrippedStream, mergeHeaders, prefixToolNames, rewriteUrl, setOAuthHeaders, } from './transform';
4
4
  export const AnthropicAuthPlugin = async ({ client }) => {
@@ -120,9 +120,11 @@ export const AnthropicAuthPlugin = async ({ client }) => {
120
120
  const result = await authorize('max');
121
121
  return {
122
122
  url: result.url,
123
- instructions: 'Complete authorization in the browser.',
124
- method: 'auto',
125
- callback: result.callback,
123
+ instructions: 'Paste the authorization code here:',
124
+ method: 'code',
125
+ callback: async (code) => {
126
+ return exchange(code, result.verifier, result.redirectUri, result.state);
127
+ },
126
128
  };
127
129
  },
128
130
  },
@@ -130,23 +132,23 @@ export const AnthropicAuthPlugin = async ({ client }) => {
130
132
  label: 'Create an API Key',
131
133
  type: 'oauth',
132
134
  authorize: async () => {
133
- const auth = await authorize('console');
135
+ const result = await authorize('console');
134
136
  return {
135
- url: auth.url,
136
- instructions: 'Complete authorization in the browser.',
137
- method: 'auto',
138
- callback: async () => {
139
- const credentials = await auth.callback();
137
+ url: result.url,
138
+ instructions: 'Paste the authorization code here:',
139
+ method: 'code',
140
+ callback: async (code) => {
141
+ const credentials = await exchange(code, result.verifier, result.redirectUri, result.state);
140
142
  if (credentials.type === 'failed')
141
143
  return credentials;
142
- const result = await fetch(`https://api.anthropic.com/api/oauth/claude_cli/create_api_key`, {
144
+ const apiKey = await fetch(`https://api.anthropic.com/api/oauth/claude_cli/create_api_key`, {
143
145
  method: 'POST',
144
146
  headers: {
145
147
  'Content-Type': 'application/json',
146
148
  authorization: `Bearer ${credentials.access}`,
147
149
  },
148
150
  }).then((r) => r.json());
149
- return { type: 'success', key: result.raw_key };
151
+ return { type: 'success', key: apiKey.raw_key };
150
152
  },
151
153
  };
152
154
  },
package/dist/pkce.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ export declare function generatePKCE(): Promise<{
2
+ verifier: string;
3
+ challenge: string;
4
+ method: 'S256';
5
+ }>;
package/dist/pkce.js ADDED
@@ -0,0 +1,17 @@
1
+ function base64UrlEncode(bytes) {
2
+ let bin = '';
3
+ for (const byte of bytes)
4
+ bin += String.fromCharCode(byte);
5
+ return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
6
+ }
7
+ export async function generatePKCE() {
8
+ const buf = new Uint8Array(64);
9
+ crypto.getRandomValues(buf);
10
+ const verifier = base64UrlEncode(buf);
11
+ const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
12
+ return {
13
+ verifier,
14
+ challenge: base64UrlEncode(new Uint8Array(digest)),
15
+ method: 'S256',
16
+ };
17
+ }
package/package.json CHANGED
@@ -1,6 +1,10 @@
1
1
  {
2
2
  "name": "@ex-machina/opencode-anthropic-auth",
3
- "version": "0.2.1",
3
+ "version": "1.0.1",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "https://github.com/ex-machina-co/opencode-anthropic-auth"
7
+ },
4
8
  "main": "./dist/index.js",
5
9
  "types": "./dist/index.d.ts",
6
10
  "files": [
@@ -15,19 +19,12 @@
15
19
  "lint": "biome lint .",
16
20
  "bump": "bun script/bump.ts"
17
21
  },
18
- "repository": {
19
- "type": "git",
20
- "url": "https://github.com/ex-machina-co/opencode-anthropic-auth"
21
- },
22
22
  "devDependencies": {
23
- "@biomejs/biome": "^2.4.7",
24
- "@opencode-ai/plugin": "^0.4.45",
25
- "@tsconfig/bun": "^1.0.10",
26
- "@types/bun": "^1.3.10",
27
- "lefthook": "^2.1.4",
28
- "typescript": "^5.9.3"
29
- },
30
- "dependencies": {
31
- "@openauthjs/openauth": "^0.4.3"
23
+ "@biomejs/biome": "2.4.7",
24
+ "@opencode-ai/plugin": "0.4.45",
25
+ "@tsconfig/bun": "1.0.10",
26
+ "@types/bun": "1.3.10",
27
+ "lefthook": "2.1.4",
28
+ "typescript": "5.9.3"
32
29
  }
33
30
  }