openllmprovider 0.1.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 +192 -0
- package/dist/auth/index.cjs +6 -0
- package/dist/auth/index.d.cts +3 -0
- package/dist/auth/index.d.mts +3 -0
- package/dist/auth/index.mjs +3 -0
- package/dist/auto-C2hXJY13.d.cts +33 -0
- package/dist/auto-C2hXJY13.d.cts.map +1 -0
- package/dist/auto-CBqNYBXs.mjs +48 -0
- package/dist/auto-CBqNYBXs.mjs.map +1 -0
- package/dist/auto-CInerwvs.d.mts +33 -0
- package/dist/auto-CInerwvs.d.mts.map +1 -0
- package/dist/auto-D77wgMqO.cjs +59 -0
- package/dist/auto-D77wgMqO.cjs.map +1 -0
- package/dist/file-DB-rxfzi.mjs +77 -0
- package/dist/file-DB-rxfzi.mjs.map +1 -0
- package/dist/file-DZ7FGcSW.cjs +73 -0
- package/dist/file-DZ7FGcSW.cjs.map +1 -0
- package/dist/index.cjs +1909 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1239 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +1241 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +1891 -0
- package/dist/index.mjs.map +1 -0
- package/dist/logger-BsHpI_fH.mjs +11 -0
- package/dist/logger-BsHpI_fH.mjs.map +1 -0
- package/dist/logger-jRimlMFR.cjs +69 -0
- package/dist/logger-jRimlMFR.cjs.map +1 -0
- package/dist/plugin/index.cjs +29 -0
- package/dist/plugin/index.cjs.map +1 -0
- package/dist/plugin/index.d.cts +10 -0
- package/dist/plugin/index.d.cts.map +1 -0
- package/dist/plugin/index.d.mts +10 -0
- package/dist/plugin/index.d.mts.map +1 -0
- package/dist/plugin/index.mjs +25 -0
- package/dist/plugin/index.mjs.map +1 -0
- package/dist/plugin-BkeUu5LW.d.mts +46 -0
- package/dist/plugin-BkeUu5LW.d.mts.map +1 -0
- package/dist/plugin-wK7RmJhZ.d.cts +46 -0
- package/dist/plugin-wK7RmJhZ.d.cts.map +1 -0
- package/dist/resolver-BA7LWSJO.mjs +645 -0
- package/dist/resolver-BA7LWSJO.mjs.map +1 -0
- package/dist/resolver-BMTvzTt9.cjs +662 -0
- package/dist/resolver-BMTvzTt9.cjs.map +1 -0
- package/dist/resolver-MgJryMWG.d.cts +75 -0
- package/dist/resolver-MgJryMWG.d.cts.map +1 -0
- package/dist/resolver-_gfXzr_S.d.mts +76 -0
- package/dist/resolver-_gfXzr_S.d.mts.map +1 -0
- package/dist/storage/index.cjs +7 -0
- package/dist/storage/index.d.cts +12 -0
- package/dist/storage/index.d.cts.map +1 -0
- package/dist/storage/index.d.mts +12 -0
- package/dist/storage/index.d.mts.map +1 -0
- package/dist/storage/index.mjs +4 -0
- package/package.json +137 -0
- package/src/auth/.gitkeep +0 -0
- package/src/auth/index.ts +10 -0
- package/src/auth/resolver.ts +46 -0
- package/src/auth/scanners.ts +462 -0
- package/src/auth/store.ts +357 -0
- package/src/catalog/.gitkeep +0 -0
- package/src/catalog/catalog.ts +302 -0
- package/src/catalog/index.ts +17 -0
- package/src/catalog/mapper.ts +129 -0
- package/src/catalog/merger.ts +99 -0
- package/src/index.ts +37 -0
- package/src/logger.ts +7 -0
- package/src/plugin/.gitkeep +0 -0
- package/src/plugin/anthropic.test.ts +505 -0
- package/src/plugin/anthropic.ts +324 -0
- package/src/plugin/codex.ts +656 -0
- package/src/plugin/copilot.ts +161 -0
- package/src/plugin/google.ts +454 -0
- package/src/plugin/index.ts +30 -0
- package/src/provider/.gitkeep +0 -0
- package/src/provider/bundled.ts +59 -0
- package/src/provider/index.ts +249 -0
- package/src/provider/state.ts +163 -0
- package/src/storage/.gitkeep +0 -0
- package/src/storage/auto.ts +32 -0
- package/src/storage/file.ts +84 -0
- package/src/storage/index.ts +10 -0
- package/src/storage/memory.ts +23 -0
- package/src/types/.gitkeep +0 -0
- package/src/types/auth.ts +18 -0
- package/src/types/errors.ts +87 -0
- package/src/types/index.ts +26 -0
- package/src/types/model.ts +88 -0
- package/src/types/plugin.ts +49 -0
- package/src/types/provider.ts +48 -0
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test'
|
|
2
|
+
import type { AuthCredential } from '../types/plugin.js'
|
|
3
|
+
import { anthropicPlugin } from './anthropic.js'
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Anthropic plugin supports two auth modes. These tests verify the loader
|
|
7
|
+
// behavior for each:
|
|
8
|
+
//
|
|
9
|
+
// 1. API key auth (type: 'api')
|
|
10
|
+
// - The loader returns {} immediately — no custom SDK config needed.
|
|
11
|
+
// - The SDK uses the `key` field via the standard x-api-key header.
|
|
12
|
+
//
|
|
13
|
+
// 2. OAuth auth (type: 'oauth')
|
|
14
|
+
// - The loader inspects the credential and may:
|
|
15
|
+
// a. Refresh an expired access_token using the refresh_token
|
|
16
|
+
// b. Use a token directly as apiKey if it looks like an API key (sk-ant-*)
|
|
17
|
+
// c. Exchange an OAuth access_token for a real API key via Anthropic API
|
|
18
|
+
// d. Fall back to Bearer token auth with a custom fetch wrapper
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
const originalFetch = globalThis.fetch
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
// Reset fetch mock before each test
|
|
25
|
+
globalThis.fetch = originalFetch
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
globalThis.fetch = originalFetch
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
function makeGetAuth(credential: AuthCredential) {
|
|
33
|
+
return async () => credential
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// =====================================================================
|
|
37
|
+
// 1. API key auth
|
|
38
|
+
// =====================================================================
|
|
39
|
+
describe('anthropic loader — API key auth', () => {
|
|
40
|
+
it('returns empty config for type=api (SDK handles x-api-key automatically)', async () => {
|
|
41
|
+
const getAuth = makeGetAuth({
|
|
42
|
+
type: 'api',
|
|
43
|
+
key: 'sk-ant-api03-test-key',
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
const result = await anthropicPlugin.loader(getAuth, { id: 'anthropic' }, async () => {})
|
|
47
|
+
expect(result).toEqual({})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('returns empty config for type=wellknown', async () => {
|
|
51
|
+
const getAuth = makeGetAuth({
|
|
52
|
+
type: 'wellknown',
|
|
53
|
+
key: 'sk-ant-api03-discovered-key',
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const result = await anthropicPlugin.loader(getAuth, { id: 'anthropic' }, async () => {})
|
|
57
|
+
expect(result).toEqual({})
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
// =====================================================================
|
|
62
|
+
// 2. OAuth auth
|
|
63
|
+
// =====================================================================
|
|
64
|
+
describe('anthropic loader — OAuth auth', () => {
|
|
65
|
+
it('uses token as apiKey when it looks like an API key (sk-ant-api03-*)', async () => {
|
|
66
|
+
// After OAuth flow, the token was already exchanged for an API key and
|
|
67
|
+
// stored back in `key`. On next load, it looks like an API key directly.
|
|
68
|
+
const getAuth = makeGetAuth({
|
|
69
|
+
type: 'oauth',
|
|
70
|
+
key: 'sk-ant-api03-already-exchanged',
|
|
71
|
+
expires: Date.now() + 3600_000, // not expired
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const result = await anthropicPlugin.loader(getAuth, { id: 'anthropic' }, async () => {})
|
|
75
|
+
expect(result).toEqual({ apiKey: 'sk-ant-api03-already-exchanged' })
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('exchanges OAuth access_token for API key via create_api_key endpoint', async () => {
|
|
79
|
+
const getAuth = makeGetAuth({
|
|
80
|
+
type: 'oauth',
|
|
81
|
+
key: 'sk-ant-oat-valid-access-token',
|
|
82
|
+
refresh: 'sk-ant-ort-valid-refresh-token',
|
|
83
|
+
expires: Date.now() + 3600_000, // not expired
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
// Mock the API key exchange endpoint
|
|
87
|
+
globalThis.fetch = mock(async (url: string | URL | Request) => {
|
|
88
|
+
const urlStr = typeof url === 'string' ? url : url instanceof URL ? url.toString() : url.url
|
|
89
|
+
if (urlStr.includes('create_api_key')) {
|
|
90
|
+
return new Response(JSON.stringify({ raw_key: 'sk-ant-api03-exchanged-key' }), {
|
|
91
|
+
status: 200,
|
|
92
|
+
headers: { 'Content-Type': 'application/json' },
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
throw new Error(`Unexpected fetch: ${urlStr}`)
|
|
96
|
+
}) as unknown as typeof fetch
|
|
97
|
+
|
|
98
|
+
const result = await anthropicPlugin.loader(getAuth, { id: 'anthropic' }, async () => {})
|
|
99
|
+
expect(result).toEqual({ apiKey: 'sk-ant-api03-exchanged-key' })
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('refreshes expired token before exchanging for API key', async () => {
|
|
103
|
+
const getAuth = makeGetAuth({
|
|
104
|
+
type: 'oauth',
|
|
105
|
+
key: 'sk-ant-oat-expired-access-token',
|
|
106
|
+
refresh: 'sk-ant-ort-valid-refresh-token',
|
|
107
|
+
expires: Date.now() - 1000, // already expired
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
const fetchCalls: string[] = []
|
|
111
|
+
|
|
112
|
+
globalThis.fetch = mock(async (url: string | URL | Request) => {
|
|
113
|
+
const urlStr = typeof url === 'string' ? url : url instanceof URL ? url.toString() : url.url
|
|
114
|
+
fetchCalls.push(urlStr)
|
|
115
|
+
|
|
116
|
+
// Token refresh endpoint
|
|
117
|
+
if (urlStr.includes('/v1/oauth/token')) {
|
|
118
|
+
return new Response(
|
|
119
|
+
JSON.stringify({
|
|
120
|
+
access_token: 'sk-ant-oat-refreshed-token',
|
|
121
|
+
refresh_token: 'sk-ant-ort-new-refresh',
|
|
122
|
+
expires_in: 3600,
|
|
123
|
+
}),
|
|
124
|
+
{ status: 200, headers: { 'Content-Type': 'application/json' } }
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// API key exchange endpoint (called with refreshed token)
|
|
129
|
+
if (urlStr.includes('create_api_key')) {
|
|
130
|
+
return new Response(JSON.stringify({ raw_key: 'sk-ant-api03-from-refreshed' }), {
|
|
131
|
+
status: 200,
|
|
132
|
+
headers: { 'Content-Type': 'application/json' },
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
throw new Error(`Unexpected fetch: ${urlStr}`)
|
|
137
|
+
}) as unknown as typeof fetch
|
|
138
|
+
|
|
139
|
+
const result = await anthropicPlugin.loader(getAuth, { id: 'anthropic' }, async () => {})
|
|
140
|
+
|
|
141
|
+
// Should have called refresh first, then exchange
|
|
142
|
+
expect(fetchCalls).toHaveLength(2)
|
|
143
|
+
expect(fetchCalls[0]).toContain('/v1/oauth/token')
|
|
144
|
+
expect(fetchCalls[1]).toContain('create_api_key')
|
|
145
|
+
expect(result).toEqual({ apiKey: 'sk-ant-api03-from-refreshed' })
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('falls back to Bearer auth when API key exchange fails', async () => {
|
|
149
|
+
const credential: AuthCredential = {
|
|
150
|
+
type: 'oauth',
|
|
151
|
+
key: 'sk-ant-oat-valid-but-exchange-fails',
|
|
152
|
+
expires: Date.now() + 3600_000,
|
|
153
|
+
}
|
|
154
|
+
const getAuth = makeGetAuth(credential)
|
|
155
|
+
|
|
156
|
+
globalThis.fetch = mock(async (url: string | URL | Request) => {
|
|
157
|
+
const urlStr = typeof url === 'string' ? url : url instanceof URL ? url.toString() : url.url
|
|
158
|
+
|
|
159
|
+
// API key exchange fails
|
|
160
|
+
if (urlStr.includes('create_api_key')) {
|
|
161
|
+
return new Response('Forbidden', { status: 403 })
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// The fallback custom fetch — just return a dummy response
|
|
165
|
+
return new Response('ok', { status: 200 })
|
|
166
|
+
}) as unknown as typeof fetch
|
|
167
|
+
|
|
168
|
+
const result = await anthropicPlugin.loader(getAuth, { id: 'anthropic' }, async () => {})
|
|
169
|
+
|
|
170
|
+
// Should return headers + custom fetch (Bearer fallback)
|
|
171
|
+
expect(result).toHaveProperty('headers')
|
|
172
|
+
expect(result).toHaveProperty('fetch')
|
|
173
|
+
expect((result.headers as Record<string, string>)['anthropic-beta']).toBeDefined()
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('falls back to Bearer auth when token is empty after refresh failure', async () => {
|
|
177
|
+
const getAuth = makeGetAuth({
|
|
178
|
+
type: 'oauth',
|
|
179
|
+
key: undefined, // no access token
|
|
180
|
+
refresh: 'sk-ant-ort-valid-refresh-token',
|
|
181
|
+
expires: Date.now() - 1000, // expired
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
globalThis.fetch = mock(async () => {
|
|
185
|
+
// Refresh fails
|
|
186
|
+
return new Response('Server Error', { status: 500 })
|
|
187
|
+
}) as unknown as typeof fetch
|
|
188
|
+
|
|
189
|
+
const result = await anthropicPlugin.loader(getAuth, { id: 'anthropic' }, async () => {})
|
|
190
|
+
|
|
191
|
+
// No valid token — falls through to Bearer fallback
|
|
192
|
+
expect(result).toHaveProperty('headers')
|
|
193
|
+
expect(result).toHaveProperty('fetch')
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('does not refresh when token is not expired', async () => {
|
|
197
|
+
const getAuth = makeGetAuth({
|
|
198
|
+
type: 'oauth',
|
|
199
|
+
key: 'sk-ant-oat-still-valid',
|
|
200
|
+
refresh: 'sk-ant-ort-should-not-be-used',
|
|
201
|
+
expires: Date.now() + 3600_000, // still valid
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
const fetchCalls: string[] = []
|
|
205
|
+
|
|
206
|
+
globalThis.fetch = mock(async (url: string | URL | Request) => {
|
|
207
|
+
const urlStr = typeof url === 'string' ? url : url instanceof URL ? url.toString() : url.url
|
|
208
|
+
fetchCalls.push(urlStr)
|
|
209
|
+
|
|
210
|
+
if (urlStr.includes('create_api_key')) {
|
|
211
|
+
return new Response(JSON.stringify({ raw_key: 'sk-ant-api03-from-valid' }), {
|
|
212
|
+
status: 200,
|
|
213
|
+
headers: { 'Content-Type': 'application/json' },
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
throw new Error(`Unexpected fetch: ${urlStr}`)
|
|
218
|
+
}) as unknown as typeof fetch
|
|
219
|
+
|
|
220
|
+
const result = await anthropicPlugin.loader(getAuth, { id: 'anthropic' }, async () => {})
|
|
221
|
+
|
|
222
|
+
// Should NOT call refresh endpoint — only API key exchange
|
|
223
|
+
expect(fetchCalls).toHaveLength(1)
|
|
224
|
+
expect(fetchCalls[0]).toContain('create_api_key')
|
|
225
|
+
expect(result).toEqual({ apiKey: 'sk-ant-api03-from-valid' })
|
|
226
|
+
})
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
// =====================================================================
|
|
231
|
+
// Bearer fallback: per-request token refresh (the bug that was fixed)
|
|
232
|
+
// =====================================================================
|
|
233
|
+
describe('anthropic loader — Bearer fallback refresh', () => {
|
|
234
|
+
it('refreshes expired token inside fallback fetch (not just at loader setup)', async () => {
|
|
235
|
+
// This is the core bug scenario: token is expired, API key exchange fails,
|
|
236
|
+
// the Bearer fallback fetch MUST refresh the token per-request (like google/codex).
|
|
237
|
+
// Previously, the fallback just used getAuth().key directly — the old expired token.
|
|
238
|
+
const storedAuth: AuthCredential = {
|
|
239
|
+
type: 'oauth',
|
|
240
|
+
key: 'sk-ant-oat-expired-token',
|
|
241
|
+
refresh: 'sk-ant-ort-valid-refresh',
|
|
242
|
+
expires: Date.now() - 1000,
|
|
243
|
+
}
|
|
244
|
+
const getAuth = makeGetAuth(storedAuth)
|
|
245
|
+
|
|
246
|
+
let phase: 'loader' | 'fetch' = 'loader'
|
|
247
|
+
const fetchCalls: string[] = []
|
|
248
|
+
|
|
249
|
+
globalThis.fetch = mock(async (url: string | URL | Request, init?: RequestInit) => {
|
|
250
|
+
const urlStr = typeof url === 'string' ? url : url instanceof URL ? url.toString() : url.url
|
|
251
|
+
fetchCalls.push(urlStr)
|
|
252
|
+
|
|
253
|
+
if (urlStr.includes('/v1/oauth/token')) {
|
|
254
|
+
return new Response(
|
|
255
|
+
JSON.stringify({
|
|
256
|
+
access_token: 'sk-ant-oat-fresh-token',
|
|
257
|
+
refresh_token: 'sk-ant-ort-new-refresh',
|
|
258
|
+
expires_in: 3600,
|
|
259
|
+
}),
|
|
260
|
+
{ status: 200, headers: { 'Content-Type': 'application/json' } },
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (urlStr.includes('create_api_key')) {
|
|
265
|
+
return new Response('Forbidden', { status: 403 })
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (phase === 'fetch') {
|
|
269
|
+
// Actual API call — verify it uses the refreshed token
|
|
270
|
+
const authHeader = init?.headers instanceof Headers
|
|
271
|
+
? init.headers.get('Authorization')
|
|
272
|
+
: undefined
|
|
273
|
+
expect(authHeader).toBe('Bearer sk-ant-oat-fresh-token')
|
|
274
|
+
return new Response('ok', { status: 200 })
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
throw new Error(`Unexpected fetch: ${urlStr}`)
|
|
278
|
+
}) as unknown as typeof fetch
|
|
279
|
+
|
|
280
|
+
// Step 1: loader runs — refresh + exchange fail → Bearer fallback returned
|
|
281
|
+
const result = await anthropicPlugin.loader(getAuth, { id: 'anthropic' }, async () => {})
|
|
282
|
+
expect(result).toHaveProperty('fetch')
|
|
283
|
+
const customFetch = result.fetch as typeof globalThis.fetch
|
|
284
|
+
|
|
285
|
+
// Step 2: simulate an API call through the fallback fetch
|
|
286
|
+
phase = 'fetch'
|
|
287
|
+
fetchCalls.length = 0
|
|
288
|
+
await customFetch('https://api.anthropic.com/v1/messages', {})
|
|
289
|
+
|
|
290
|
+
// The fallback fetch should have refreshed the token before the API call
|
|
291
|
+
expect(fetchCalls[0]).toContain('/v1/oauth/token')
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
it('uses non-expired token as-is in fallback fetch (no unnecessary refresh)', async () => {
|
|
295
|
+
// exchange fails → Bearer fallback, but token is still valid → no refresh in fetch
|
|
296
|
+
const storedAuth: AuthCredential = {
|
|
297
|
+
type: 'oauth',
|
|
298
|
+
key: 'sk-ant-oat-still-valid',
|
|
299
|
+
expires: Date.now() + 3600_000,
|
|
300
|
+
}
|
|
301
|
+
const getAuth = makeGetAuth(storedAuth)
|
|
302
|
+
|
|
303
|
+
let phase: 'loader' | 'fetch' = 'loader'
|
|
304
|
+
const fetchCalls: string[] = []
|
|
305
|
+
|
|
306
|
+
globalThis.fetch = mock(async (url: string | URL | Request, init?: RequestInit) => {
|
|
307
|
+
const urlStr = typeof url === 'string' ? url : url instanceof URL ? url.toString() : url.url
|
|
308
|
+
fetchCalls.push(urlStr)
|
|
309
|
+
|
|
310
|
+
if (urlStr.includes('create_api_key')) {
|
|
311
|
+
return new Response('Forbidden', { status: 403 })
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (phase === 'fetch') {
|
|
315
|
+
const authHeader = init?.headers instanceof Headers
|
|
316
|
+
? init.headers.get('Authorization')
|
|
317
|
+
: undefined
|
|
318
|
+
expect(authHeader).toBe('Bearer sk-ant-oat-still-valid')
|
|
319
|
+
return new Response('ok', { status: 200 })
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
throw new Error(`Unexpected fetch: ${urlStr}`)
|
|
323
|
+
}) as unknown as typeof fetch
|
|
324
|
+
|
|
325
|
+
const result = await anthropicPlugin.loader(getAuth, { id: 'anthropic' }, async () => {})
|
|
326
|
+
expect(result).toHaveProperty('fetch')
|
|
327
|
+
const customFetch = result.fetch as typeof globalThis.fetch
|
|
328
|
+
|
|
329
|
+
phase = 'fetch'
|
|
330
|
+
fetchCalls.length = 0
|
|
331
|
+
await customFetch('https://api.anthropic.com/v1/messages', {})
|
|
332
|
+
|
|
333
|
+
// No refresh call — only the actual API call
|
|
334
|
+
expect(fetchCalls.every((u) => !u.includes('/v1/oauth/token'))).toBe(true)
|
|
335
|
+
})
|
|
336
|
+
})
|
|
337
|
+
// =====================================================================
|
|
338
|
+
// Token type detection helpers
|
|
339
|
+
// =====================================================================
|
|
340
|
+
describe('anthropic loader — token type detection', () => {
|
|
341
|
+
it('recognizes sk-ant-oat-* as OAuth token (not API key)', async () => {
|
|
342
|
+
const getAuth = makeGetAuth({
|
|
343
|
+
type: 'oauth',
|
|
344
|
+
key: 'sk-ant-oat-some-oauth-token',
|
|
345
|
+
expires: Date.now() + 3600_000,
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
// If it were treated as API key, it would return { apiKey: ... } without
|
|
349
|
+
// calling create_api_key. Since it's an OAuth token, it should call the
|
|
350
|
+
// exchange endpoint.
|
|
351
|
+
globalThis.fetch = mock(async (url: string | URL | Request) => {
|
|
352
|
+
const urlStr = typeof url === 'string' ? url : url instanceof URL ? url.toString() : url.url
|
|
353
|
+
if (urlStr.includes('create_api_key')) {
|
|
354
|
+
return new Response(JSON.stringify({ raw_key: 'sk-ant-api03-exchanged' }), {
|
|
355
|
+
status: 200,
|
|
356
|
+
headers: { 'Content-Type': 'application/json' },
|
|
357
|
+
})
|
|
358
|
+
}
|
|
359
|
+
throw new Error(`Unexpected fetch: ${urlStr}`)
|
|
360
|
+
}) as unknown as typeof fetch
|
|
361
|
+
|
|
362
|
+
const result = await anthropicPlugin.loader(getAuth, { id: 'anthropic' }, async () => {})
|
|
363
|
+
expect(result).toEqual({ apiKey: 'sk-ant-api03-exchanged' })
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
it('recognizes sk-ant-api03-* as direct API key (skips exchange)', async () => {
|
|
367
|
+
const getAuth = makeGetAuth({
|
|
368
|
+
type: 'oauth',
|
|
369
|
+
key: 'sk-ant-api03-direct-key',
|
|
370
|
+
expires: Date.now() + 3600_000,
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
// fetch should never be called since the token is recognized as an API key
|
|
374
|
+
globalThis.fetch = mock(async () => {
|
|
375
|
+
throw new Error('fetch should not be called for API key tokens')
|
|
376
|
+
}) as unknown as typeof fetch
|
|
377
|
+
|
|
378
|
+
const result = await anthropicPlugin.loader(getAuth, { id: 'anthropic' }, async () => {})
|
|
379
|
+
expect(result).toEqual({ apiKey: 'sk-ant-api03-direct-key' })
|
|
380
|
+
})
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
// =====================================================================
|
|
385
|
+
// setAuth persistence: verify refreshed tokens are written back
|
|
386
|
+
// =====================================================================
|
|
387
|
+
describe('anthropic loader — setAuth persistence', () => {
|
|
388
|
+
it('calls setAuth with refreshed credential when token is expired (loader path)', async () => {
|
|
389
|
+
const getAuth = makeGetAuth({
|
|
390
|
+
type: 'oauth',
|
|
391
|
+
key: 'sk-ant-oat-expired',
|
|
392
|
+
refresh: 'sk-ant-ort-refresh',
|
|
393
|
+
expires: Date.now() - 1000,
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
globalThis.fetch = mock(async (url: string | URL | Request) => {
|
|
397
|
+
const urlStr = typeof url === 'string' ? url : url instanceof URL ? url.toString() : url.url
|
|
398
|
+
if (urlStr.includes('/v1/oauth/token')) {
|
|
399
|
+
return new Response(
|
|
400
|
+
JSON.stringify({
|
|
401
|
+
access_token: 'sk-ant-oat-new-access',
|
|
402
|
+
refresh_token: 'sk-ant-ort-new-refresh',
|
|
403
|
+
expires_in: 7200,
|
|
404
|
+
}),
|
|
405
|
+
{ status: 200, headers: { 'Content-Type': 'application/json' } },
|
|
406
|
+
)
|
|
407
|
+
}
|
|
408
|
+
if (urlStr.includes('create_api_key')) {
|
|
409
|
+
return new Response(JSON.stringify({ raw_key: 'sk-ant-api03-exchanged' }), {
|
|
410
|
+
status: 200,
|
|
411
|
+
headers: { 'Content-Type': 'application/json' },
|
|
412
|
+
})
|
|
413
|
+
}
|
|
414
|
+
throw new Error(`Unexpected fetch: ${urlStr}`)
|
|
415
|
+
}) as unknown as typeof fetch
|
|
416
|
+
|
|
417
|
+
const persisted: AuthCredential[] = []
|
|
418
|
+
const setAuth = async (cred: AuthCredential) => { persisted.push(cred) }
|
|
419
|
+
|
|
420
|
+
await anthropicPlugin.loader(getAuth, { id: 'anthropic' }, setAuth)
|
|
421
|
+
|
|
422
|
+
expect(persisted).toHaveLength(1)
|
|
423
|
+
expect(persisted[0].key).toBe('sk-ant-oat-new-access')
|
|
424
|
+
expect(persisted[0].refresh).toBe('sk-ant-ort-new-refresh')
|
|
425
|
+
expect(persisted[0].type).toBe('oauth')
|
|
426
|
+
expect(typeof persisted[0].expires).toBe('number')
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
it('calls setAuth in Bearer fallback fetch when token is refreshed', async () => {
|
|
430
|
+
const storedAuth: AuthCredential = {
|
|
431
|
+
type: 'oauth',
|
|
432
|
+
key: 'sk-ant-oat-expired-bearer',
|
|
433
|
+
refresh: 'sk-ant-ort-refresh-bearer',
|
|
434
|
+
expires: Date.now() - 1000,
|
|
435
|
+
}
|
|
436
|
+
const getAuth = makeGetAuth(storedAuth)
|
|
437
|
+
|
|
438
|
+
let phase: 'loader' | 'fetch' = 'loader'
|
|
439
|
+
|
|
440
|
+
globalThis.fetch = mock(async (url: string | URL | Request) => {
|
|
441
|
+
const urlStr = typeof url === 'string' ? url : url instanceof URL ? url.toString() : url.url
|
|
442
|
+
if (urlStr.includes('/v1/oauth/token')) {
|
|
443
|
+
return new Response(
|
|
444
|
+
JSON.stringify({
|
|
445
|
+
access_token: 'sk-ant-oat-bearer-refreshed',
|
|
446
|
+
refresh_token: 'sk-ant-ort-bearer-new',
|
|
447
|
+
expires_in: 3600,
|
|
448
|
+
}),
|
|
449
|
+
{ status: 200, headers: { 'Content-Type': 'application/json' } },
|
|
450
|
+
)
|
|
451
|
+
}
|
|
452
|
+
if (urlStr.includes('create_api_key')) {
|
|
453
|
+
return new Response('Forbidden', { status: 403 })
|
|
454
|
+
}
|
|
455
|
+
if (phase === 'fetch') {
|
|
456
|
+
return new Response('ok', { status: 200 })
|
|
457
|
+
}
|
|
458
|
+
throw new Error(`Unexpected fetch: ${urlStr}`)
|
|
459
|
+
}) as unknown as typeof fetch
|
|
460
|
+
|
|
461
|
+
const persisted: AuthCredential[] = []
|
|
462
|
+
const setAuth = async (cred: AuthCredential) => { persisted.push(cred) }
|
|
463
|
+
|
|
464
|
+
// Loader: refresh + exchange fail → Bearer fallback
|
|
465
|
+
const result = await anthropicPlugin.loader(getAuth, { id: 'anthropic' }, setAuth)
|
|
466
|
+
const loaderPersistCount = persisted.length
|
|
467
|
+
expect(loaderPersistCount).toBe(1) // refresh during loader setup
|
|
468
|
+
|
|
469
|
+
// Fetch: triggers another refresh (getAuth returns expired token again)
|
|
470
|
+
phase = 'fetch'
|
|
471
|
+
const customFetch = result.fetch as typeof globalThis.fetch
|
|
472
|
+
await customFetch('https://api.anthropic.com/v1/messages', {})
|
|
473
|
+
|
|
474
|
+
expect(persisted.length).toBeGreaterThan(loaderPersistCount)
|
|
475
|
+
expect(persisted[persisted.length - 1].key).toBe('sk-ant-oat-bearer-refreshed')
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
it('does not call setAuth when token is not expired', async () => {
|
|
479
|
+
const getAuth = makeGetAuth({
|
|
480
|
+
type: 'oauth',
|
|
481
|
+
key: 'sk-ant-oat-still-good',
|
|
482
|
+
refresh: 'sk-ant-ort-not-needed',
|
|
483
|
+
expires: Date.now() + 3600_000,
|
|
484
|
+
})
|
|
485
|
+
|
|
486
|
+
globalThis.fetch = mock(async (url: string | URL | Request) => {
|
|
487
|
+
const urlStr = typeof url === 'string' ? url : url instanceof URL ? url.toString() : url.url
|
|
488
|
+
if (urlStr.includes('create_api_key')) {
|
|
489
|
+
return new Response(JSON.stringify({ raw_key: 'sk-ant-api03-from-valid' }), {
|
|
490
|
+
status: 200,
|
|
491
|
+
headers: { 'Content-Type': 'application/json' },
|
|
492
|
+
})
|
|
493
|
+
}
|
|
494
|
+
throw new Error(`Unexpected fetch: ${urlStr}`)
|
|
495
|
+
}) as unknown as typeof fetch
|
|
496
|
+
|
|
497
|
+
const persisted: AuthCredential[] = []
|
|
498
|
+
const setAuth = async (cred: AuthCredential) => { persisted.push(cred) }
|
|
499
|
+
|
|
500
|
+
await anthropicPlugin.loader(getAuth, { id: 'anthropic' }, setAuth)
|
|
501
|
+
|
|
502
|
+
// No refresh happened, so setAuth should not be called
|
|
503
|
+
expect(persisted).toHaveLength(0)
|
|
504
|
+
})
|
|
505
|
+
})
|