@ex-machina/opencode-anthropic-auth 1.8.2 → 1.8.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/README.md CHANGED
@@ -31,7 +31,7 @@ Add the plugin to your OpenCode configuration:
31
31
 
32
32
  ```json
33
33
  {
34
- "plugin": ["@ex-machina/opencode-anthropic-auth@1.8.1"]
34
+ "plugin": ["@ex-machina/opencode-anthropic-auth@1.8.2"]
35
35
  }
36
36
  ```
37
37
 
@@ -1,3 +1,5 @@
1
+ export declare const MAX_SSE_LINE_BYTES: number;
2
+ export declare const MAX_JSON_TOOL_NAME_BYTES = 1024;
1
3
  export type FetchInput = string | URL | Request;
2
4
  /**
3
5
  * Merge headers from a Request object and/or a RequestInit headers value
package/dist/transform.js CHANGED
@@ -1,5 +1,208 @@
1
1
  import { buildBillingHeaderValue } from "./cch.js";
2
2
  import { CLAUDE_CODE_ENTRYPOINT, CLAUDE_CODE_IDENTITY, OPENCODE_IDENTITY_PREFIX, PARAGRAPH_REMOVAL_ANCHORS, REQUIRED_BETAS, TEXT_REPLACEMENTS, TOOL_PREFIX, USER_AGENT, } from "./constants.js";
3
+ // Bound an incomplete SSE line so malformed streams cannot grow memory forever.
4
+ export const MAX_SSE_LINE_BYTES = 5 * 1024 * 1024;
5
+ function headersAfterBodyTransform(source) {
6
+ const headers = new Headers(source);
7
+ for (const name of [
8
+ 'content-digest',
9
+ 'content-encoding',
10
+ 'content-length',
11
+ 'content-md5',
12
+ 'content-range',
13
+ 'digest',
14
+ 'etag',
15
+ ]) {
16
+ headers.delete(name);
17
+ }
18
+ return headers;
19
+ }
20
+ const JSON_NAME_KEY_SUFFIX = new TextEncoder().encode('name"');
21
+ const JSON_TOOL_PREFIX = new TextEncoder().encode(TOOL_PREFIX);
22
+ const UTF8_ENCODER = new TextEncoder();
23
+ const UTF8_FATAL_DECODER = new TextDecoder('utf-8', { fatal: true });
24
+ export const MAX_JSON_TOOL_NAME_BYTES = 1024;
25
+ function isJsonWhitespace(byte) {
26
+ return byte === 0x20 || byte === 0x09 || byte === 0x0a || byte === 0x0d;
27
+ }
28
+ /**
29
+ * Rewrite JSON `name` string values without buffering the whole document.
30
+ * Only a bounded tool-name candidate is retained across chunks; all document
31
+ * content outside that string value is emitted immediately.
32
+ */
33
+ function createJsonToolNameStream(body) {
34
+ let state = 'outside';
35
+ let held = [];
36
+ let candidateIndex = 0;
37
+ let escaped = false;
38
+ const enterStringAfter = (byte) => {
39
+ if (byte === 0x22) {
40
+ state = 'outside';
41
+ escaped = false;
42
+ return;
43
+ }
44
+ state = 'string';
45
+ escaped = byte === 0x5c;
46
+ };
47
+ return body.pipeThrough(new TransformStream({
48
+ transform(chunk, controller) {
49
+ const output = new Uint8Array(chunk.byteLength + 32);
50
+ let outputLength = 0;
51
+ const write = (byte) => {
52
+ output[outputLength++] = byte;
53
+ };
54
+ const enqueueOutput = () => {
55
+ if (outputLength === 0)
56
+ return;
57
+ controller.enqueue(output.slice(0, outputLength));
58
+ outputLength = 0;
59
+ };
60
+ const writeHeld = () => {
61
+ for (const byte of held)
62
+ write(byte);
63
+ held = [];
64
+ };
65
+ const processOutside = (byte) => {
66
+ if (byte === 0x22) {
67
+ held = [byte];
68
+ candidateIndex = 0;
69
+ state = 'key-candidate';
70
+ return;
71
+ }
72
+ write(byte);
73
+ };
74
+ for (const byte of chunk) {
75
+ if (state === 'outside') {
76
+ processOutside(byte);
77
+ continue;
78
+ }
79
+ if (state === 'key-candidate') {
80
+ if (byte === JSON_NAME_KEY_SUFFIX[candidateIndex]) {
81
+ held.push(byte);
82
+ candidateIndex++;
83
+ if (candidateIndex === JSON_NAME_KEY_SUFFIX.byteLength) {
84
+ writeHeld();
85
+ state = 'after-name-key';
86
+ }
87
+ continue;
88
+ }
89
+ writeHeld();
90
+ write(byte);
91
+ enterStringAfter(byte);
92
+ continue;
93
+ }
94
+ if (state === 'string') {
95
+ write(byte);
96
+ if (escaped) {
97
+ escaped = false;
98
+ }
99
+ else if (byte === 0x5c) {
100
+ escaped = true;
101
+ }
102
+ else if (byte === 0x22) {
103
+ state = 'outside';
104
+ }
105
+ continue;
106
+ }
107
+ if (state === 'after-name-key') {
108
+ if (isJsonWhitespace(byte)) {
109
+ write(byte);
110
+ }
111
+ else if (byte === 0x3a) {
112
+ write(byte);
113
+ state = 'after-colon';
114
+ }
115
+ else {
116
+ processOutside(byte);
117
+ }
118
+ continue;
119
+ }
120
+ if (state === 'after-colon') {
121
+ if (isJsonWhitespace(byte)) {
122
+ write(byte);
123
+ }
124
+ else if (byte === 0x22) {
125
+ write(byte);
126
+ held = [];
127
+ candidateIndex = 0;
128
+ state = 'prefix-candidate';
129
+ }
130
+ else {
131
+ processOutside(byte);
132
+ }
133
+ continue;
134
+ }
135
+ if (state === 'prefix-candidate') {
136
+ if (byte === JSON_TOOL_PREFIX[candidateIndex]) {
137
+ held.push(byte);
138
+ candidateIndex++;
139
+ if (candidateIndex === JSON_TOOL_PREFIX.byteLength) {
140
+ held = [];
141
+ candidateIndex = 0;
142
+ escaped = false;
143
+ state = 'tool-name-candidate';
144
+ }
145
+ continue;
146
+ }
147
+ writeHeld();
148
+ write(byte);
149
+ enterStringAfter(byte);
150
+ continue;
151
+ }
152
+ if (escaped) {
153
+ held.push(byte);
154
+ escaped = false;
155
+ if (held.length > MAX_JSON_TOOL_NAME_BYTES) {
156
+ throw new Error(`JSON tool name exceeds ${MAX_JSON_TOOL_NAME_BYTES} byte limit`);
157
+ }
158
+ continue;
159
+ }
160
+ if (byte === 0x5c) {
161
+ held.push(byte);
162
+ escaped = true;
163
+ if (held.length > MAX_JSON_TOOL_NAME_BYTES) {
164
+ throw new Error(`JSON tool name exceeds ${MAX_JSON_TOOL_NAME_BYTES} byte limit`);
165
+ }
166
+ continue;
167
+ }
168
+ if (byte === 0x22) {
169
+ let replacement;
170
+ if (held.length === 0) {
171
+ replacement = JSON_TOOL_PREFIX;
172
+ }
173
+ else {
174
+ try {
175
+ replacement = UTF8_ENCODER.encode(unprefixName(UTF8_FATAL_DECODER.decode(Uint8Array.from(held))));
176
+ }
177
+ catch {
178
+ replacement = Uint8Array.from([...JSON_TOOL_PREFIX, ...held]);
179
+ }
180
+ }
181
+ enqueueOutput();
182
+ controller.enqueue(replacement);
183
+ write(byte);
184
+ held = [];
185
+ candidateIndex = 0;
186
+ state = 'outside';
187
+ continue;
188
+ }
189
+ held.push(byte);
190
+ if (held.length > MAX_JSON_TOOL_NAME_BYTES) {
191
+ throw new Error(`JSON tool name exceeds ${MAX_JSON_TOOL_NAME_BYTES} byte limit`);
192
+ }
193
+ }
194
+ enqueueOutput();
195
+ },
196
+ flush(controller) {
197
+ const trailing = Uint8Array.from(state === 'tool-name-candidate'
198
+ ? [...JSON_TOOL_PREFIX, ...held]
199
+ : held);
200
+ if (trailing.byteLength === 0)
201
+ return;
202
+ controller.enqueue(trailing);
203
+ },
204
+ }));
205
+ }
3
206
  /**
4
207
  * Prefix a tool name with TOOL_PREFIX and uppercase the first character.
5
208
  * Claude Code uses PascalCase tool names (e.g. mcp_Bash, mcp_Read);
@@ -289,26 +492,83 @@ export function rewriteRequestBody(body) {
289
492
  * Create a streaming response that strips the tool prefix from tool names.
290
493
  */
