@ex-machina/opencode-anthropic-auth 1.2.0 → 1.3.0
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 +38 -2
- package/dist/constants.d.ts +19 -1
- package/dist/constants.js +22 -4
- package/dist/index.js +0 -1
- package/dist/transform.d.ts +15 -7
- package/dist/transform.js +45 -31
- package/package.json +4 -1
- package/dist/tests/auth.test.d.ts +0 -1
- package/dist/tests/auth.test.js +0 -87
- package/dist/tests/index.test.d.ts +0 -1
- package/dist/tests/index.test.js +0 -481
- package/dist/tests/pkce.test.d.ts +0 -1
- package/dist/tests/pkce.test.js +0 -111
- package/dist/tests/transform.test.d.ts +0 -1
- package/dist/tests/transform.test.js +0 -621
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ Add the plugin to your OpenCode configuration:
|
|
|
21
21
|
|
|
22
22
|
```json
|
|
23
23
|
{
|
|
24
|
-
"plugin": ["@ex-machina/opencode-anthropic-auth@1.
|
|
24
|
+
"plugin": ["@ex-machina/opencode-anthropic-auth@1.2.0"]
|
|
25
25
|
}
|
|
26
26
|
```
|
|
27
27
|
|
|
@@ -41,10 +41,46 @@ For Claude Pro/Max authentication, the plugin:
|
|
|
41
41
|
2. Exchanges the authorization code for access and refresh tokens
|
|
42
42
|
3. Automatically refreshes expired tokens
|
|
43
43
|
4. Injects the required OAuth headers and beta flags into API requests
|
|
44
|
-
5.
|
|
44
|
+
5. Sanitizes the system prompt for compatibility (see below)
|
|
45
|
+
6. Zeros out model costs (since usage is covered by the subscription)
|
|
46
|
+
|
|
47
|
+
### System Prompt Sanitization
|
|
48
|
+
|
|
49
|
+
The Anthropic API for Max subscriptions requires the system prompt to identify as Claude Code. The plugin rewrites the system prompt on each request using an **anchor-based** approach that minimizes what gets changed:
|
|
50
|
+
|
|
51
|
+
1. **Identity swap** — The OpenCode identity line is removed and replaced with the Claude Code identity.
|
|
52
|
+
2. **Paragraph removal by anchor** — Any paragraph containing a known URL anchor (e.g. `github.com/anomalyco/opencode`, `opencode.ai/docs`) is removed entirely. This is resilient to upstream rewording — as long as the anchor URL appears somewhere in the paragraph, the removal works regardless of surrounding text changes.
|
|
53
|
+
3. **Inline text replacements** — Short branded strings inside paragraphs we want to keep are replaced (e.g. "OpenCode" → "the assistant" in the professional objectivity section).
|
|
54
|
+
|
|
55
|
+
Everything else in the system prompt is preserved: tone/style guidance, task management instructions, tool usage policy, environment info, skills, user/project instructions, and file paths containing "opencode".
|
|
45
56
|
|
|
46
57
|
## Development
|
|
47
58
|
|
|
59
|
+
### Local Testing
|
|
60
|
+
|
|
61
|
+
Use `bun run dev` to test plugin changes locally without publishing to npm:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
bun run dev
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
This does three things:
|
|
68
|
+
|
|
69
|
+
1. Builds the plugin
|
|
70
|
+
2. Symlinks the build output into `.opencode/plugins/` so OpenCode loads it as a local plugin
|
|
71
|
+
3. Starts `tsc --watch` for automatic rebuilds on source changes
|
|
72
|
+
|
|
73
|
+
After starting the dev script, restart OpenCode in this project directory to pick up the local build. Any edits to `src/` will trigger a rebuild — restart OpenCode again to load the new version.
|
|
74
|
+
|
|
75
|
+
Ctrl+C stops the watcher and cleans up the symlink. If the process was killed without cleanup (e.g. `kill -9`), you can manually remove the symlink:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
bun run dev:clean
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
> [!NOTE]
|
|
82
|
+
> If you have the npm version of this plugin in your global OpenCode config, both will load. The local version takes precedence for auth handling.
|
|
83
|
+
|
|
48
84
|
### Publishing
|
|
49
85
|
|
|
50
86
|
This project uses [changesets](https://github.com/changesets/changesets) for versioning and publishing. See the [changeset README](.changeset/README.md) for more details.
|
package/dist/constants.d.ts
CHANGED
|
@@ -11,4 +11,22 @@ export declare const REQUIRED_BETAS: string[];
|
|
|
11
11
|
export declare const OPENCODE_IDENTITY = "You are OpenCode, the best coding agent on the planet.";
|
|
12
12
|
export declare const CLAUDE_CODE_IDENTITY = "You are Claude Code, Anthropic's official CLI for Claude.";
|
|
13
13
|
export declare const USER_AGENT = "claude-cli/2.1.2 (external, cli)";
|
|
14
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Anchors that identify paragraphs to remove from the system prompt.
|
|
16
|
+
* Any paragraph (text between blank lines) containing one of these
|
|
17
|
+
* strings is removed entirely.
|
|
18
|
+
*
|
|
19
|
+
* This is resilient to upstream rewording — as long as the anchor
|
|
20
|
+
* string (typically a URL) still appears somewhere in the paragraph,
|
|
21
|
+
* the removal works regardless of how the surrounding text changes.
|
|
22
|
+
*/
|
|
23
|
+
export declare const PARAGRAPH_REMOVAL_ANCHORS: string[];
|
|
24
|
+
/**
|
|
25
|
+
* Inline text replacements applied after paragraph removal.
|
|
26
|
+
* These handle cases where "OpenCode" appears inside a paragraph
|
|
27
|
+
* we want to keep (so we can't remove the whole paragraph).
|
|
28
|
+
*/
|
|
29
|
+
export declare const TEXT_REPLACEMENTS: {
|
|
30
|
+
match: string;
|
|
31
|
+
replacement: string;
|
|
32
|
+
}[];
|
package/dist/constants.js
CHANGED
|
@@ -21,8 +21,26 @@ export const REQUIRED_BETAS = [
|
|
|
21
21
|
export const OPENCODE_IDENTITY = 'You are OpenCode, the best coding agent on the planet.';
|
|
22
22
|
export const CLAUDE_CODE_IDENTITY = "You are Claude Code, Anthropic's official CLI for Claude.";
|
|
23
23
|
export const USER_AGENT = 'claude-cli/2.1.2 (external, cli)';
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Anchors that identify paragraphs to remove from the system prompt.
|
|
26
|
+
* Any paragraph (text between blank lines) containing one of these
|
|
27
|
+
* strings is removed entirely.
|
|
28
|
+
*
|
|
29
|
+
* This is resilient to upstream rewording — as long as the anchor
|
|
30
|
+
* string (typically a URL) still appears somewhere in the paragraph,
|
|
31
|
+
* the removal works regardless of how the surrounding text changes.
|
|
32
|
+
*/
|
|
33
|
+
export const PARAGRAPH_REMOVAL_ANCHORS = [
|
|
34
|
+
// Help/feedback block — references the OpenCode GitHub repo
|
|
35
|
+
'github.com/anomalyco/opencode',
|
|
36
|
+
// OpenCode docs guidance — references the OpenCode docs URL
|
|
37
|
+
'opencode.ai/docs',
|
|
38
|
+
];
|
|
39
|
+
/**
|
|
40
|
+
* Inline text replacements applied after paragraph removal.
|
|
41
|
+
* These handle cases where "OpenCode" appears inside a paragraph
|
|
42
|
+
* we want to keep (so we can't remove the whole paragraph).
|
|
43
|
+
*/
|
|
44
|
+
export const TEXT_REPLACEMENTS = [
|
|
45
|
+
{ match: 'if OpenCode honestly', replacement: 'if the assistant honestly' },
|
|
28
46
|
];
|
package/dist/index.js
CHANGED
|
@@ -110,7 +110,6 @@ export const AnthropicAuthPlugin = async ({ client }) => {
|
|
|
110
110
|
...init,
|
|
111
111
|
body,
|
|
112
112
|
headers: requestHeaders,
|
|
113
|
-
// biome-ignore lint/suspicious/noExplicitAny: tls option is Bun-specific, not in standard RequestInit
|
|
114
113
|
...(isInsecure() && { tls: { rejectUnauthorized: false } }),
|
|
115
114
|
});
|
|
116
115
|
return createStrippedStream(response);
|
package/dist/transform.d.ts
CHANGED
|
@@ -38,12 +38,20 @@ export declare function rewriteUrl(input: FetchInput): {
|
|
|
38
38
|
url: URL | null;
|
|
39
39
|
};
|
|
40
40
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
41
|
+
* Sanitize OpenCode-branded strings from the system prompt text.
|
|
42
|
+
*
|
|
43
|
+
* 1. Removes the OPENCODE_IDENTITY line.
|
|
44
|
+
* 2. Removes any paragraph (text between blank lines) that contains
|
|
45
|
+
* one of the PARAGRAPH_REMOVAL_ANCHORS — typically URLs that
|
|
46
|
+
* identify OpenCode-specific content.
|
|
47
|
+
* 3. Applies TEXT_REPLACEMENTS for inline occurrences of "OpenCode"
|
|
48
|
+
* inside paragraphs we want to keep.
|
|
49
|
+
*
|
|
50
|
+
* This approach is resilient to upstream rewording of the OpenCode
|
|
51
|
+
* prompt — as long as the anchor strings (URLs, etc.) still appear
|
|
52
|
+
* somewhere in the paragraph, the removal works.
|
|
45
53
|
*/
|
|
46
|
-
export declare function sanitizeSystemText(text: string
|
|
54
|
+
export declare function sanitizeSystemText(text: string): string;
|
|
47
55
|
type SystemBlock = {
|
|
48
56
|
type: string;
|
|
49
57
|
text: string;
|
|
@@ -53,11 +61,11 @@ type SystemBlock = {
|
|
|
53
61
|
* Sanitize system prompt and prepend Claude Code identity.
|
|
54
62
|
* Handles all Anthropic API system formats: undefined, string, or array of text blocks.
|
|
55
63
|
*/
|
|
56
|
-
export declare function prependClaudeCodeIdentity(system: unknown
|
|
64
|
+
export declare function prependClaudeCodeIdentity(system: unknown): SystemBlock[];
|
|
57
65
|
/**
|
|
58
66
|
* Rewrite the full request body: sanitize system prompt and prefix tool names.
|
|
59
67
|
*/
|
|
60
|
-
export declare function rewriteRequestBody(body: string
|
|
68
|
+
export declare function rewriteRequestBody(body: string): string;
|
|
61
69
|
/**
|
|
62
70
|
* Create a streaming response that strips the tool prefix from tool names.
|
|
63
71
|
*/
|
package/dist/transform.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CLAUDE_CODE_IDENTITY, OPENCODE_IDENTITY,
|
|
1
|
+
import { CLAUDE_CODE_IDENTITY, OPENCODE_IDENTITY, PARAGRAPH_REMOVAL_ANCHORS, REQUIRED_BETAS, TEXT_REPLACEMENTS, TOOL_PREFIX, USER_AGENT, } from './constants';
|
|
2
2
|
/**
|
|
3
3
|
* Merge headers from a Request object and/or a RequestInit headers value
|
|
4
4
|
* into a single Headers instance.
|
|
@@ -169,30 +169,47 @@ export function rewriteUrl(input) {
|
|
|
169
169
|
return { input: newInput, url: requestUrl };
|
|
170
170
|
}
|
|
171
171
|
/**
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
172
|
+
* Sanitize OpenCode-branded strings from the system prompt text.
|
|
173
|
+
*
|
|
174
|
+
* 1. Removes the OPENCODE_IDENTITY line.
|
|
175
|
+
* 2. Removes any paragraph (text between blank lines) that contains
|
|
176
|
+
* one of the PARAGRAPH_REMOVAL_ANCHORS — typically URLs that
|
|
177
|
+
* identify OpenCode-specific content.
|
|
178
|
+
* 3. Applies TEXT_REPLACEMENTS for inline occurrences of "OpenCode"
|
|
179
|
+
* inside paragraphs we want to keep.
|
|
180
|
+
*
|
|
181
|
+
* This approach is resilient to upstream rewording of the OpenCode
|
|
182
|
+
* prompt — as long as the anchor strings (URLs, etc.) still appear
|
|
183
|
+
* somewhere in the paragraph, the removal works.
|
|
176
184
|
*/
|
|
177
|
-
export function sanitizeSystemText(text
|
|
178
|
-
|
|
179
|
-
if (startIdx === -1)
|
|
185
|
+
export function sanitizeSystemText(text) {
|
|
186
|
+
if (!text.includes(OPENCODE_IDENTITY))
|
|
180
187
|
return text;
|
|
181
|
-
//
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if (
|
|
186
|
-
|
|
188
|
+
// Split into paragraphs (separated by one or more blank lines)
|
|
189
|
+
const paragraphs = text.split(/\n\n+/);
|
|
190
|
+
const filtered = paragraphs.filter((paragraph) => {
|
|
191
|
+
// Remove the identity line (may be its own paragraph or part of one)
|
|
192
|
+
if (paragraph.includes(OPENCODE_IDENTITY)) {
|
|
193
|
+
// If the paragraph is JUST the identity, drop it entirely
|
|
194
|
+
if (paragraph.trim() === OPENCODE_IDENTITY)
|
|
195
|
+
return false;
|
|
196
|
+
// Otherwise it's mixed — we'll handle inline below
|
|
187
197
|
}
|
|
198
|
+
// Remove paragraphs containing any removal anchor
|
|
199
|
+
for (const anchor of PARAGRAPH_REMOVAL_ANCHORS) {
|
|
200
|
+
if (paragraph.includes(anchor))
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
return true;
|
|
204
|
+
});
|
|
205
|
+
let result = filtered.join('\n\n');
|
|
206
|
+
// Remove the identity line if it was part of a larger paragraph
|
|
207
|
+
result = result.replace(OPENCODE_IDENTITY, '').replace(/\n{3,}/g, '\n\n');
|
|
208
|
+
// Apply inline text replacements
|
|
209
|
+
for (const rule of TEXT_REPLACEMENTS) {
|
|
210
|
+
result = result.replace(rule.match, rule.replacement);
|
|
188
211
|
}
|
|
189
|
-
|
|
190
|
-
onError?.('sanitizeSystemText: could not find any preserved tail marker after OpenCode identity');
|
|
191
|
-
return text;
|
|
192
|
-
}
|
|
193
|
-
// Preserve content from the marker onwards (skip leading newline if present)
|
|
194
|
-
const tail = text[endIdx] === '\n' ? text.slice(endIdx + 1) : text.slice(endIdx);
|
|
195
|
-
return text.slice(0, startIdx) + tail;
|
|
212
|
+
return result.trim();
|
|
196
213
|
}
|
|
197
214
|
function isRecord(value) {
|
|
198
215
|
return value != null && typeof value === 'object' && !Array.isArray(value);
|
|
@@ -201,7 +218,7 @@ function isRecord(value) {
|
|
|
201
218
|
* Sanitize system prompt and prepend Claude Code identity.
|
|
202
219
|
* Handles all Anthropic API system formats: undefined, string, or array of text blocks.
|
|
203
220
|
*/
|
|
204
|
-
export function prependClaudeCodeIdentity(system
|
|
221
|
+
export function prependClaudeCodeIdentity(system) {
|
|
205
222
|
const identityBlock = {
|
|
206
223
|
type: 'text',
|
|
207
224
|
text: CLAUDE_CODE_IDENTITY,
|
|
@@ -209,7 +226,7 @@ export function prependClaudeCodeIdentity(system, onError) {
|
|
|
209
226
|
if (system == null)
|
|
210
227
|
return [identityBlock];
|
|
211
228
|
if (typeof system === 'string') {
|
|
212
|
-
const sanitized = sanitizeSystemText(system
|
|
229
|
+
const sanitized = sanitizeSystemText(system);
|
|
213
230
|
if (sanitized === CLAUDE_CODE_IDENTITY)
|
|
214
231
|
return [identityBlock];
|
|
215
232
|
return [identityBlock, { type: 'text', text: sanitized }];
|
|
@@ -217,16 +234,13 @@ export function prependClaudeCodeIdentity(system, onError) {
|
|
|
217
234
|
if (isRecord(system)) {
|
|
218
235
|
const type = typeof system.type === 'string' ? system.type : 'text';
|
|
219
236
|
const text = typeof system.text === 'string' ? system.text : '';
|
|
220
|
-
return [
|
|
221
|
-
identityBlock,
|
|
222
|
-
{ ...system, type, text: sanitizeSystemText(text, onError) },
|
|
223
|
-
];
|
|
237
|
+
return [identityBlock, { ...system, type, text: sanitizeSystemText(text) }];
|
|
224
238
|
}
|
|
225
239
|
if (!Array.isArray(system))
|
|
226
240
|
return [identityBlock];
|
|
227
241
|
const sanitized = system.map((item) => {
|
|
228
242
|
if (typeof item === 'string') {
|
|
229
|
-
return { type: 'text', text: sanitizeSystemText(item
|
|
243
|
+
return { type: 'text', text: sanitizeSystemText(item) };
|
|
230
244
|
}
|
|
231
245
|
if (isRecord(item) &&
|
|
232
246
|
item.type === 'text' &&
|
|
@@ -234,7 +248,7 @@ export function prependClaudeCodeIdentity(system, onError) {
|
|
|
234
248
|
return {
|
|
235
249
|
...item,
|
|
236
250
|
type: 'text',
|
|
237
|
-
text: sanitizeSystemText(item.text
|
|
251
|
+
text: sanitizeSystemText(item.text),
|
|
238
252
|
};
|
|
239
253
|
}
|
|
240
254
|
return { type: 'text', text: String(item) };
|
|
@@ -248,11 +262,11 @@ export function prependClaudeCodeIdentity(system, onError) {
|
|
|
248
262
|
/**
|
|
249
263
|
* Rewrite the full request body: sanitize system prompt and prefix tool names.
|
|
250
264
|
*/
|
|
251
|
-
export function rewriteRequestBody(body
|
|
265
|
+
export function rewriteRequestBody(body) {
|
|
252
266
|
try {
|
|
253
267
|
const parsed = JSON.parse(body);
|
|
254
268
|
// Sanitize system prompt and prepend Claude Code identity
|
|
255
|
-
parsed.system = prependClaudeCodeIdentity(parsed.system
|
|
269
|
+
parsed.system = prependClaudeCodeIdentity(parsed.system);
|
|
256
270
|
// Prefix tool names
|
|
257
271
|
if (parsed.tools && Array.isArray(parsed.tools)) {
|
|
258
272
|
parsed.tools = parsed.tools.map((tool) => ({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ex-machina/opencode-anthropic-auth",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/ex-machina-co/opencode-anthropic-auth"
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
"scripts": {
|
|
14
14
|
"prepare": "tsc -p tsconfig.build.json",
|
|
15
15
|
"build": "tsc -p tsconfig.build.json",
|
|
16
|
+
"dev": "bun scripts/dev.ts",
|
|
17
|
+
"dev:clean": "bun scripts/dev-clean.ts",
|
|
16
18
|
"test": "bun test",
|
|
17
19
|
"types": "tsc",
|
|
18
20
|
"format": "biome format --write .",
|
|
@@ -28,6 +30,7 @@
|
|
|
28
30
|
"@opencode-ai/plugin": "1.3.13",
|
|
29
31
|
"@tsconfig/bun": "1.0.10",
|
|
30
32
|
"@types/bun": "1.3.11",
|
|
33
|
+
"dedent": "^1.7.2",
|
|
31
34
|
"lefthook": "2.1.4",
|
|
32
35
|
"typescript": "6.0.2"
|
|
33
36
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/tests/auth.test.js
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, mock, spyOn, test } from 'bun:test';
|
|
2
|
-
import { authorize, exchange } from '../auth';
|
|
3
|
-
import { CLIENT_ID, CODE_CALLBACK_URL, OAUTH_SCOPES } from '../constants';
|
|
4
|
-
afterEach(() => {
|
|
5
|
-
mock.restore();
|
|
6
|
-
});
|
|
7
|
-
describe('authorize', () => {
|
|
8
|
-
test('returns the hosted callback URL for max mode', async () => {
|
|
9
|
-
const result = await authorize('max');
|
|
10
|
-
expect(result.url).toBeString();
|
|
11
|
-
expect(result.redirectUri).toBe(CODE_CALLBACK_URL);
|
|
12
|
-
expect(result.verifier).toBeString();
|
|
13
|
-
const url = new URL(result.url);
|
|
14
|
-
expect(url.origin).toBe('https://claude.ai');
|
|
15
|
-
expect(url.pathname).toBe('/oauth/authorize');
|
|
16
|
-
expect(url.searchParams.get('redirect_uri')).toBe(CODE_CALLBACK_URL);
|
|
17
|
-
});
|
|
18
|
-
test('returns the hosted callback URL for console mode', async () => {
|
|
19
|
-
const result = await authorize('console');
|
|
20
|
-
const url = new URL(result.url);
|
|
21
|
-
expect(url.origin).toBe('https://platform.claude.com');
|
|
22
|
-
expect(url.pathname).toBe('/oauth/authorize');
|
|
23
|
-
expect(url.searchParams.get('redirect_uri')).toBe(CODE_CALLBACK_URL);
|
|
24
|
-
});
|
|
25
|
-
test('sets required OAuth query params', async () => {
|
|
26
|
-
const result = await authorize('max');
|
|
27
|
-
const url = new URL(result.url);
|
|
28
|
-
expect(url.searchParams.get('code')).toBe('true');
|
|
29
|
-
expect(url.searchParams.get('client_id')).toBe(CLIENT_ID);
|
|
30
|
-
expect(url.searchParams.get('response_type')).toBe('code');
|
|
31
|
-
expect(url.searchParams.get('redirect_uri')).toBe(CODE_CALLBACK_URL);
|
|
32
|
-
expect(url.searchParams.get('scope')).toBe(OAUTH_SCOPES.join(' '));
|
|
33
|
-
expect(url.searchParams.get('code_challenge_method')).toBe('S256');
|
|
34
|
-
expect(url.searchParams.get('state')).toBe(result.state);
|
|
35
|
-
});
|
|
36
|
-
test('does not use localhost', async () => {
|
|
37
|
-
const result = await authorize('max');
|
|
38
|
-
expect(result.redirectUri).not.toContain('localhost');
|
|
39
|
-
expect(result.url).not.toContain('localhost');
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
describe('exchange', () => {
|
|
43
|
-
test('accepts code#state format', async () => {
|
|
44
|
-
let capturedBody;
|
|
45
|
-
spyOn(globalThis, 'fetch').mockImplementation(((_input, init) => {
|
|
46
|
-
capturedBody = init?.body;
|
|
47
|
-
return Promise.resolve(new Response(JSON.stringify({
|
|
48
|
-
refresh_token: 'r',
|
|
49
|
-
access_token: 'a',
|
|
50
|
-
expires_in: 3600,
|
|
51
|
-
}), { status: 200 }));
|
|
52
|
-
}));
|
|
53
|
-
const result = await exchange('mycode#mystate', 'myverifier', CODE_CALLBACK_URL, 'mystate');
|
|
54
|
-
expect(result.type).toBe('success');
|
|
55
|
-
const body = JSON.parse(capturedBody);
|
|
56
|
-
expect(body.code).toBe('mycode');
|
|
57
|
-
expect(body.state).toBe('mystate');
|
|
58
|
-
expect(body.redirect_uri).toBe(CODE_CALLBACK_URL);
|
|
59
|
-
});
|
|
60
|
-
test('accepts a full callback URL', async () => {
|
|
61
|
-
let capturedBody;
|
|
62
|
-
spyOn(globalThis, 'fetch').mockImplementation(((_input, init) => {
|
|
63
|
-
capturedBody = init?.body;
|
|
64
|
-
return Promise.resolve(new Response(JSON.stringify({
|
|
65
|
-
refresh_token: 'r',
|
|
66
|
-
access_token: 'a',
|
|
67
|
-
expires_in: 3600,
|
|
68
|
-
}), { status: 200 }));
|
|
69
|
-
}));
|
|
70
|
-
await exchange('https://platform.claude.com/oauth/code/callback?code=mycode&state=mystate', 'myverifier', CODE_CALLBACK_URL, 'mystate');
|
|
71
|
-
const body = JSON.parse(capturedBody);
|
|
72
|
-
expect(body.code).toBe('mycode');
|
|
73
|
-
expect(body.state).toBe('mystate');
|
|
74
|
-
});
|
|
75
|
-
test('returns failed on invalid callback input', async () => {
|
|
76
|
-
const fetchSpy = spyOn(globalThis, 'fetch').mockImplementation((() => Promise.resolve(new Response(null))));
|
|
77
|
-
const result = await exchange('not-a-callback', 'verifier', CODE_CALLBACK_URL);
|
|
78
|
-
expect(result.type).toBe('failed');
|
|
79
|
-
expect(fetchSpy).not.toHaveBeenCalled();
|
|
80
|
-
});
|
|
81
|
-
test('returns failed on state mismatch', async () => {
|
|
82
|
-
const fetchSpy = spyOn(globalThis, 'fetch').mockImplementation((() => Promise.resolve(new Response(null))));
|
|
83
|
-
const result = await exchange('code#wrong', 'verifier', CODE_CALLBACK_URL, 'expected');
|
|
84
|
-
expect(result.type).toBe('failed');
|
|
85
|
-
expect(fetchSpy).not.toHaveBeenCalled();
|
|
86
|
-
});
|
|
87
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|