@ex-machina/opencode-anthropic-auth 0.0.15

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.
@@ -0,0 +1,34 @@
1
+ name: Publish to npm
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ paths:
8
+ - "package.json"
9
+
10
+ permissions:
11
+ id-token: write
12
+ contents: read
13
+
14
+ jobs:
15
+ publish:
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - name: Checkout
19
+ uses: actions/checkout@v4
20
+
21
+ - name: Setup Node
22
+ uses: actions/setup-node@v4
23
+ with:
24
+ node-version: "24"
25
+ registry-url: "https://registry.npmjs.org"
26
+
27
+ - name: Install dependencies
28
+ run: npm install
29
+
30
+ - name: Publish
31
+ run: npm publish --access public
32
+ env:
33
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
34
+ NPM_CONFIG_PROVENANCE: false
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 SST
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/bun.lock ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "lockfileVersion": 1,
3
+ "workspaces": {
4
+ "": {
5
+ "name": "opencode-anthropic-auth",
6
+ "dependencies": {
7
+ "@openauthjs/openauth": "^0.4.3",
8
+ },
9
+ "devDependencies": {
10
+ "@opencode-ai/plugin": "^0.4.45",
11
+ },
12
+ },
13
+ },
14
+ "packages": {
15
+ "@openauthjs/openauth": ["@openauthjs/openauth@0.4.3", "", { "dependencies": { "@standard-schema/spec": "1.0.0-beta.3", "aws4fetch": "1.0.20", "jose": "5.9.6" }, "peerDependencies": { "arctic": "^2.2.2", "hono": "^4.0.0" } }, "sha512-RlnjqvHzqcbFVymEwhlUEuac4utA5h4nhSK/i2szZuQmxTIqbGUxZ+nM+avM+VV4Ing+/ZaNLKILoXS3yrkOOw=="],
16
+
17
+ "@opencode-ai/plugin": ["@opencode-ai/plugin@0.4.45", "", { "dependencies": { "@opencode-ai/sdk": "0.4.19" } }, "sha512-TuD+FNmA6vN+/B82qayCvOyTXRuAtfvU0U95UKSZoNrYgIBhpD/sW/oS65iUv5QwQqzO8BxI4DYWjmLIqCz8uw=="],
18
+
19
+ "@opencode-ai/sdk": ["@opencode-ai/sdk@0.4.19", "", {}, "sha512-7V+wDR1+m+TQZAraAh/bOSObiA/uysG1YIXZVe6gl1sQAXDtkG2FYCzs0gTZ/ORdkUKEnr3vyQIk895Mu0CC/w=="],
20
+
21
+ "@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
22
+
23
+ "@oslojs/binary": ["@oslojs/binary@1.0.0", "", {}, "sha512-9RCU6OwXU6p67H4NODbuxv2S3eenuQ4/WFLrsq+K/k682xrznH5EVWA7N4VFk9VYVcbFtKqur5YQQZc0ySGhsQ=="],
24
+
25
+ "@oslojs/crypto": ["@oslojs/crypto@1.0.1", "", { "dependencies": { "@oslojs/asn1": "1.0.0", "@oslojs/binary": "1.0.0" } }, "sha512-7n08G8nWjAr/Yu3vu9zzrd0L9XnrJfpMioQcvCMxBIiF5orECHe5/3J0jmXRVvgfqMm/+4oxlQ+Sq39COYLcNQ=="],
26
+
27
+ "@oslojs/encoding": ["@oslojs/encoding@1.1.0", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="],
28
+
29
+ "@oslojs/jwt": ["@oslojs/jwt@0.2.0", "", { "dependencies": { "@oslojs/encoding": "0.4.1" } }, "sha512-bLE7BtHrURedCn4Mco3ma9L4Y1GR2SMBuIvjWr7rmQ4/W/4Jy70TIAgZ+0nIlk0xHz1vNP8x8DCns45Sb2XRbg=="],
30
+
31
+ "@standard-schema/spec": ["@standard-schema/spec@1.0.0-beta.3", "", {}, "sha512-0ifF3BjA1E8SY9C+nUew8RefNOIq0cDlYALPty4rhUm8Rrl6tCM8hBT4bhGhx7I7iXD0uAgt50lgo8dD73ACMw=="],
32
+
33
+ "arctic": ["arctic@2.3.4", "", { "dependencies": { "@oslojs/crypto": "1.0.1", "@oslojs/encoding": "1.1.0", "@oslojs/jwt": "0.2.0" } }, "sha512-+p30BOWsctZp+CVYCt7oAean/hWGW42sH5LAcRQX56ttEkFJWbzXBhmSpibbzwSJkRrotmsA+oAoJoVsU0f5xA=="],
34
+
35
+ "aws4fetch": ["aws4fetch@1.0.20", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="],
36
+
37
+ "hono": ["hono@4.9.1", "", {}, "sha512-qfvdJ42t6CQE0N/iSCa8KsW8SQqYD67YB+TYbwPHlnALvX+s7ynh8otR1NEk5jXtUg73gpV/B82OSufDmwtX3w=="],
38
+
39
+ "jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="],
40
+
41
+ "@oslojs/jwt/@oslojs/encoding": ["@oslojs/encoding@0.4.1", "", {}, "sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q=="],
42
+ }
43
+ }
package/index.mjs ADDED
@@ -0,0 +1,365 @@
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 url = new URL(
12
+ `https://${mode === "console" ? "console.anthropic.com" : "claude.ai"}/oauth/authorize`,
13
+ import.meta.url,
14
+ );
15
+ url.searchParams.set("code", "true");
16
+ url.searchParams.set("client_id", CLIENT_ID);
17
+ url.searchParams.set("response_type", "code");
18
+ url.searchParams.set(
19
+ "redirect_uri",
20
+ "https://console.anthropic.com/oauth/code/callback",
21
+ );
22
+ url.searchParams.set(
23
+ "scope",
24
+ "org:create_api_key user:profile user:inference",
25
+ );
26
+ url.searchParams.set("code_challenge", pkce.challenge);
27
+ url.searchParams.set("code_challenge_method", "S256");
28
+ url.searchParams.set("state", pkce.verifier);
29
+ return {
30
+ url: url.toString(),
31
+ verifier: pkce.verifier,
32
+ };
33
+ }
34
+
35
+ /**
36
+ * @param {string} code
37
+ * @param {string} verifier
38
+ */
39
+ async function exchange(code, verifier) {
40
+ const splits = code.split("#");
41
+ const result = await fetch("https://console.anthropic.com/v1/oauth/token", {
42
+ method: "POST",
43
+ headers: {
44
+ "Content-Type": "application/json",
45
+ },
46
+ body: JSON.stringify({
47
+ code: splits[0],
48
+ state: splits[1],
49
+ grant_type: "authorization_code",
50
+ client_id: CLIENT_ID,
51
+ redirect_uri: "https://console.anthropic.com/oauth/code/callback",
52
+ code_verifier: verifier,
53
+ }),
54
+ });
55
+ if (!result.ok)
56
+ return {
57
+ type: "failed",
58
+ };
59
+ const json = await result.json();
60
+ return {
61
+ type: "success",
62
+ refresh: json.refresh_token,
63
+ access: json.access_token,
64
+ expires: Date.now() + json.expires_in * 1000,
65
+ };
66
+ }
67
+
68
+ /**
69
+ * @type {import('@opencode-ai/plugin').Plugin}
70
+ */
71
+ export async function AnthropicAuthPlugin({ client }) {
72
+ return {
73
+ "experimental.chat.system.transform": (input, output) => {
74
+ const prefix =
75
+ "You are Claude Code, Anthropic's official CLI for Claude.";
76
+ if (input.model?.providerID === "anthropic") {
77
+ output.system.unshift(prefix);
78
+ if (output.system[1])
79
+ output.system[1] = prefix + "\n\n" + output.system[1];
80
+ }
81
+ },
82
+ auth: {
83
+ provider: "anthropic",
84
+ async loader(getAuth, provider) {
85
+ const auth = await getAuth();
86
+ if (auth.type === "oauth") {
87
+ // zero out cost for max plan
88
+ for (const model of Object.values(provider.models)) {
89
+ model.cost = {
90
+ input: 0,
91
+ output: 0,
92
+ cache: {
93
+ read: 0,
94
+ write: 0,
95
+ },
96
+ };
97
+ }
98
+ return {
99
+ apiKey: "",
100
+ /**
101
+ * @param {any} input
102
+ * @param {any} init
103
+ */
104
+ async fetch(input, init) {
105
+ const auth = await getAuth();
106
+ if (auth.type !== "oauth") return fetch(input, init);
107
+ if (!auth.access || auth.expires < Date.now()) {
108
+ const response = await fetch(
109
+ "https://console.anthropic.com/v1/oauth/token",
110
+ {
111
+ method: "POST",
112
+ headers: {
113
+ "Content-Type": "application/json",
114
+ },
115
+ body: JSON.stringify({
116
+ grant_type: "refresh_token",
117
+ refresh_token: auth.refresh,
118
+ client_id: CLIENT_ID,
119
+ }),
120
+ },
121
+ );
122
+ if (!response.ok) {
123
+ throw new Error(`Token refresh failed: ${response.status}`);
124
+ }
125
+ const json = await response.json();
126
+ await client.auth.set({
127
+ path: {
128
+ id: "anthropic",
129
+ },
130
+ body: {
131
+ type: "oauth",
132
+ refresh: json.refresh_token,
133
+ access: json.access_token,
134
+ expires: Date.now() + json.expires_in * 1000,
135
+ },
136
+ });
137
+ auth.access = json.access_token;
138
+ }
139
+ const requestInit = init ?? {};
140
+
141
+ const requestHeaders = new Headers();
142
+ if (input instanceof Request) {
143
+ input.headers.forEach((value, key) => {
144
+ requestHeaders.set(key, value);
145
+ });
146
+ }
147
+ if (requestInit.headers) {
148
+ if (requestInit.headers instanceof Headers) {
149
+ requestInit.headers.forEach((value, key) => {
150
+ requestHeaders.set(key, value);
151
+ });
152
+ } else if (Array.isArray(requestInit.headers)) {
153
+ for (const [key, value] of requestInit.headers) {
154
+ if (typeof value !== "undefined") {
155
+ requestHeaders.set(key, String(value));
156
+ }
157
+ }
158
+ } else {
159
+ for (const [key, value] of Object.entries(
160
+ requestInit.headers,
161
+ )) {
162
+ if (typeof value !== "undefined") {
163
+ requestHeaders.set(key, String(value));
164
+ }
165
+ }
166
+ }
167
+ }
168
+
169
+ // Preserve all incoming beta headers while ensuring OAuth requirements
170
+ const incomingBeta = requestHeaders.get("anthropic-beta") || "";
171
+ const incomingBetasList = incomingBeta
172
+ .split(",")
173
+ .map((b) => b.trim())
174
+ .filter(Boolean);
175
+
176
+ const requiredBetas = [
177
+ "oauth-2025-04-20",
178
+ "interleaved-thinking-2025-05-14",
179
+ ];
180
+ const mergedBetas = [
181
+ ...new Set([...requiredBetas, ...incomingBetasList]),
182
+ ].join(",");
183
+
184
+ requestHeaders.set("authorization", `Bearer ${auth.access}`);
185
+ requestHeaders.set("anthropic-beta", mergedBetas);
186
+ requestHeaders.set(
187
+ "user-agent",
188
+ "claude-cli/2.1.2 (external, cli)",
189
+ );
190
+ requestHeaders.delete("x-api-key");
191
+
192
+ const TOOL_PREFIX = "mcp_";
193
+ let body = requestInit.body;
194
+ if (body && typeof body === "string") {
195
+ try {
196
+ const parsed = JSON.parse(body);
197
+
198
+ // Sanitize system prompt - server blocks "OpenCode" string
199
+ // Note: (?<!\/) preserves paths like /path/to/opencode-foo
200
+ if (parsed.system && Array.isArray(parsed.system)) {
201
+ parsed.system = parsed.system.map((item) => {
202
+ if (item.type === "text" && item.text) {
203
+ return {
204
+ ...item,
205
+ text: item.text
206
+ .replace(/OpenCode/g, "Claude Code")
207
+ .replace(/(?<!\/)opencode/gi, "Claude"),
208
+ };
209
+ }
210
+ return item;
211
+ });
212
+ }
213
+
214
+ // Add prefix to tools definitions
215
+ if (parsed.tools && Array.isArray(parsed.tools)) {
216
+ parsed.tools = parsed.tools.map((tool) => ({
217
+ ...tool,
218
+ name: tool.name
219
+ ? `${TOOL_PREFIX}${tool.name}`
220
+ : tool.name,
221
+ }));
222
+ }
223
+ // Add prefix to tool_use blocks in messages
224
+ if (parsed.messages && Array.isArray(parsed.messages)) {
225
+ parsed.messages = parsed.messages.map((msg) => {
226
+ if (msg.content && Array.isArray(msg.content)) {
227
+ msg.content = msg.content.map((block) => {
228
+ if (block.type === "tool_use" && block.name) {
229
+ return {
230
+ ...block,
231
+ name: `${TOOL_PREFIX}${block.name}`,
232
+ };
233
+ }
234
+ return block;
235
+ });
236
+ }
237
+ return msg;
238
+ });
239
+ }
240
+ body = JSON.stringify(parsed);
241
+ } catch (e) {
242
+ // ignore parse errors
243
+ }
244
+ }
245
+
246
+ let requestInput = input;
247
+ let requestUrl = null;
248
+ try {
249
+ if (typeof input === "string" || input instanceof URL) {
250
+ requestUrl = new URL(input.toString());
251
+ } else if (input instanceof Request) {
252
+ requestUrl = new URL(input.url);
253
+ }
254
+ } catch {
255
+ requestUrl = null;
256
+ }
257
+
258
+ if (
259
+ requestUrl &&
260
+ requestUrl.pathname === "/v1/messages" &&
261
+ !requestUrl.searchParams.has("beta")
262
+ ) {
263
+ requestUrl.searchParams.set("beta", "true");
264
+ requestInput =
265
+ input instanceof Request
266
+ ? new Request(requestUrl.toString(), input)
267
+ : requestUrl;
268
+ }
269
+
270
+ const response = await fetch(requestInput, {
271
+ ...requestInit,
272
+ body,
273
+ headers: requestHeaders,
274
+ });
275
+
276
+ // Transform streaming response to rename tools back
277
+ if (response.body) {
278
+ const reader = response.body.getReader();
279
+ const decoder = new TextDecoder();
280
+ const encoder = new TextEncoder();
281
+
282
+ const stream = new ReadableStream({
283
+ async pull(controller) {
284
+ const { done, value } = await reader.read();
285
+ if (done) {
286
+ controller.close();
287
+ return;
288
+ }
289
+
290
+ let text = decoder.decode(value, { stream: true });
291
+ text = text.replace(
292
+ /"name"\s*:\s*"mcp_([^"]+)"/g,
293
+ '"name": "$1"',
294
+ );
295
+ controller.enqueue(encoder.encode(text));
296
+ },
297
+ });
298
+
299
+ return new Response(stream, {
300
+ status: response.status,
301
+ statusText: response.statusText,
302
+ headers: response.headers,
303
+ });
304
+ }
305
+
306
+ return response;
307
+ },
308
+ };
309
+ }
310
+
311
+ return {};
312
+ },
313
+ methods: [
314
+ {
315
+ label: "Claude Pro/Max",
316
+ type: "oauth",
317
+ authorize: async () => {
318
+ const { url, verifier } = await authorize("max");
319
+ return {
320
+ url: url,
321
+ instructions: "Paste the authorization code here: ",
322
+ method: "code",
323
+ callback: async (code) => {
324
+ const credentials = await exchange(code, verifier);
325
+ return credentials;
326
+ },
327
+ };
328
+ },
329
+ },
330
+ {
331
+ label: "Create an API Key",
332
+ type: "oauth",
333
+ authorize: async () => {
334
+ const { url, verifier } = await authorize("console");
335
+ return {
336
+ url: url,
337
+ instructions: "Paste the authorization code here: ",
338
+ method: "code",
339
+ callback: async (code) => {
340
+ const credentials = await exchange(code, verifier);
341
+ if (credentials.type === "failed") return credentials;
342
+ const result = await fetch(
343
+ `https://api.anthropic.com/api/oauth/claude_cli/create_api_key`,
344
+ {
345
+ method: "POST",
346
+ headers: {
347
+ "Content-Type": "application/json",
348
+ authorization: `Bearer ${credentials.access}`,
349
+ },
350
+ },
351
+ ).then((r) => r.json());
352
+ return { type: "success", key: result.raw_key };
353
+ },
354
+ };
355
+ },
356
+ },
357
+ {
358
+ provider: "anthropic",
359
+ label: "Manually enter API Key",
360
+ type: "api",
361
+ },
362
+ ],
363
+ },
364
+ };
365
+ }
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "@ex-machina/opencode-anthropic-auth",
3
+ "version": "0.0.15",
4
+ "main": "./index.mjs",
5
+ "scripts": {
6
+ "bump": "bun script/bump.ts"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/ex-machina-co/opencode-anthropic-auth"
11
+ },
12
+ "devDependencies": {
13
+ "@opencode-ai/plugin": "^0.4.45"
14
+ },
15
+ "dependencies": {
16
+ "@openauthjs/openauth": "^0.4.3"
17
+ }
18
+ }
package/script/bump.ts ADDED
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Bumps the version in package.json, commits, and pushes.
4
+ * Pushing the version change to main triggers the CI publish workflow.
5
+ *
6
+ * Usage: bun script/bump.ts [patch|minor|major] [--dry-run]
7
+ */
8
+
9
+ import { $ } from "bun";
10
+
11
+ const dir = new URL("..", import.meta.url).pathname;
12
+ process.chdir(dir);
13
+
14
+ const args = process.argv.slice(2).filter((a) => !a.startsWith("--"));
15
+ const bumpType = args[0] || "patch";
16
+ const dry = process.argv.includes("--dry-run");
17
+
18
+ const log = (msg: string) => console.log(dry ? `[DRY RUN] ${msg}` : msg);
19
+
20
+ // Read current version
21
+ const currentPkg = await Bun.file("./package.json").json();
22
+ log(`Current version: ${currentPkg.version}`);
23
+ log(`Bumping ${bumpType}...`);
24
+
25
+ // Bump version in package.json
26
+ if (!dry) {
27
+ await $`npm version ${bumpType} --no-git-tag-version`;
28
+ }
29
+
30
+ // Read the new version
31
+ const pkg = await Bun.file("./package.json").json();
32
+ const version = pkg.version;
33
+
34
+ log(`New version: ${version}`);
35
+
36
+ // Commit and push — CI will handle npm publish
37
+ if (!dry) {
38
+ await $`git add package.json`;
39
+ await $`git commit -m ${"chore: bump to " + version}`;
40
+ await $`git push`;
41
+ }
42
+
43
+ log(`Done!`);