codora-anthropic-auth 0.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/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # codora-anthropic-auth
2
+
3
+ Anthropic (Claude) authentication plugin for Codora.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install codora-anthropic-auth
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```javascript
14
+ import { AnthropicAuthPlugin } from 'codora-anthropic-auth';
15
+
16
+ // 在 Codora 中注册插件
17
+ const plugin = await AnthropicAuthPlugin({ client: codoraClient });
18
+ ```
19
+
20
+ ## Authentication Methods
21
+
22
+ This plugin provides three authentication methods:
23
+
24
+ 1. **Claude Pro/Max** - OAuth login for Claude Pro/Max subscribers
25
+ 2. **Create an API Key** - OAuth login that creates an API key automatically
26
+ 3. **Manually enter API Key** - Direct API key input
27
+
28
+ ## How It Works
29
+
30
+ ### OAuth Flow (Claude Pro/Max)
31
+
32
+ 1. User initiates login
33
+ 2. Plugin generates PKCE challenge
34
+ 3. User is redirected to `claude.ai/oauth/authorize`
35
+ 4. User authorizes and receives a code
36
+ 5. Plugin exchanges code for access/refresh tokens
37
+ 6. Tokens are stored and used for API requests
38
+
39
+ ### API Key Flow
40
+
41
+ 1. User enters API key manually
42
+ 2. Key is stored and used for API requests
43
+
44
+ ## Plugin Interface
45
+
46
+ ```typescript
47
+ interface AuthHook {
48
+ provider: "anthropic"
49
+ loader: (getAuth, provider) => Promise<{ apiKey, fetch }>
50
+ methods: AuthMethod[]
51
+ }
52
+ ```
53
+
54
+ ## License
55
+
56
+ MIT
package/index.mjs ADDED
@@ -0,0 +1,346 @@
1
+ import { generatePKCE } from "@openauthjs/openauth/pkce";
2
+
3
+ const CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
4
+
5
+ /**
6
+ * @param {"max" | "console"} mode
7
+ */
8
+ async function authorize(mode) {
9
+ const pkce = await generatePKCE();
10
+
11
+ const baseUrl = mode === "console"
12
+ ? "https://console.anthropic.com/oauth/authorize"
13
+ : "https://claude.ai/oauth/authorize";
14
+
15
+ const url = new URL(baseUrl);
16
+ url.searchParams.set("code", "true");
17
+ url.searchParams.set("client_id", CLIENT_ID);
18
+ url.searchParams.set("response_type", "code");
19
+ url.searchParams.set("redirect_uri", "https://console.anthropic.com/oauth/code/callback");
20
+ url.searchParams.set("scope", "org:create_api_key user:profile user:inference");
21
+ url.searchParams.set("code_challenge", pkce.challenge);
22
+ url.searchParams.set("code_challenge_method", "S256");
23
+ url.searchParams.set("state", pkce.verifier);
24
+
25
+ return {
26
+ url: url.toString(),
27
+ verifier: pkce.verifier,
28
+ };
29
+ }
30
+
31
+ /**
32
+ * @param {string} code
33
+ * @param {string} verifier
34
+ */
35
+ async function exchange(code, verifier) {
36
+ const splits = code.split("#");
37
+ const result = await fetch("https://console.anthropic.com/v1/oauth/token", {
38
+ method: "POST",
39
+ headers: {
40
+ "Content-Type": "application/json",
41
+ },
42
+ body: JSON.stringify({
43
+ code: splits[0],
44
+ state: splits[1],
45
+ grant_type: "authorization_code",
46
+ client_id: CLIENT_ID,
47
+ redirect_uri: "https://console.anthropic.com/oauth/code/callback",
48
+ code_verifier: verifier,
49
+ }),
50
+ });
51
+ if (!result.ok)
52
+ return {
53
+ type: "failed",
54
+ };
55
+ const json = await result.json();
56
+ return {
57
+ type: "success",
58
+ refresh: json.refresh_token,
59
+ access: json.access_token,
60
+ expires: Date.now() + json.expires_in * 1000,
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Anthropic Auth Plugin for Codora
66
+ * @param {Object} input - Plugin input from Codora
67
+ * @param {Object} input.client - Codora client instance
68
+ */
69
+ export async function AnthropicAuthPlugin({ client }) {
70
+ return {
71
+ auth: {
72
+ provider: "anthropic",
73
+
74
+ /**
75
+ * Loader - 当用户已认证时调用,用于配置 provider
76
+ */
77
+ async loader(getAuth, provider) {
78
+ const auth = await getAuth();
79
+ if (auth.type === "oauth") {
80
+ // Claude Pro/Max 用户 - 费用为 0
81
+ for (const model of Object.values(provider.models)) {
82
+ model.cost = {
83
+ input: 0,
84
+ output: 0,
85
+ cache: { read: 0, write: 0 },
86
+ };
87
+ }
88
+
89
+ return {
90
+ apiKey: "",
91
+ /**
92
+ * 自定义 fetch - 处理 OAuth token 刷新和请求修改
93
+ */
94
+ async fetch(input, init) {
95
+ const auth = await getAuth();
96
+ if (auth.type !== "oauth") return fetch(input, init);
97
+
98
+ // Token 过期,刷新
99
+ if (!auth.access || auth.expires < Date.now()) {
100
+ const response = await fetch(
101
+ "https://console.anthropic.com/v1/oauth/token",
102
+ {
103
+ method: "POST",
104
+ headers: { "Content-Type": "application/json" },
105
+ body: JSON.stringify({
106
+ grant_type: "refresh_token",
107
+ refresh_token: auth.refresh,
108
+ client_id: CLIENT_ID,
109
+ }),
110
+ },
111
+ );
112
+ if (!response.ok) {
113
+ throw new Error(`Token refresh failed: ${response.status}`);
114
+ }
115
+ const json = await response.json();
116
+ await client.auth.set({
117
+ path: { id: "anthropic" },
118
+ body: {
119
+ type: "oauth",
120
+ refresh: json.refresh_token,
121
+ access: json.access_token,
122
+ expires: Date.now() + json.expires_in * 1000,
123
+ },
124
+ });
125
+ auth.access = json.access_token;
126
+ }
127
+
128
+ // 构建请求头
129
+ const requestHeaders = new Headers();
130
+ if (input instanceof Request) {
131
+ input.headers.forEach((value, key) => {
132
+ requestHeaders.set(key, value);
133
+ });
134
+ }
135
+ if (init?.headers) {
136
+ if (init.headers instanceof Headers) {
137
+ init.headers.forEach((value, key) => {
138
+ requestHeaders.set(key, value);
139
+ });
140
+ } else if (Array.isArray(init.headers)) {
141
+ for (const [key, value] of init.headers) {
142
+ if (typeof value !== "undefined") {
143
+ requestHeaders.set(key, String(value));
144
+ }
145
+ }
146
+ } else {
147
+ for (const [key, value] of Object.entries(init.headers)) {
148
+ if (typeof value !== "undefined") {
149
+ requestHeaders.set(key, String(value));
150
+ }
151
+ }
152
+ }
153
+ }
154
+
155
+ // 设置 OAuth 相关头
156
+ const incomingBeta = requestHeaders.get("anthropic-beta") || "";
157
+ const incomingBetasList = incomingBeta
158
+ .split(",")
159
+ .map((b) => b.trim())
160
+ .filter(Boolean);
161
+
162
+ const mergedBetas = [
163
+ "oauth-2025-04-20",
164
+ "interleaved-thinking-2025-05-14",
165
+ ...incomingBetasList.filter(b => !b.startsWith("oauth-") && !b.startsWith("interleaved-thinking-")),
166
+ ].join(",");
167
+
168
+ requestHeaders.set("authorization", `Bearer ${auth.access}`);
169
+ requestHeaders.set("anthropic-beta", mergedBetas);
170
+ requestHeaders.set("user-agent", "claude-cli/2.1.2 (external, cli)");
171
+ requestHeaders.delete("x-api-key");
172
+
173
+ // 处理请求体 - 添加工具前缀
174
+ const TOOL_PREFIX = "mcp_";
175
+ let body = init?.body;
176
+ if (body && typeof body === "string") {
177
+ try {
178
+ const parsed = JSON.parse(body);
179
+
180
+ // 系统提示词处理
181
+ if (parsed.system && Array.isArray(parsed.system)) {
182
+ parsed.system = parsed.system.map(item => {
183
+ if (item.type === 'text' && item.text) {
184
+ return {
185
+ ...item,
186
+ text: item.text
187
+ .replace(/Codora/g, 'Claude Code')
188
+ .replace(/codora/gi, 'Claude')
189
+ };
190
+ }
191
+ return item;
192
+ });
193
+ }
194
+
195
+ // 工具定义添加前缀
196
+ if (parsed.tools && Array.isArray(parsed.tools)) {
197
+ parsed.tools = parsed.tools.map((tool) => ({
198
+ ...tool,
199
+ name: tool.name ? `${TOOL_PREFIX}${tool.name}` : tool.name,
200
+ }));
201
+ }
202
+
203
+ // 消息中的 tool_use 添加前缀
204
+ if (parsed.messages && Array.isArray(parsed.messages)) {
205
+ parsed.messages = parsed.messages.map((msg) => {
206
+ if (msg.content && Array.isArray(msg.content)) {
207
+ msg.content = msg.content.map((block) => {
208
+ if (block.type === "tool_use" && block.name) {
209
+ return { ...block, name: `${TOOL_PREFIX}${block.name}` };
210
+ }
211
+ return block;
212
+ });
213
+ }
214
+ return msg;
215
+ });
216
+ }
217
+ body = JSON.stringify(parsed);
218
+ } catch (e) {
219
+ // ignore parse errors
220
+ }
221
+ }
222
+
223
+ // 处理 URL
224
+ let requestInput = input;
225
+ let requestUrl = null;
226
+ try {
227
+ if (typeof input === "string" || input instanceof URL) {
228
+ requestUrl = new URL(input.toString());
229
+ } else if (input instanceof Request) {
230
+ requestUrl = new URL(input.url);
231
+ }
232
+ } catch {
233
+ requestUrl = null;
234
+ }
235
+
236
+ if (
237
+ requestUrl &&
238
+ requestUrl.pathname === "/v1/messages" &&
239
+ !requestUrl.searchParams.has("beta")
240
+ ) {
241
+ requestUrl.searchParams.set("beta", "true");
242
+ requestInput =
243
+ input instanceof Request
244
+ ? new Request(requestUrl.toString(), input)
245
+ : requestUrl;
246
+ }
247
+
248
+ const response = await fetch(requestInput, {
249
+ ...init,
250
+ body,
251
+ headers: requestHeaders,
252
+ });
253
+
254
+ // 转换响应流 - 移除工具前缀
255
+ if (response.body) {
256
+ const reader = response.body.getReader();
257
+ const decoder = new TextDecoder();
258
+ const encoder = new TextEncoder();
259
+
260
+ const stream = new ReadableStream({
261
+ async pull(controller) {
262
+ const { done, value } = await reader.read();
263
+ if (done) {
264
+ controller.close();
265
+ return;
266
+ }
267
+ let text = decoder.decode(value, { stream: true });
268
+ text = text.replace(/"name"\s*:\s*"mcp_([^"]+)"/g, '"name": "$1"');
269
+ controller.enqueue(encoder.encode(text));
270
+ },
271
+ });
272
+
273
+ return new Response(stream, {
274
+ status: response.status,
275
+ statusText: response.statusText,
276
+ headers: response.headers,
277
+ });
278
+ }
279
+
280
+ return response;
281
+ },
282
+ };
283
+ }
284
+
285
+ return {};
286
+ },
287
+
288
+ /**
289
+ * 认证方法列表 - UI 中显示的选项
290
+ */
291
+ methods: [
292
+ {
293
+ label: "Claude Pro/Max",
294
+ type: "oauth",
295
+ authorize: async () => {
296
+ const { url, verifier } = await authorize("max");
297
+ return {
298
+ url: url,
299
+ instructions: "Paste the authorization code here: ",
300
+ method: "code",
301
+ callback: async (code) => {
302
+ const credentials = await exchange(code, verifier);
303
+ return credentials;
304
+ },
305
+ };
306
+ },
307
+ },
308
+ {
309
+ label: "Create an API Key",
310
+ type: "oauth",
311
+ authorize: async () => {
312
+ const { url, verifier } = await authorize("console");
313
+ return {
314
+ url: url,
315
+ instructions: "Paste the authorization code here: ",
316
+ method: "code",
317
+ callback: async (code) => {
318
+ const credentials = await exchange(code, verifier);
319
+ if (credentials.type === "failed") return credentials;
320
+ const result = await fetch(
321
+ `https://api.anthropic.com/api/oauth/claude_cli/create_api_key`,
322
+ {
323
+ method: "POST",
324
+ headers: {
325
+ "Content-Type": "application/json",
326
+ authorization: `Bearer ${credentials.access}`,
327
+ },
328
+ },
329
+ ).then((r) => r.json());
330
+ return { type: "success", key: result.raw_key };
331
+ },
332
+ };
333
+ },
334
+ },
335
+ {
336
+ provider: "anthropic",
337
+ label: "Manually enter API Key",
338
+ type: "api",
339
+ },
340
+ ],
341
+ },
342
+ };
343
+ }
344
+
345
+ // 默认导出
346
+ export default AnthropicAuthPlugin;
package/oauth-url.txt ADDED
@@ -0,0 +1 @@
1
+ https://claude.ai/oauth/authorize?code=true&client_id=9d1c250a-e61b-44d9-88ed-5944d1962f5e&response_type=code&redirect_uri=https%3A%2F%2Fconsole.anthropic.com%2Foauth%2Fcode%2Fcallback&scope=org%3Acreate_api_key+user%3Aprofile+user%3Ainference&code_challenge=rOluxCwFXn3G1Cu8x5sdNaf2--B5Psh5vE4VATtoIBg&code_challenge_method=S256&state=pmUohgr1gN_oJIs0K3nC1tT4ps8XECWadAHOMFBuG1sF_hsOx4imIOjUmXh-ElwpgCeH26AW3hnG_cyiK0R3cg
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "codora-anthropic-auth",
3
+ "version": "0.0.1",
4
+ "description": "Anthropic authentication plugin for Codora",
5
+ "main": "./index.mjs",
6
+ "type": "module",
7
+ "keywords": [
8
+ "codora",
9
+ "anthropic",
10
+ "claude",
11
+ "auth",
12
+ "plugin"
13
+ ],
14
+ "author": "",
15
+ "license": "MIT",
16
+ "dependencies": {
17
+ "@openauthjs/openauth": "^0.4.3"
18
+ }
19
+ }
package/test-oauth.mjs ADDED
@@ -0,0 +1,87 @@
1
+ import { AnthropicAuthPlugin } from "./index.mjs";
2
+ import * as readline from "readline";
3
+
4
+ const rl = readline.createInterface({
5
+ input: process.stdin,
6
+ output: process.stdout,
7
+ });
8
+
9
+ function question(prompt) {
10
+ return new Promise((resolve) => rl.question(prompt, resolve));
11
+ }
12
+
13
+ // 模拟存储
14
+ const authStore = {};
15
+
16
+ const mockClient = {
17
+ auth: {
18
+ async set({ path, body }) {
19
+ authStore[path.id] = body;
20
+ console.log("\n✓ 认证信息已保存");
21
+ },
22
+ },
23
+ };
24
+
25
+ async function testOAuth() {
26
+ console.log("=== Codora Anthropic Auth - OAuth 测试 ===\n");
27
+
28
+ const plugin = await AnthropicAuthPlugin({ client: mockClient });
29
+
30
+ console.log("选择认证方式:");
31
+ console.log(" 1. Claude Pro/Max (OAuth)");
32
+ console.log(" 2. Create an API Key (OAuth)");
33
+ console.log(" 3. Manually enter API Key");
34
+
35
+ const choice = await question("\n请选择 (1-3): ");
36
+
37
+ const methodIndex = parseInt(choice) - 1;
38
+ const method = plugin.auth.methods[methodIndex];
39
+
40
+ if (!method) {
41
+ console.log("无效选择");
42
+ rl.close();
43
+ return;
44
+ }
45
+
46
+ if (method.type === "api") {
47
+ const apiKey = await question("请输入 API Key: ");
48
+ authStore["anthropic"] = { type: "api", key: apiKey };
49
+ console.log("\n✓ API Key 已保存");
50
+ console.log("存储内容:", authStore);
51
+ rl.close();
52
+ return;
53
+ }
54
+
55
+ // OAuth 流程
56
+ console.log("\n正在生成授权 URL...");
57
+ const authResult = await method.authorize();
58
+
59
+ console.log("\n请在浏览器中打开以下 URL:");
60
+ console.log("\n" + authResult.url + "\n");
61
+
62
+ console.log("完成授权后,将页面显示的授权码粘贴到下方");
63
+ const code = await question("\n授权码: ");
64
+
65
+ console.log("\n正在交换 Token...");
66
+ const result = await authResult.callback(code.trim());
67
+
68
+ if (result.type === "success") {
69
+ console.log("\n✓ 认证成功!");
70
+ if (result.key) {
71
+ console.log("API Key:", result.key.substring(0, 20) + "...");
72
+ } else {
73
+ console.log("Access Token:", result.access?.substring(0, 20) + "...");
74
+ console.log("Refresh Token:", result.refresh?.substring(0, 20) + "...");
75
+ console.log("过期时间:", new Date(result.expires).toLocaleString());
76
+ }
77
+ } else {
78
+ console.log("\n✗ 认证失败");
79
+ }
80
+
81
+ rl.close();
82
+ }
83
+
84
+ testOAuth().catch((e) => {
85
+ console.error("错误:", e.message);
86
+ rl.close();
87
+ });
package/test.mjs ADDED
@@ -0,0 +1,51 @@
1
+ import { AnthropicAuthPlugin } from "./index.mjs";
2
+
3
+ // 模拟 Codora client
4
+ const mockClient = {
5
+ auth: {
6
+ async set({ path, body }) {
7
+ console.log("保存认证信息:", { provider: path.id, ...body });
8
+ },
9
+ },
10
+ };
11
+
12
+ async function test() {
13
+ console.log("=== 测试 Codora Anthropic Auth 插件 ===\n");
14
+
15
+ // 1. 加载插件
16
+ const plugin = await AnthropicAuthPlugin({ client: mockClient });
17
+ console.log("✓ 插件加载成功\n");
18
+
19
+ // 2. 检查认证方法
20
+ console.log("认证方法列表:");
21
+ plugin.auth.methods.forEach((method, i) => {
22
+ console.log(` ${i + 1}. ${method.label} (${method.type})`);
23
+ });
24
+ console.log("");
25
+
26
+ // 3. 测试 OAuth 授权 URL 生成
27
+ console.log("测试 OAuth 流程 (Claude Pro/Max):");
28
+ const oauthMethod = plugin.auth.methods[0];
29
+ const authResult = await oauthMethod.authorize();
30
+
31
+ console.log(" 授权 URL:", authResult.url.substring(0, 80) + "...");
32
+ console.log(" 方法:", authResult.method);
33
+ console.log(" 提示:", authResult.instructions);
34
+ console.log("");
35
+
36
+ // 4. 测试 API Key 方式
37
+ console.log("测试 API Key 流程:");
38
+ const apiMethod = plugin.auth.methods[2];
39
+ console.log(" 类型:", apiMethod.type);
40
+ console.log(" 标签:", apiMethod.label);
41
+ console.log("");
42
+
43
+ console.log("=== 测试完成 ===");
44
+ console.log("\n要完成完整的 OAuth 测试:");
45
+ console.log("1. 在浏览器打开上面的授权 URL");
46
+ console.log("2. 登录并授权");
47
+ console.log("3. 复制授权码");
48
+ console.log("4. 调用 authResult.callback(code) 完成认证");
49
+ }
50
+
51
+ test().catch(console.error);