291
494
  export function createStrippedStream(response) {
495
+ const mediaType = response.headers
496
+ .get('content-type')
497
+ ?.split(';', 1)[0]
498
+ ?.trim()
499
+ .toLowerCase();
292
500
  if (!response.body)
293
501
  return response;
294
- const reader = response.body.getReader();
502
+ if (mediaType === 'application/json' || mediaType?.endsWith('+json')) {
503
+ const stream = createJsonToolNameStream(response.body);
504
+ const headers = headersAfterBodyTransform(response.headers);
505
+ return new Response(stream, {
506
+ status: response.status,
507
+ statusText: response.statusText,
508
+ headers,
509
+ });
510
+ }
511
+ if (mediaType !== 'text/event-stream')
512
+ return response;
295
513
  const decoder = new TextDecoder();
296
514
  const encoder = new TextEncoder();
297
- const stream = new ReadableStream({
298
- async pull(controller) {
299
- const { done, value } = await reader.read();
300
- if (done) {
301
- controller.close();
515
+ let pending = new Uint8Array(0);
516
+ let pendingLength = 0;
517
+ const appendPending = (bytes) => {
518
+ const requiredLength = pendingLength + bytes.byteLength;
519
+ if (requiredLength > MAX_SSE_LINE_BYTES) {
520
+ throw new Error(`SSE line exceeds ${MAX_SSE_LINE_BYTES} byte limit`);
521
+ }
522
+ if (requiredLength > pending.byteLength) {
523
+ let capacity = Math.max(1024, pending.byteLength);
524
+ while (capacity < requiredLength) {
525
+ capacity = Math.min(MAX_SSE_LINE_BYTES, capacity * 2);
526
+ }
527
+ const expanded = new Uint8Array(capacity);
528
+ expanded.set(pending.subarray(0, pendingLength));
529
+ pending = expanded;
530
+ }
531
+ pending.set(bytes, pendingLength);
532
+ pendingLength = requiredLength;
533
+ };
534
+ const stream = response.body.pipeThrough(new TransformStream({
535
+ transform(chunk, controller) {
536
+ let lastLineBreak = -1;
537
+ let lineLength = pendingLength;
538
+ for (let index = 0; index < chunk.byteLength; index++) {
539
+ if (chunk[index] === 0x0a || chunk[index] === 0x0d) {
540
+ lastLineBreak = index;
541
+ lineLength = 0;
542
+ }
543
+ else {
544
+ lineLength++;
545
+ if (lineLength > MAX_SSE_LINE_BYTES) {
546
+ throw new Error(`SSE line exceeds ${MAX_SSE_LINE_BYTES} byte limit`);
547
+ }
548
+ }
549
+ }
550
+ if (lastLineBreak < 0) {
551
+ appendPending(chunk);
302
552
  return;
303
553
  }
304
- let text = decoder.decode(value, { stream: true });
305
- text = stripToolPrefix(text);
306
- controller.enqueue(encoder.encode(text));
554
+ const completeLines = decoder.decode(pending.subarray(0, pendingLength), {
555
+ stream: true,
556
+ }) + decoder.decode(chunk.subarray(0, lastLineBreak + 1));
557
+ pendingLength = 0;
558
+ appendPending(chunk.subarray(lastLineBreak + 1));
559
+ controller.enqueue(encoder.encode(stripToolPrefix(completeLines)));
307
560
  },
308
- });
561
+ flush(controller) {
562
+ const trailing = decoder.decode(pending.subarray(0, pendingLength));
563
+ if (trailing) {
564
+ controller.enqueue(encoder.encode(stripToolPrefix(trailing)));
565
+ }
566
+ },
567
+ }));
568
+ const headers = headersAfterBodyTransform(response.headers);
309
569
  return new Response(stream, {
310
570
  status: response.status,
311
571
  statusText: response.statusText,
312
- headers: response.headers,
572
+ headers,
313
573
  });
314
574
  }
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@ex-machina/opencode-anthropic-auth",
3
- "version": "1.8.2",
3
+ "version": "1.8.3",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/ex-machina-co/opencode-anthropic-auth"
7
7
  },
8
8
  "main": "./dist/index.js",
9
9
  "types": "./dist/index.d.ts",
10
+ "packageManager": "bun@1.3.14",
10
11
  "engines": {
11
12
  "bun": "1.3.14"
12
13
  },
@@ -19,11 +20,12 @@
19
20
  "dev": "bun scripts/dev.ts",
20
21
  "dev:clean": "bun scripts/dev-clean.ts",
21
22
  "extract": "bun scripts/extract-system-prompt.ts",
23
+ "check": "bun turbo check:all",
22
24
  "test": "bun test",
23
25
  "types": "tsc",
24
26
  "format": "biome check --write --unsafe",
25
27
  "format:check": "biome format .",
26
- "lint": "biome lint .",
28
+ "lint": "biome lint --error-on-warnings .",
27
29
  "change": "changeset",
28
30
  "release": "bun run build && bun change publish"
29
31
  },
@@ -39,6 +41,7 @@
39
41
  "@types/bun": "1.3.14",
40
42
  "dedent": "^1.7.2",
41
43
  "lefthook": "2.1.9",
44
+ "turbo": "2.9.18",
42
45
  "typescript": "6.0.3"
43
46
  }
44
47
  }