oc-auth-switcher 0.9.2 → 0.9.3

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/cli.js CHANGED
@@ -49,6 +49,11 @@ var OAUTH_SCOPES = [
49
49
  "user:mcp_servers",
50
50
  "user:file_upload"
51
51
  ];
52
+ var CLAUDE_CODE_VERSION = "2.1.258";
53
+ function formatUserAgent(version) {
54
+ return `claude-cli/${version} (external, cli)`;
55
+ }
56
+ var USER_AGENT = formatUserAgent(CLAUDE_CODE_VERSION);
52
57
 
53
58
  // src/accounts.ts
54
59
  function isAccessTokenFresh(account, nowMs = Date.now()) {
@@ -1063,7 +1068,7 @@ var PROBE_MODELS = [
1063
1068
  var PROBE_BETA_HEADERS = "oauth-2025-04-20,interleaved-thinking-2025-05-14";
1064
1069
  var PROBE_USER_AGENT = "claude-cli/2.1.87 (external, cli)";
1065
1070
  var PROBE_VERSION = "2023-06-01";
1066
- var CLAUDE_CODE_VERSION = "2.1.87";
1071
+ var CLAUDE_CODE_VERSION2 = "2.1.87";
1067
1072
  var CLAUDE_CODE_IDENTITY = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
1068
1073
  var CCH_SALT = "59cf53e54c78";
1069
1074
  var CCH_POSITIONS = [4, 7, 20];
@@ -1072,12 +1077,12 @@ function computeCCH(messageText) {
1072
1077
  }
1073
1078
  function computeVersionSuffix(messageText) {
1074
1079
  const chars = CCH_POSITIONS.map((i) => messageText[i] || "0").join("");
1075
- return createHash("sha256").update(`${CCH_SALT}${chars}${CLAUDE_CODE_VERSION}`).digest("hex").slice(0, 3);
1080
+ return createHash("sha256").update(`${CCH_SALT}${chars}${CLAUDE_CODE_VERSION2}`).digest("hex").slice(0, 3);
1076
1081
  }
1077
1082
  function buildBillingHeader(messageText) {
1078
1083
  const suffix = computeVersionSuffix(messageText);
1079
1084
  const cch = computeCCH(messageText);
1080
- return "x-anthropic-billing-header: " + `cc_version=${CLAUDE_CODE_VERSION}.${suffix}; ` + `cc_entrypoint=sdk-cli; ` + `cch=${cch};`;
1085
+ return "x-anthropic-billing-header: " + `cc_version=${CLAUDE_CODE_VERSION2}.${suffix}; ` + `cc_entrypoint=sdk-cli; ` + `cch=${cch};`;
1081
1086
  }
1082
1087
  async function fetchAndApplyOAuthUsage(account, state, result) {
1083
1088
  const oauthResult = await fetchOAuthUsage(account.access);
package/dist/index.js CHANGED
@@ -26,9 +26,12 @@ var OPENCODE_IDENTITY_PREFIX = "You are OpenCode";
26
26
  var CLAUDE_CODE_IDENTITY = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
27
27
  var CCH_SALT = "59cf53e54c78";
28
28
  var CCH_POSITIONS = [4, 7, 20];
29
- var CLAUDE_CODE_VERSION = "2.1.87";
29
+ var CLAUDE_CODE_VERSION = "2.1.258";
30
30
  var CLAUDE_CODE_ENTRYPOINT = "sdk-cli";
31
- var USER_AGENT = "claude-cli/2.1.87 (external, cli)";
31
+ function formatUserAgent(version) {
32
+ return `claude-cli/${version} (external, cli)`;
33
+ }
34
+ var USER_AGENT = formatUserAgent(CLAUDE_CODE_VERSION);
32
35
  var PARAGRAPH_REMOVAL_ANCHORS = [
33
36
  "github.com/anomalyco/opencode",
34
37
  "opencode.ai/docs"
@@ -71,6 +74,193 @@ function buildBillingHeaderValue(messages, version = CLAUDE_CODE_VERSION, entryp
71
74
  }
72
75
 
73
76
  // node_modules/@ex-machina/opencode-anthropic-auth/dist/transform.js
77
+ var MAX_SSE_LINE_BYTES = 5 * 1024 * 1024;
78
+ function headersAfterBodyTransform(source) {
79
+ const headers = new Headers(source);
80
+ for (const name of [
81
+ "content-digest",
82
+ "content-encoding",
83
+ "content-length",
84
+ "content-md5",
85
+ "content-range",
86
+ "digest",
87
+ "etag"
88
+ ]) {
89
+ headers.delete(name);
90
+ }
91
+ return headers;
92
+ }
93
+ var JSON_NAME_KEY_SUFFIX = new TextEncoder().encode('name"');
94
+ var JSON_TOOL_PREFIX = new TextEncoder().encode(TOOL_PREFIX);
95
+ var UTF8_ENCODER = new TextEncoder;
96
+ var UTF8_FATAL_DECODER = new TextDecoder("utf-8", { fatal: true });
97
+ var MAX_JSON_TOOL_NAME_BYTES = 1024;
98
+ function isJsonWhitespace(byte) {
99
+ return byte === 32 || byte === 9 || byte === 10 || byte === 13;
100
+ }
101
+ function createJsonToolNameStream(body) {
102
+ let state = "outside";
103
+ let held = [];
104
+ let candidateIndex = 0;
105
+ let escaped = false;
106
+ const enterStringAfter = (byte) => {
107
+ if (byte === 34) {
108
+ state = "outside";
109
+ escaped = false;
110
+ return;
111
+ }
112
+ state = "string";
113
+ escaped = byte === 92;
114
+ };
115
+ return body.pipeThrough(new TransformStream({
116
+ transform(chunk, controller) {
117
+ const output = new Uint8Array(chunk.byteLength + 32);
118
+ let outputLength = 0;
119
+ const write = (byte) => {
120
+ output[outputLength++] = byte;
121
+ };
122
+ const enqueueOutput = () => {
123
+ if (outputLength === 0)
124
+ return;
125
+ controller.enqueue(output.slice(0, outputLength));
126
+ outputLength = 0;
127
+ };
128
+ const writeHeld = () => {
129
+ for (const byte of held)
130
+ write(byte);
131
+ held = [];
132
+ };
133
+ const processOutside = (byte) => {
134
+ if (byte === 34) {
135
+ held = [byte];
136
+ candidateIndex = 0;
137
+ state = "key-candidate";
138
+ return;
139
+ }
140
+ write(byte);
141
+ };
142
+ for (const byte of chunk) {
143
+ if (state === "outside") {
144
+ processOutside(byte);
145
+ continue;
146
+ }
147
+ if (state === "key-candidate") {
148
+ if (byte === JSON_NAME_KEY_SUFFIX[candidateIndex]) {
149
+ held.push(byte);
150
+ candidateIndex++;
151
+ if (candidateIndex === JSON_NAME_KEY_SUFFIX.byteLength) {
152
+ writeHeld();
153
+ state = "after-name-key";
154
+ }
155
+ continue;
156
+ }
157
+ writeHeld();
158
+ write(byte);
159
+ enterStringAfter(byte);
160
+ continue;
161
+ }
162
+ if (state === "string") {
163
+ write(byte);
164
+ if (escaped) {
165
+ escaped = false;
166
+ } else if (byte === 92) {
167
+ escaped = true;
168
+ } else if (byte === 34) {
169
+ state = "outside";
170
+ }
171
+ continue;
172
+ }
173
+ if (state === "after-name-key") {
174
+ if (isJsonWhitespace(byte)) {
175
+ write(byte);
176
+ } else if (byte === 58) {
177
+ write(byte);
178
+ state = "after-colon";
179
+ } else {
180
+ processOutside(byte);
181
+ }
182
+ continue;
183
+ }
184
+ if (state === "after-colon") {
185
+ if (isJsonWhitespace(byte)) {
186
+ write(byte);
187
+ } else if (byte === 34) {
188
+ write(byte);
189
+ held = [];
190
+ candidateIndex = 0;
191
+ state = "prefix-candidate";
192
+ } else {
193
+ processOutside(byte);
194
+ }
195
+ continue;
196
+ }
197
+ if (state === "prefix-candidate") {
198
+ if (byte === JSON_TOOL_PREFIX[candidateIndex]) {
199
+ held.push(byte);
200
+ candidateIndex++;
201
+ if (candidateIndex === JSON_TOOL_PREFIX.byteLength) {
202
+ held = [];
203
+ candidateIndex = 0;
204
+ escaped = false;
205
+ state = "tool-name-candidate";
206
+ }
207
+ continue;
208
+ }
209
+ writeHeld();
210
+ write(byte);
211
+ enterStringAfter(byte);
212
+ continue;
213
+ }
214
+ if (escaped) {
215
+ held.push(byte);
216
+ escaped = false;
217
+ if (held.length > MAX_JSON_TOOL_NAME_BYTES) {
218
+ throw new Error(`JSON tool name exceeds ${MAX_JSON_TOOL_NAME_BYTES} byte limit`);
219
+ }
220
+ continue;
221
+ }
222
+ if (byte === 92) {
223
+ held.push(byte);
224
+ escaped = true;
225
+ if (held.length > MAX_JSON_TOOL_NAME_BYTES) {
226
+ throw new Error(`JSON tool name exceeds ${MAX_JSON_TOOL_NAME_BYTES} byte limit`);
227
+ }
228
+ continue;
229
+ }
230
+ if (byte === 34) {
231
+ let replacement;
232
+ if (held.length === 0) {
233
+ replacement = JSON_TOOL_PREFIX;
234
+ } else {
235
+ try {
236
+ replacement = UTF8_ENCODER.encode(unprefixName(UTF8_FATAL_DECODER.decode(Uint8Array.from(held))));
237
+ } catch {
238
+ replacement = Uint8Array.from([...JSON_TOOL_PREFIX, ...held]);
239
+ }
240
+ }
241
+ enqueueOutput();
242
+ controller.enqueue(replacement);
243
+ write(byte);
244
+ held = [];
245
+ candidateIndex = 0;
246
+ state = "outside";
247
+ continue;
248
+ }
249
+ held.push(byte);
250
+ if (held.length > MAX_JSON_TOOL_NAME_BYTES) {
251
+ throw new Error(`JSON tool name exceeds ${MAX_JSON_TOOL_NAME_BYTES} byte limit`);
252
+ }
253
+ }
254
+ enqueueOutput();
255
+ },
256
+ flush(controller) {
257
+ const trailing = Uint8Array.from(state === "tool-name-candidate" ? [...JSON_TOOL_PREFIX, ...held] : held);
258
+ if (trailing.byteLength === 0)
259
+ return;
260
+ controller.enqueue(trailing);
261
+ }
262
+ }));
263
+ }
74
264
  function prefixName(name) {
75
265
  return `${TOOL_PREFIX}${name.charAt(0).toUpperCase()}${name.slice(1)}`;
76
266
  }
@@ -115,10 +305,10 @@ function mergeBetaHeaders(headers) {
115
305
  const incomingBetasList = incomingBeta.split(",").map((b) => b.trim()).filter(Boolean);
116
306
  return [...new Set([...REQUIRED_BETAS, ...incomingBetasList])].join(",");
117
307
  }
118
- function setOAuthHeaders(headers, accessToken) {
308
+ function setOAuthHeaders(headers, accessToken, version = CLAUDE_CODE_VERSION) {
119
309
  headers.set("authorization", `Bearer ${accessToken}`);
120
310
  headers.set("anthropic-beta", mergeBetaHeaders(headers));
121
- headers.set("user-agent", USER_AGENT);
311
+ headers.set("user-agent", formatUserAgent(version));
122
312
  headers.delete("x-api-key");
123
313
  return headers;
124
314
  }
@@ -256,10 +446,10 @@ function prependClaudeCodeIdentity(system) {
256
446
  }
257
447
  return [identityBlock, ...sanitized];
258
448
  }
259
- function rewriteRequestBody(body) {
449
+ function rewriteRequestBody(body, version = CLAUDE_CODE_VERSION) {
260
450
  try {
261
451
  const parsed = JSON.parse(body);
262
- const billingHeader = Array.isArray(parsed.messages) && parsed.messages.some((message) => message.role === "user") ? buildBillingHeaderValue(parsed.messages, undefined, CLAUDE_CODE_ENTRYPOINT) : null;
452
+ const billingHeader = Array.isArray(parsed.messages) && parsed.messages.some((message) => message.role === "user") ? buildBillingHeaderValue(parsed.messages, version, CLAUDE_CODE_ENTRYPOINT) : null;
263
453
  parsed.system = prependClaudeCodeIdentity(parsed.system);
264
454
  if (billingHeader && Array.isArray(parsed.system)) {
265
455
  parsed.system.unshift({ type: "text", text: billingHeader });
@@ -270,27 +460,79 @@ function rewriteRequestBody(body) {
270
460
  }
271
461
  }
272
462
  function createStrippedStream(response) {
463
+ const mediaType = response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
273
464
  if (!response.body)
274
465
  return response;
275
- const reader = response.body.getReader();
466
+ if (mediaType === "application/json" || mediaType?.endsWith("+json")) {
467
+ const stream2 = createJsonToolNameStream(response.body);
468
+ const headers2 = headersAfterBodyTransform(response.headers);
469
+ return new Response(stream2, {
470
+ status: response.status,
471
+ statusText: response.statusText,
472
+ headers: headers2
473
+ });
474
+ }
475
+ if (mediaType !== "text/event-stream")
476
+ return response;
276
477
  const decoder = new TextDecoder;
277
478
  const encoder = new TextEncoder;
278
- const stream = new ReadableStream({
279
- async pull(controller) {
280
- const { done, value } = await reader.read();
281
- if (done) {
282
- controller.close();
479
+ let pending = new Uint8Array(0);
480
+ let pendingLength = 0;
481
+ const appendPending = (bytes) => {
482
+ const requiredLength = pendingLength + bytes.byteLength;
483
+ if (requiredLength > MAX_SSE_LINE_BYTES) {
484
+ throw new Error(`SSE line exceeds ${MAX_SSE_LINE_BYTES} byte limit`);
485
+ }
486
+ if (requiredLength > pending.byteLength) {
487
+ let capacity = Math.max(1024, pending.byteLength);
488
+ while (capacity < requiredLength) {
489
+ capacity = Math.min(MAX_SSE_LINE_BYTES, capacity * 2);
490
+ }
491
+ const expanded = new Uint8Array(capacity);
492
+ expanded.set(pending.subarray(0, pendingLength));
493
+ pending = expanded;
494
+ }
495
+ pending.set(bytes, pendingLength);
496
+ pendingLength = requiredLength;
497
+ };
498
+ const stream = response.body.pipeThrough(new TransformStream({
499
+ transform(chunk, controller) {
500
+ let lastLineBreak = -1;
501
+ let lineLength = pendingLength;
502
+ for (let index = 0;index < chunk.byteLength; index++) {
503
+ if (chunk[index] === 10 || chunk[index] === 13) {
504
+ lastLineBreak = index;
505
+ lineLength = 0;
506
+ } else {
507
+ lineLength++;
508
+ if (lineLength > MAX_SSE_LINE_BYTES) {
509
+ throw new Error(`SSE line exceeds ${MAX_SSE_LINE_BYTES} byte limit`);
510
+ }
511
+ }
512
+ }
513
+ if (lastLineBreak < 0) {
514
+ appendPending(chunk);
283
515
  return;
284
516
  }
285
- let text = decoder.decode(value, { stream: true });
286
- text = stripToolPrefix(text);
287
- controller.enqueue(encoder.encode(text));
517
+ const completeLines = decoder.decode(pending.subarray(0, pendingLength), {
518
+ stream: true
519
+ }) + decoder.decode(chunk.subarray(0, lastLineBreak + 1));
520
+ pendingLength = 0;
521
+ appendPending(chunk.subarray(lastLineBreak + 1));
522
+ controller.enqueue(encoder.encode(stripToolPrefix(completeLines)));
523
+ },
524
+ flush(controller) {
525
+ const trailing = decoder.decode(pending.subarray(0, pendingLength));
526
+ if (trailing) {
527
+ controller.enqueue(encoder.encode(stripToolPrefix(trailing)));
528
+ }
288
529
  }
289
- });
530
+ }));
531
+ const headers = headersAfterBodyTransform(response.headers);
290
532
  return new Response(stream, {
291
533
  status: response.status,
292
534
  statusText: response.statusText,
293
- headers: response.headers
535
+ headers
294
536
  });
295
537
  }
296
538
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oc-auth-switcher",
3
- "version": "0.9.2",
3
+ "version": "0.9.3",
4
4
  "description": "OpenCode auth plugin for multi-account Anthropic Claude Max rotation with automatic failover.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -17,7 +17,7 @@
17
17
  "@opencode-ai/plugin": "*"
18
18
  },
19
19
  "dependencies": {
20
- "@ex-machina/opencode-anthropic-auth": "1.8.3"
20
+ "@ex-machina/opencode-anthropic-auth": "1.8.4"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@opencode-ai/plugin": "latest",