@antseed/provider-core 0.1.0 → 0.1.2

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.
@@ -1,269 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import { HttpRelay, type RelayConfig, type RelayCallbacks } from './http-relay.js';
3
- import type { SerializedHttpRequest, SerializedHttpResponse } from '@antseed/node';
4
-
5
- function makeRequest(overrides?: Partial<SerializedHttpRequest>): SerializedHttpRequest {
6
- return {
7
- requestId: 'req-1',
8
- method: 'POST',
9
- path: '/v1/messages',
10
- headers: { 'content-type': 'application/json' },
11
- body: new TextEncoder().encode(JSON.stringify({ model: 'claude-sonnet-4-20250514', messages: [] })),
12
- ...overrides,
13
- };
14
- }
15
-
16
- function makeConfig(overrides?: Partial<RelayConfig>): RelayConfig {
17
- return {
18
- baseUrl: 'https://api.example.com',
19
- authHeaderName: 'x-api-key',
20
- authHeaderValue: 'sk-test-key',
21
- maxConcurrency: 2,
22
- allowedModels: ['claude-sonnet-4-20250514'],
23
- ...overrides,
24
- };
25
- }
26
-
27
- describe('HttpRelay', () => {
28
- let fetchMock: ReturnType<typeof vi.fn>;
29
- const originalFetch = globalThis.fetch;
30
-
31
- beforeEach(() => {
32
- fetchMock = vi.fn();
33
- globalThis.fetch = fetchMock;
34
- });
35
-
36
- afterEach(() => {
37
- globalThis.fetch = originalFetch;
38
- });
39
-
40
- it('relays a successful non-streaming response', async () => {
41
- const responseBody = JSON.stringify({ id: 'msg_1', content: [{ text: 'Hello' }] });
42
- fetchMock.mockResolvedValueOnce(new Response(responseBody, {
43
- status: 200,
44
- headers: { 'content-type': 'application/json', 'request-id': 'upstream-1' },
45
- }));
46
-
47
- const responses: SerializedHttpResponse[] = [];
48
- const callbacks: RelayCallbacks = {
49
- onResponse: (res) => responses.push(res),
50
- };
51
-
52
- const relay = new HttpRelay(makeConfig(), callbacks);
53
- await relay.handleRequest(makeRequest());
54
-
55
- expect(responses).toHaveLength(1);
56
- expect(responses[0]!.statusCode).toBe(200);
57
- expect(responses[0]!.requestId).toBe('req-1');
58
- expect(responses[0]!.headers['content-type']).toBe('application/json');
59
-
60
- // Verify fetch was called with the right URL and auth
61
- expect(fetchMock).toHaveBeenCalledTimes(1);
62
- const [url, opts] = fetchMock.mock.calls[0] as [string, RequestInit];
63
- expect(url).toBe('https://api.example.com/v1/messages');
64
- expect((opts.headers as Record<string, string>)['x-api-key']).toBe('sk-test-key');
65
- });
66
-
67
- it('rejects disallowed model', async () => {
68
- const responses: SerializedHttpResponse[] = [];
69
- const callbacks: RelayCallbacks = {
70
- onResponse: (res) => responses.push(res),
71
- };
72
-
73
- const relay = new HttpRelay(makeConfig(), callbacks);
74
- const req = makeRequest({
75
- body: new TextEncoder().encode(JSON.stringify({ model: 'gpt-4', messages: [] })),
76
- });
77
- await relay.handleRequest(req);
78
-
79
- expect(responses).toHaveLength(1);
80
- expect(responses[0]!.statusCode).toBe(403);
81
- const body = JSON.parse(new TextDecoder().decode(responses[0]!.body)) as { error: string };
82
- expect(body.error).toContain('not in the allowed list');
83
- expect(fetchMock).not.toHaveBeenCalled();
84
- });
85
-
86
- it('allows any model when allowedModels is empty', async () => {
87
- fetchMock.mockResolvedValueOnce(new Response('{}', { status: 200 }));
88
-
89
- const responses: SerializedHttpResponse[] = [];
90
- const callbacks: RelayCallbacks = {
91
- onResponse: (res) => responses.push(res),
92
- };
93
-
94
- const relay = new HttpRelay(makeConfig({ allowedModels: [] }), callbacks);
95
- const req = makeRequest({
96
- body: new TextEncoder().encode(JSON.stringify({ model: 'any-model', messages: [] })),
97
- });
98
- await relay.handleRequest(req);
99
-
100
- expect(responses).toHaveLength(1);
101
- expect(responses[0]!.statusCode).toBe(200);
102
- });
103
-
104
- it('enforces concurrency limit', async () => {
105
- // Create a fetch that blocks until we resolve it
106
- let resolveFirst!: (value: Response) => void;
107
- const firstFetch = new Promise<Response>((resolve) => { resolveFirst = resolve; });
108
-
109
- fetchMock.mockReturnValueOnce(firstFetch);
110
-
111
- const responses: SerializedHttpResponse[] = [];
112
- const callbacks: RelayCallbacks = {
113
- onResponse: (res) => responses.push(res),
114
- };
115
-
116
- const relay = new HttpRelay(makeConfig({ maxConcurrency: 1 }), callbacks);
117
-
118
- // Start first request (fills concurrency) — do NOT await
119
- const p1 = relay.handleRequest(makeRequest({ requestId: 'req-1' }));
120
-
121
- // Yield to allow the first handleRequest to progress to its await
122
- await new Promise((r) => setTimeout(r, 0));
123
-
124
- // Active count should be 1 now
125
- expect(relay.getActiveCount()).toBe(1);
126
-
127
- // Second request should be rejected (concurrency full)
128
- await relay.handleRequest(makeRequest({ requestId: 'req-2' }));
129
- expect(responses).toHaveLength(1);
130
- expect(responses[0]!.requestId).toBe('req-2');
131
- expect(responses[0]!.statusCode).toBe(429);
132
-
133
- // Complete first request
134
- resolveFirst(new Response('{}', { status: 200 }));
135
- await p1;
136
- expect(responses).toHaveLength(2);
137
- expect(responses[1]!.requestId).toBe('req-1');
138
- expect(responses[1]!.statusCode).toBe(200);
139
-
140
- // Now concurrency is free, third request should work
141
- fetchMock.mockResolvedValueOnce(new Response('{}', { status: 200 }));
142
- await relay.handleRequest(makeRequest({ requestId: 'req-3' }));
143
- expect(responses).toHaveLength(3);
144
- expect(responses[2]!.requestId).toBe('req-3');
145
- expect(responses[2]!.statusCode).toBe(200);
146
- });
147
-
148
- it('strips hop-by-hop and internal headers from request', async () => {
149
- fetchMock.mockResolvedValueOnce(new Response('{}', { status: 200 }));
150
-
151
- const responses: SerializedHttpResponse[] = [];
152
- const callbacks: RelayCallbacks = {
153
- onResponse: (res) => responses.push(res),
154
- };
155
-
156
- const relay = new HttpRelay(makeConfig(), callbacks);
157
- await relay.handleRequest(makeRequest({
158
- headers: {
159
- 'content-type': 'application/json',
160
- 'connection': 'keep-alive',
161
- 'x-antseed-provider': 'anthropic',
162
- 'host': 'localhost:3000',
163
- 'x-custom': 'keep-me',
164
- },
165
- }));
166
-
167
- const [, opts] = fetchMock.mock.calls[0] as [string, RequestInit];
168
- const sentHeaders = opts.headers as Record<string, string>;
169
- expect(sentHeaders['connection']).toBeUndefined();
170
- expect(sentHeaders['x-antseed-provider']).toBeUndefined();
171
- expect(sentHeaders['host']).toBeUndefined();
172
- expect(sentHeaders['x-custom']).toBe('keep-me');
173
- });
174
-
175
- it('uses tokenProvider when present', async () => {
176
- fetchMock.mockResolvedValueOnce(new Response('{}', { status: 200 }));
177
-
178
- const responses: SerializedHttpResponse[] = [];
179
- const callbacks: RelayCallbacks = {
180
- onResponse: (res) => responses.push(res),
181
- };
182
-
183
- const tokenProvider = {
184
- getToken: vi.fn().mockResolvedValue('fresh-token'),
185
- stop: vi.fn(),
186
- };
187
-
188
- const relay = new HttpRelay(
189
- makeConfig({
190
- authHeaderName: 'authorization',
191
- authHeaderValue: 'Bearer old-token',
192
- tokenProvider,
193
- }),
194
- callbacks,
195
- );
196
-
197
- await relay.handleRequest(makeRequest());
198
-
199
- expect(tokenProvider.getToken).toHaveBeenCalledOnce();
200
- const [, opts] = fetchMock.mock.calls[0] as [string, RequestInit];
201
- const sentHeaders = opts.headers as Record<string, string>;
202
- expect(sentHeaders['authorization']).toBe('Bearer fresh-token');
203
- });
204
-
205
- it('returns 502 on fetch failure', async () => {
206
- fetchMock.mockRejectedValueOnce(new Error('Connection refused'));
207
-
208
- const responses: SerializedHttpResponse[] = [];
209
- const callbacks: RelayCallbacks = {
210
- onResponse: (res) => responses.push(res),
211
- };
212
-
213
- const relay = new HttpRelay(makeConfig(), callbacks);
214
- await relay.handleRequest(makeRequest());
215
-
216
- expect(responses).toHaveLength(1);
217
- expect(responses[0]!.statusCode).toBe(502);
218
- const body = JSON.parse(new TextDecoder().decode(responses[0]!.body)) as { error: string };
219
- expect(body.error).toContain('Connection refused');
220
- });
221
-
222
- it('accumulates SSE response into complete body', async () => {
223
- const sseChunks = [
224
- 'event: message\ndata: {"text":"Hello"}\n\n',
225
- 'event: message\ndata: {"text":"World"}\n\n',
226
- ];
227
- const stream = new ReadableStream({
228
- start(controller) {
229
- for (const chunk of sseChunks) {
230
- controller.enqueue(new TextEncoder().encode(chunk));
231
- }
232
- controller.close();
233
- },
234
- });
235
-
236
- fetchMock.mockResolvedValueOnce(new Response(stream, {
237
- status: 200,
238
- headers: { 'content-type': 'text/event-stream' },
239
- }));
240
-
241
- const responses: SerializedHttpResponse[] = [];
242
- const callbacks: RelayCallbacks = {
243
- onResponse: (res) => responses.push(res),
244
- };
245
-
246
- const relay = new HttpRelay(makeConfig(), callbacks);
247
- await relay.handleRequest(makeRequest());
248
-
249
- expect(responses).toHaveLength(1);
250
- expect(responses[0]!.statusCode).toBe(200);
251
- const bodyText = new TextDecoder().decode(responses[0]!.body);
252
- expect(bodyText).toContain('Hello');
253
- expect(bodyText).toContain('World');
254
- });
255
-
256
- it('tracks active count correctly', async () => {
257
- fetchMock.mockResolvedValue(new Response('{}', { status: 200 }));
258
-
259
- const callbacks: RelayCallbacks = {
260
- onResponse: () => {},
261
- };
262
-
263
- const relay = new HttpRelay(makeConfig(), callbacks);
264
- expect(relay.getActiveCount()).toBe(0);
265
-
266
- await relay.handleRequest(makeRequest());
267
- expect(relay.getActiveCount()).toBe(0); // decremented after completion
268
- });
269
- });
package/src/http-relay.ts DELETED
@@ -1,187 +0,0 @@
1
- import type { TokenProvider } from '@antseed/node';
2
- import type { SerializedHttpRequest, SerializedHttpResponse, SerializedHttpResponseChunk } from '@antseed/node';
3
- import { swapAuthHeader, validateRequestModel } from './auth-swap.js';
4
-
5
- /** Hop-by-hop headers that must not be forwarded. */
6
- const HOP_BY_HOP_HEADERS = new Set([
7
- 'connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization',
8
- 'te', 'trailers', 'transfer-encoding', 'upgrade',
9
- ]);
10
-
11
- /** Internal headers used only within Antseed routing. */
12
- const INTERNAL_HEADERS = new Set([
13
- 'x-antseed-provider',
14
- ]);
15
-
16
- export interface RelayConfig {
17
- baseUrl: string;
18
- authHeaderName: string;
19
- authHeaderValue: string;
20
- tokenProvider?: TokenProvider;
21
- extraHeaders?: Record<string, string>;
22
- maxConcurrency: number;
23
- allowedModels: string[];
24
- timeoutMs?: number;
25
- }
26
-
27
- export interface RelayCallbacks {
28
- onResponse: (response: SerializedHttpResponse) => void;
29
- onResponseChunk?: (chunk: SerializedHttpResponseChunk) => void;
30
- }
31
-
32
- export class HttpRelay {
33
- private readonly _config: RelayConfig;
34
- private readonly _callbacks: RelayCallbacks;
35
- private _activeCount = 0;
36
-
37
- constructor(config: RelayConfig, callbacks: RelayCallbacks) {
38
- this._config = config;
39
- this._callbacks = callbacks;
40
- }
41
-
42
- getActiveCount(): number {
43
- return this._activeCount;
44
- }
45
-
46
- private _sendError(requestId: string, statusCode: number, error: string): void {
47
- this._callbacks.onResponse({
48
- requestId,
49
- statusCode,
50
- headers: { 'content-type': 'application/json' },
51
- body: new TextEncoder().encode(JSON.stringify({ error })),
52
- });
53
- }
54
-
55
- async handleRequest(request: SerializedHttpRequest): Promise<void> {
56
- // Validate model against allowedModels
57
- const validationError = validateRequestModel(request, this._config.allowedModels);
58
- if (validationError) {
59
- this._sendError(request.requestId, 403, validationError);
60
- return;
61
- }
62
-
63
- // Check concurrency
64
- if (this._activeCount >= this._config.maxConcurrency) {
65
- this._sendError(request.requestId, 429, 'Max concurrency reached');
66
- return;
67
- }
68
-
69
- // Increment active count
70
- this._activeCount++;
71
-
72
- try {
73
- // Resolve dynamic auth token if provider uses OAuth / keychain
74
- let effectiveConfig: { authHeaderName: string; authHeaderValue: string; extraHeaders?: Record<string, string> } = {
75
- authHeaderName: this._config.authHeaderName,
76
- authHeaderValue: this._config.authHeaderValue,
77
- extraHeaders: this._config.extraHeaders,
78
- };
79
- if (this._config.tokenProvider) {
80
- const freshToken = await this._config.tokenProvider.getToken();
81
- // Preserve Bearer prefix for OAuth providers that use Authorization header
82
- const isBearer = this._config.authHeaderName === 'authorization';
83
- const headerValue = isBearer ? `Bearer ${freshToken}` : freshToken;
84
- effectiveConfig = { ...effectiveConfig, authHeaderValue: headerValue };
85
- }
86
-
87
- // Swap auth headers
88
- const swappedRequest = swapAuthHeader(request, effectiveConfig);
89
-
90
- // Build upstream URL
91
- const base = this._config.baseUrl.replace(/\/+$/, '');
92
- const path = request.path.startsWith('/') ? request.path : `/${request.path}`;
93
- const url = `${base}${path}`;
94
-
95
- // Build fetch headers, stripping hop-by-hop
96
- const fetchHeaders: Record<string, string> = {};
97
- for (const [key, value] of Object.entries(swappedRequest.headers)) {
98
- const lower = key.toLowerCase();
99
- if (!HOP_BY_HOP_HEADERS.has(lower) && !INTERNAL_HEADERS.has(lower) && lower !== 'host' && lower !== 'content-length' && lower !== 'accept-encoding') {
100
- fetchHeaders[key] = value;
101
- }
102
- }
103
-
104
- const timeoutMs = this._config.timeoutMs ?? 120_000;
105
- const controller = new AbortController();
106
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
107
- let fetchResponse: Response;
108
- try {
109
- fetchResponse = await fetch(url, {
110
- method: swappedRequest.method,
111
- headers: fetchHeaders,
112
- body: swappedRequest.method !== 'GET' && swappedRequest.method !== 'HEAD'
113
- ? Buffer.from(swappedRequest.body)
114
- : undefined,
115
- signal: controller.signal,
116
- });
117
- } finally {
118
- clearTimeout(timeout);
119
- }
120
-
121
- const contentType = fetchResponse.headers.get('content-type') ?? '';
122
- const isSSE = contentType.includes('text/event-stream');
123
-
124
- // Build response headers, stripping hop-by-hop and encoding headers.
125
- // Node.js fetch auto-decompresses gzip/br responses, so we must strip
126
- // content-encoding to prevent the client from double-decompressing.
127
- const responseHeaders: Record<string, string> = {};
128
- fetchResponse.headers.forEach((value, key) => {
129
- const lower = key.toLowerCase();
130
- if (!HOP_BY_HOP_HEADERS.has(lower) && lower !== 'content-encoding' && lower !== 'content-length') {
131
- responseHeaders[lower] = value;
132
- }
133
- });
134
-
135
- if (isSSE && fetchResponse.body) {
136
- // Accumulate SSE body and send as a complete response so that
137
- // upstream response headers (request-id, usage metadata, etc.)
138
- // are preserved for the buyer.
139
- const reader = fetchResponse.body.getReader();
140
- const chunks: Uint8Array[] = [];
141
- try {
142
- while (true) {
143
- const { done, value } = await reader.read();
144
- if (done) break;
145
- chunks.push(value);
146
- }
147
- } catch (err) {
148
- chunks.push(
149
- new TextEncoder().encode(
150
- `event: error\ndata: ${err instanceof Error ? err.message : 'stream error'}\n\n`
151
- ),
152
- );
153
- }
154
-
155
- const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
156
- const body = new Uint8Array(totalLength);
157
- let offset = 0;
158
- for (const c of chunks) {
159
- body.set(c, offset);
160
- offset += c.length;
161
- }
162
-
163
- this._callbacks.onResponse({
164
- requestId: request.requestId,
165
- statusCode: fetchResponse.status,
166
- headers: responseHeaders,
167
- body,
168
- });
169
- } else {
170
- // Complete response
171
- const body = new Uint8Array(await fetchResponse.arrayBuffer());
172
- this._callbacks.onResponse({
173
- requestId: request.requestId,
174
- statusCode: fetchResponse.status,
175
- headers: responseHeaders,
176
- body,
177
- });
178
- }
179
- } catch (err) {
180
- const errMsg = err instanceof Error ? err.message : String(err);
181
- const sanitized = errMsg.replace(/sk-ant-[a-zA-Z0-9_-]+/g, 'sk-***');
182
- this._sendError(request.requestId, 502, `Upstream error: ${sanitized}`);
183
- } finally {
184
- this._activeCount--;
185
- }
186
- }
187
- }
package/src/index.ts DELETED
@@ -1,5 +0,0 @@
1
- export { HttpRelay, type RelayConfig, type RelayCallbacks } from './http-relay.js';
2
- export { swapAuthHeader, validateRequestModel, KNOWN_AUTH_HEADERS } from './auth-swap.js';
3
- export { StaticTokenProvider, OAuthTokenProvider, createTokenProvider, type AuthType } from './token-providers.js';
4
- export type { TokenProvider, TokenProviderState } from './token-providers.js';
5
- export { BaseProvider, type BaseProviderConfig } from './base-provider.js';
@@ -1,196 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import { StaticTokenProvider, OAuthTokenProvider, createTokenProvider } from './token-providers.js';
3
-
4
- describe('StaticTokenProvider', () => {
5
- it('returns the static token', async () => {
6
- const provider = new StaticTokenProvider('sk-test-key');
7
- const token = await provider.getToken();
8
- expect(token).toBe('sk-test-key');
9
- });
10
-
11
- it('returns the same token on multiple calls', async () => {
12
- const provider = new StaticTokenProvider('sk-test-key');
13
- expect(await provider.getToken()).toBe('sk-test-key');
14
- expect(await provider.getToken()).toBe('sk-test-key');
15
- });
16
-
17
- it('getState returns token state', () => {
18
- const provider = new StaticTokenProvider('sk-test-key');
19
- expect(provider.getState()).toEqual({ accessToken: 'sk-test-key' });
20
- });
21
-
22
- it('stop is a no-op', () => {
23
- const provider = new StaticTokenProvider('sk-test-key');
24
- expect(() => provider.stop()).not.toThrow();
25
- });
26
- });
27
-
28
- describe('OAuthTokenProvider', () => {
29
- let fetchMock: ReturnType<typeof vi.fn>;
30
- const originalFetch = globalThis.fetch;
31
-
32
- beforeEach(() => {
33
- fetchMock = vi.fn();
34
- globalThis.fetch = fetchMock;
35
- });
36
-
37
- afterEach(() => {
38
- globalThis.fetch = originalFetch;
39
- });
40
-
41
- it('returns access token when not expired', async () => {
42
- const provider = new OAuthTokenProvider({
43
- accessToken: 'access-1',
44
- refreshToken: 'refresh-1',
45
- expiresAt: Date.now() + 60 * 60 * 1000, // 1 hour from now
46
- });
47
-
48
- const token = await provider.getToken();
49
- expect(token).toBe('access-1');
50
- expect(fetchMock).not.toHaveBeenCalled();
51
- });
52
-
53
- it('refreshes token when expired', async () => {
54
- fetchMock.mockResolvedValueOnce(new Response(
55
- JSON.stringify({
56
- access_token: 'access-2',
57
- refresh_token: 'refresh-2',
58
- expires_in: 3600,
59
- }),
60
- { status: 200 },
61
- ));
62
-
63
- const provider = new OAuthTokenProvider({
64
- accessToken: 'access-1',
65
- refreshToken: 'refresh-1',
66
- expiresAt: Date.now() - 1000, // already expired
67
- });
68
-
69
- const token = await provider.getToken();
70
- expect(token).toBe('access-2');
71
- expect(fetchMock).toHaveBeenCalledTimes(1);
72
- });
73
-
74
- it('refreshes when within 5 minute buffer', async () => {
75
- fetchMock.mockResolvedValueOnce(new Response(
76
- JSON.stringify({
77
- access_token: 'access-refreshed',
78
- expires_in: 3600,
79
- }),
80
- { status: 200 },
81
- ));
82
-
83
- const provider = new OAuthTokenProvider({
84
- accessToken: 'access-old',
85
- refreshToken: 'refresh-1',
86
- expiresAt: Date.now() + 2 * 60 * 1000, // 2 minutes from now (within 5 min buffer)
87
- });
88
-
89
- const token = await provider.getToken();
90
- expect(token).toBe('access-refreshed');
91
- });
92
-
93
- it('deduplicates concurrent refresh calls', async () => {
94
- let resolveRefresh!: (value: Response) => void;
95
- const refreshPromise = new Promise<Response>((resolve) => { resolveRefresh = resolve; });
96
- fetchMock.mockReturnValueOnce(refreshPromise);
97
-
98
- const provider = new OAuthTokenProvider({
99
- accessToken: 'access-1',
100
- refreshToken: 'refresh-1',
101
- expiresAt: Date.now() - 1000,
102
- });
103
-
104
- // Start two concurrent getToken calls
105
- const p1 = provider.getToken();
106
- const p2 = provider.getToken();
107
-
108
- resolveRefresh(new Response(
109
- JSON.stringify({ access_token: 'access-new', expires_in: 3600 }),
110
- { status: 200 },
111
- ));
112
-
113
- const [t1, t2] = await Promise.all([p1, p2]);
114
- expect(t1).toBe('access-new');
115
- expect(t2).toBe('access-new');
116
- expect(fetchMock).toHaveBeenCalledTimes(1);
117
- });
118
-
119
- it('throws on refresh failure', async () => {
120
- fetchMock.mockResolvedValueOnce(new Response('Unauthorized', { status: 401 }));
121
-
122
- const provider = new OAuthTokenProvider({
123
- accessToken: 'access-1',
124
- refreshToken: 'refresh-1',
125
- expiresAt: Date.now() - 1000,
126
- });
127
-
128
- await expect(provider.getToken()).rejects.toThrow('OAuth refresh failed (401)');
129
- });
130
-
131
- it('getState returns current state', () => {
132
- const provider = new OAuthTokenProvider({
133
- accessToken: 'access-1',
134
- refreshToken: 'refresh-1',
135
- expiresAt: 1234567890,
136
- });
137
-
138
- const state = provider.getState();
139
- expect(state).toEqual({
140
- accessToken: 'access-1',
141
- refreshToken: 'refresh-1',
142
- expiresAt: 1234567890,
143
- });
144
- });
145
-
146
- it('updates refresh token when provided in response', async () => {
147
- fetchMock.mockResolvedValueOnce(new Response(
148
- JSON.stringify({
149
- access_token: 'access-2',
150
- refresh_token: 'refresh-2',
151
- expires_in: 3600,
152
- }),
153
- { status: 200 },
154
- ));
155
-
156
- const provider = new OAuthTokenProvider({
157
- accessToken: 'access-1',
158
- refreshToken: 'refresh-1',
159
- expiresAt: Date.now() - 1000,
160
- });
161
-
162
- await provider.getToken();
163
- const state = provider.getState();
164
- expect(state.refreshToken).toBe('refresh-2');
165
- });
166
- });
167
-
168
- describe('createTokenProvider', () => {
169
- it('creates StaticTokenProvider for apikey type', () => {
170
- const provider = createTokenProvider({ authType: 'apikey', authValue: 'sk-key' });
171
- expect(provider).toBeInstanceOf(StaticTokenProvider);
172
- });
173
-
174
- it('defaults to StaticTokenProvider when authType is omitted', () => {
175
- const provider = createTokenProvider({ authValue: 'sk-key' });
176
- expect(provider).toBeInstanceOf(StaticTokenProvider);
177
- });
178
-
179
- it('creates OAuthTokenProvider for oauth type with refresh token', () => {
180
- const provider = createTokenProvider({
181
- authType: 'oauth',
182
- authValue: 'access-1',
183
- refreshToken: 'refresh-1',
184
- expiresAt: Date.now() + 3600_000,
185
- });
186
- expect(provider).toBeInstanceOf(OAuthTokenProvider);
187
- });
188
-
189
- it('creates StaticTokenProvider for oauth type without refresh token', () => {
190
- const provider = createTokenProvider({
191
- authType: 'oauth',
192
- authValue: 'access-1',
193
- });
194
- expect(provider).toBeInstanceOf(StaticTokenProvider);
195
- });
196
- });