@ex-machina/opencode-anthropic-auth 1.0.2 → 1.0.4

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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 Ex Machina
3
+ Copyright (c) 2026 Ex Machina
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -2,12 +2,6 @@
2
2
 
3
3
  An [OpenCode](https://github.com/anomalyco/opencode) plugin that provides Anthropic OAuth authentication, enabling Claude Pro/Max users to use their subscription directly with OpenCode.
4
4
 
5
- <details>
6
- <summary>This solves the ".opencode" -> ".Claude" issues too!</summary>
7
-
8
- ![renaming.jpg](images/renaming.jpg)
9
- </details>
10
-
11
5
  ## Usage
12
6
 
13
7
  Add the plugin to your OpenCode configuration:
@@ -40,15 +34,14 @@ For Claude Pro/Max authentication, the plugin:
40
34
 
41
35
  ### Publishing
42
36
 
43
- Bump the version and push to `main` -- CI will publish to npm automatically:
37
+ This project uses [changesets](https://github.com/changesets/changesets) for versioning and publishing. See the [changeset README](.changeset/README.md) for more details.
44
38
 
45
39
  ```bash
46
- bun bump # patch bump (0.0.13 -> 0.0.14)
47
- bun bump minor # minor bump (0.0.13 -> 0.1.0)
48
- bun bump major # major bump (0.0.13 -> 1.0.0)
49
- bun bump -- --dry-run # preview without changes
40
+ bun change # create a changeset describing your changes
50
41
  ```
51
42
 
43
+ When changesets are merged to `main`, CI will automatically open a release PR. Merging that PR publishes to npm.
44
+
52
45
  ## License
53
46
 
54
47
  MIT
package/dist/index.js CHANGED
@@ -27,6 +27,9 @@ export const AnthropicAuthPlugin = async ({ client }) => {
27
27
  },
28
28
  };
29
29
  }
30
+ // Shared inflight refresh promise — prevents concurrent token refreshes
31
+ // from racing against each other (and causing 401 cascades with token rotation)
32
+ let refreshPromise = null;
30
33
  return {
31
34
  apiKey: '',
32
35
  async fetch(input, init) {
@@ -34,64 +37,73 @@ export const AnthropicAuthPlugin = async ({ client }) => {
34
37
  if (auth.type !== 'oauth')
35
38
  return fetch(input, init);
36
39
  if (!auth.access || !auth.expires || auth.expires < Date.now()) {
37
- const maxRetries = 2;
38
- const baseDelayMs = 500;
39
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
40
- try {
41
- if (attempt > 0) {
42
- const delay = baseDelayMs * 2 ** (attempt - 1);
43
- await new Promise((resolve) => setTimeout(resolve, delay));
44
- }
45
- const response = await fetch(TOKEN_URL, {
46
- method: 'POST',
47
- headers: {
48
- 'Content-Type': 'application/json',
49
- Accept: 'application/json, text/plain, */*',
50
- 'User-Agent': 'axios/1.13.6',
51
- },
52
- body: JSON.stringify({
53
- grant_type: 'refresh_token',
54
- refresh_token: auth.refresh,
55
- client_id: CLIENT_ID,
56
- }),
57
- });
58
- if (!response.ok) {
59
- if (response.status >= 500 && attempt < maxRetries) {
60
- await response.body?.cancel();
61
- continue;
40
+ if (!refreshPromise) {
41
+ refreshPromise = (async () => {
42
+ const maxRetries = 2;
43
+ const baseDelayMs = 500;
44
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
45
+ try {
46
+ if (attempt > 0) {
47
+ const delay = baseDelayMs * 2 ** (attempt - 1);
48
+ await new Promise((resolve) => setTimeout(resolve, delay));
49
+ }
50
+ const response = await fetch(TOKEN_URL, {
51
+ method: 'POST',
52
+ headers: {
53
+ 'Content-Type': 'application/json',
54
+ Accept: 'application/json, text/plain, */*',
55
+ 'User-Agent': 'axios/1.13.6',
56
+ },
57
+ body: JSON.stringify({
58
+ grant_type: 'refresh_token',
59
+ refresh_token: auth.refresh,
60
+ client_id: CLIENT_ID,
61
+ }),
62
+ });
63
+ if (!response.ok) {
64
+ if (response.status >= 500 && attempt < maxRetries) {
65
+ await response.body?.cancel();
66
+ continue;
67
+ }
68
+ throw new Error(`Token refresh failed: ${response.status}`);
69
+ }
70
+ const json = (await response.json());
71
+ // biome-ignore lint/suspicious/noExplicitAny: SDK types don't expose auth.set
72
+ await client.auth.set({
73
+ path: {
74
+ id: 'anthropic',
75
+ },
76
+ body: {
77
+ type: 'oauth',
78
+ refresh: json.refresh_token,
79
+ access: json.access_token,
80
+ expires: Date.now() + json.expires_in * 1000,
81
+ },
82
+ });
83
+ return json.access_token;
84
+ }
85
+ catch (error) {
86
+ const isNetworkError = error instanceof Error &&
87
+ (error.message.includes('fetch failed') ||
88
+ ('code' in error &&
89
+ (error.code === 'ECONNRESET' ||
90
+ error.code === 'ECONNREFUSED' ||
91
+ error.code === 'ETIMEDOUT' ||
92
+ error.code === 'UND_ERR_CONNECT_TIMEOUT')));
93
+ if (attempt < maxRetries && isNetworkError) {
94
+ continue;
95
+ }
96
+ throw error;
62
97
  }
63
- throw new Error(`Token refresh failed: ${response.status}`);
64
- }
65
- const json = (await response.json());
66
- // biome-ignore lint/suspicious/noExplicitAny: SDK types don't expose auth.set
67
- await client.auth.set({
68
- path: {
69
- id: 'anthropic',
70
- },
71
- body: {
72
- type: 'oauth',
73
- refresh: json.refresh_token,
74
- access: json.access_token,
75
- expires: Date.now() + json.expires_in * 1000,
76
- },
77
- });
78
- auth.access = json.access_token;
79
- break;
80
- }
81
- catch (error) {
82
- const isNetworkError = error instanceof Error &&
83
- (error.message.includes('fetch failed') ||
84
- ('code' in error &&
85
- (error.code === 'ECONNRESET' ||
86
- error.code === 'ECONNREFUSED' ||
87
- error.code === 'ETIMEDOUT' ||
88
- error.code === 'UND_ERR_CONNECT_TIMEOUT')));
89
- if (attempt < maxRetries && isNetworkError) {
90
- continue;
91
98
  }
92
- throw error;
93
- }
99
+ // Unreachable — each iteration either returns or throws.
100
+ // Kept as a TypeScript exhaustiveness guard.
101
+ throw new Error('Token refresh exhausted all retries');
102
+ })().finally(() => {
103
+ refreshPromise = null;
104
+ });
94
105
  }
106
+ auth.access = await refreshPromise;
95
107
  }
96
108
  const requestHeaders = mergeHeaders(input, init);
97
109
  // biome-ignore lint/style/noNonNullAssertion: access is guaranteed set above
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ex-machina/opencode-anthropic-auth",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/ex-machina-co/opencode-anthropic-auth"
@@ -17,8 +17,8 @@
17
17
  "format": "biome format --write .",
18
18
  "format:check": "biome format .",
19
19
  "lint": "biome lint .",
20
- "changeset": "changeset",
21
- "release": "bun run build && changeset publish"
20
+ "change": "changeset",
21
+ "release": "bun run build && bun change publish"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@biomejs/biome": "2.4.10",