@bxb1337/windsurf-fast-context 1.0.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/LICENSE +21 -0
- package/README.md +337 -0
- package/dist/auth/api-key.d.ts +2 -0
- package/dist/auth/api-key.test.d.ts +1 -0
- package/dist/auth/jwt-manager.d.ts +18 -0
- package/dist/auth/jwt-manager.test.d.ts +1 -0
- package/dist/cjs/auth/api-key.js +10 -0
- package/dist/cjs/auth/api-key.test.js +29 -0
- package/dist/cjs/auth/jwt-manager.js +94 -0
- package/dist/cjs/auth/jwt-manager.test.js +99 -0
- package/dist/cjs/conversion/prompt-converter.js +57 -0
- package/dist/cjs/conversion/prompt-converter.test.js +95 -0
- package/dist/cjs/conversion/response-converter.js +233 -0
- package/dist/cjs/conversion/response-converter.test.js +65 -0
- package/dist/cjs/index.js +23 -0
- package/dist/cjs/model/devstral-language-model.js +399 -0
- package/dist/cjs/model/devstral-language-model.test.js +410 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/protocol/connect-frame.js +40 -0
- package/dist/cjs/protocol/connect-frame.test.js +36 -0
- package/dist/cjs/protocol/protobuf.js +114 -0
- package/dist/cjs/protocol/protobuf.test.js +58 -0
- package/dist/cjs/provider.js +13 -0
- package/dist/cjs/provider.test.js +61 -0
- package/dist/cjs/transport/http.js +83 -0
- package/dist/cjs/transport/http.test.js +196 -0
- package/dist/cjs/types/index.js +2 -0
- package/dist/conversion/prompt-converter.d.ts +49 -0
- package/dist/conversion/prompt-converter.test.d.ts +1 -0
- package/dist/conversion/response-converter.d.ts +12 -0
- package/dist/conversion/response-converter.test.d.ts +1 -0
- package/dist/esm/auth/api-key.js +7 -0
- package/dist/esm/auth/api-key.test.js +27 -0
- package/dist/esm/auth/jwt-manager.js +90 -0
- package/dist/esm/auth/jwt-manager.test.js +97 -0
- package/dist/esm/conversion/prompt-converter.js +54 -0
- package/dist/esm/conversion/prompt-converter.test.js +93 -0
- package/dist/esm/conversion/response-converter.js +230 -0
- package/dist/esm/conversion/response-converter.test.js +63 -0
- package/dist/esm/dist/cjs/index.js +3 -0
- package/dist/esm/index.js +3 -0
- package/dist/esm/model/devstral-language-model.js +395 -0
- package/dist/esm/model/devstral-language-model.test.js +408 -0
- package/dist/esm/protocol/connect-frame.js +36 -0
- package/dist/esm/protocol/connect-frame.test.js +34 -0
- package/dist/esm/protocol/protobuf.js +108 -0
- package/dist/esm/protocol/protobuf.test.js +56 -0
- package/dist/esm/provider.js +9 -0
- package/dist/esm/provider.test.js +59 -0
- package/dist/esm/scripts/postbuild.js +10 -0
- package/dist/esm/src/index.js +1 -0
- package/dist/esm/transport/http.js +78 -0
- package/dist/esm/transport/http.test.js +194 -0
- package/dist/esm/types/index.js +1 -0
- package/dist/esm/vitest.config.js +6 -0
- package/dist/index.cjs +2 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -0
- package/dist/model/devstral-language-model.d.ts +118 -0
- package/dist/model/devstral-language-model.test.d.ts +1 -0
- package/dist/protocol/connect-frame.d.ts +10 -0
- package/dist/protocol/connect-frame.test.d.ts +1 -0
- package/dist/protocol/protobuf.d.ts +11 -0
- package/dist/protocol/protobuf.test.d.ts +1 -0
- package/dist/provider.d.ts +5 -0
- package/dist/provider.test.d.ts +1 -0
- package/dist/transport/http.d.ts +22 -0
- package/dist/transport/http.test.d.ts +1 -0
- package/dist/types/index.d.ts +37 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
# @bxb1337/windsurf-fast-context
|
|
2
|
+
|
|
3
|
+
An AI SDK V3 compatible provider for Windsurf's Devstral code search API. This provider exposes tool calls for code search operations, allowing tools like OpenCode to execute them.
|
|
4
|
+
|
|
5
|
+
**Important**: This provider exposes tool calls but does **not** execute them. Tool execution is delegated to the caller (e.g., OpenCode, your application).
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @bxb1337/windsurf-fast-context
|
|
11
|
+
# or
|
|
12
|
+
pnpm add @bxb1337/windsurf-fast-context
|
|
13
|
+
# or
|
|
14
|
+
yarn add @bxb1337/windsurf-fast-context
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Peer Dependencies
|
|
18
|
+
|
|
19
|
+
This package requires `ai` as a peer dependency:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install ai
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { createWindsurfProvider } from '@bxb1337/windsurf-fast-context';
|
|
29
|
+
import { generateText } from 'ai';
|
|
30
|
+
|
|
31
|
+
// Create the provider with your API key
|
|
32
|
+
const windsurf = createWindsurfProvider({
|
|
33
|
+
apiKey: process.env.WINDSURF_API_KEY,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Use with AI SDK
|
|
37
|
+
const result = await generateText({
|
|
38
|
+
model: windsurf('MODEL_SWE_1_6_FAST'),
|
|
39
|
+
prompt: 'Find authentication logic in the codebase',
|
|
40
|
+
tools: {
|
|
41
|
+
ripgrep: {
|
|
42
|
+
description: 'Search for patterns in files',
|
|
43
|
+
parameters: {
|
|
44
|
+
type: 'object',
|
|
45
|
+
properties: {
|
|
46
|
+
pattern: { type: 'string' },
|
|
47
|
+
path: { type: 'string' },
|
|
48
|
+
},
|
|
49
|
+
required: ['pattern'],
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
readfile: {
|
|
53
|
+
description: 'Read file contents',
|
|
54
|
+
parameters: {
|
|
55
|
+
type: 'object',
|
|
56
|
+
properties: {
|
|
57
|
+
path: { type: 'string' },
|
|
58
|
+
},
|
|
59
|
+
required: ['path'],
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Tool calls are exposed for you to execute
|
|
66
|
+
for (const part of result.content) {
|
|
67
|
+
if (part.type === 'tool-call') {
|
|
68
|
+
console.log(`Tool: ${part.toolName}`);
|
|
69
|
+
console.log(`Args: ${JSON.stringify(part.args)}`);
|
|
70
|
+
// Execute the tool yourself...
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## API Reference
|
|
76
|
+
|
|
77
|
+
### `createWindsurfProvider(options?)`
|
|
78
|
+
|
|
79
|
+
Creates a Windsurf provider factory function.
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
import { createWindsurfProvider } from '@bxb1337/windsurf-fast-context';
|
|
83
|
+
|
|
84
|
+
const windsurf = createWindsurfProvider({
|
|
85
|
+
apiKey: 'your-api-key',
|
|
86
|
+
baseURL: 'https://custom-endpoint.com',
|
|
87
|
+
headers: { 'X-Custom': 'value' },
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
#### Options
|
|
92
|
+
|
|
93
|
+
| Option | Type | Description |
|
|
94
|
+
|--------|------|-------------|
|
|
95
|
+
| `apiKey` | `string` | Windsurf API key. Falls back to `WINDSURF_API_KEY` environment variable. Required. |
|
|
96
|
+
| `baseURL` | `string` | Custom API endpoint. Default: `https://server.self-serve.windsurf.com` |
|
|
97
|
+
| `headers` | `Record<string, string>` | Custom headers to send with each request. |
|
|
98
|
+
| `fetch` | `FetchFn` | Custom fetch function for testing or proxying. |
|
|
99
|
+
| `generateId` | `() => string` | Custom ID generator for tool calls. |
|
|
100
|
+
|
|
101
|
+
#### Returns
|
|
102
|
+
|
|
103
|
+
A function that accepts a model ID and returns a `LanguageModelV3` instance:
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
const model = windsurf('MODEL_SWE_1_6_FAST');
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### `windsurf` (named export)
|
|
110
|
+
|
|
111
|
+
A pre-configured provider instance that reads the API key from `WINDSURF_API_KEY`:
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
import { windsurf } from '@bxb1337/windsurf-fast-context';
|
|
115
|
+
|
|
116
|
+
const model = windsurf('MODEL_SWE_1_6_FAST');
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Default Export
|
|
120
|
+
|
|
121
|
+
The default export is the `windsurf` provider:
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
import windsurf from '@bxb1337/windsurf-fast-context';
|
|
125
|
+
|
|
126
|
+
const model = windsurf('MODEL_SWE_1_6_FAST');
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Supported Models
|
|
130
|
+
|
|
131
|
+
| Model ID | Description |
|
|
132
|
+
|----------|-------------|
|
|
133
|
+
| `MODEL_SWE_1_6_FAST` | Fast variant for quick responses |
|
|
134
|
+
| `MODEL_SWE_1_6` | Standard variant with more capability |
|
|
135
|
+
|
|
136
|
+
Custom model IDs are also accepted as strings.
|
|
137
|
+
|
|
138
|
+
## Configuration
|
|
139
|
+
|
|
140
|
+
### Environment Variable
|
|
141
|
+
|
|
142
|
+
Set your API key via environment variable:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
export WINDSURF_API_KEY="your-api-key"
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Then use the default export:
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
import windsurf from '@bxb1337/windsurf-fast-context';
|
|
152
|
+
|
|
153
|
+
// Reads WINDSURF_API_KEY automatically
|
|
154
|
+
const model = windsurf('MODEL_SWE_1_6_FAST');
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### OpenCode Integration
|
|
158
|
+
|
|
159
|
+
Add to your `opencode.json`:
|
|
160
|
+
|
|
161
|
+
```json
|
|
162
|
+
{
|
|
163
|
+
"$schema": "https://opencode.ai/config.json",
|
|
164
|
+
"provider": {
|
|
165
|
+
"windsurf": {
|
|
166
|
+
"npm": "@bxb1337/windsurf-fast-context",
|
|
167
|
+
"name": "Windsurf Devstral",
|
|
168
|
+
"options": {
|
|
169
|
+
"apiKey": "your-api-key",
|
|
170
|
+
"baseURL": "https://server.self-serve.windsurf.com"
|
|
171
|
+
},
|
|
172
|
+
"models": {
|
|
173
|
+
"MODEL_SWE_1_6_FAST": {
|
|
174
|
+
"name": "Devstral Fast",
|
|
175
|
+
"limit": {
|
|
176
|
+
"context": 128000,
|
|
177
|
+
"output": 8192
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Custom Fetch for Testing
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
import { createWindsurfProvider } from '@bxb1337/windsurf-fast-context';
|
|
190
|
+
|
|
191
|
+
const mockFetch = async (url: string | URL | Request, init?: RequestInit) => {
|
|
192
|
+
// Return mock responses for testing
|
|
193
|
+
return new Response(JSON.stringify({ result: 'mocked' }), {
|
|
194
|
+
headers: { 'Content-Type': 'application/json' },
|
|
195
|
+
});
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const windsurf = createWindsurfProvider({
|
|
199
|
+
apiKey: 'test-key',
|
|
200
|
+
fetch: mockFetch,
|
|
201
|
+
});
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Examples
|
|
205
|
+
|
|
206
|
+
### Basic Code Search
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
import windsurf from '@bxb1337/windsurf-fast-context';
|
|
210
|
+
import { generateText } from 'ai';
|
|
211
|
+
|
|
212
|
+
const result = await generateText({
|
|
213
|
+
model: windsurf('MODEL_SWE_1_6_FAST'),
|
|
214
|
+
prompt: 'Search for TODO comments in the codebase',
|
|
215
|
+
tools: {
|
|
216
|
+
ripgrep: {
|
|
217
|
+
description: 'Search files using regex patterns',
|
|
218
|
+
parameters: {
|
|
219
|
+
type: 'object',
|
|
220
|
+
properties: {
|
|
221
|
+
pattern: { type: 'string', description: 'Regex pattern to search' },
|
|
222
|
+
path: { type: 'string', description: 'Directory to search in' },
|
|
223
|
+
},
|
|
224
|
+
required: ['pattern'],
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
console.log(result.text);
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Streaming Responses
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
import windsurf from '@bxb1337/windsurf-fast-context';
|
|
237
|
+
import { streamText } from 'ai';
|
|
238
|
+
|
|
239
|
+
const stream = await streamText({
|
|
240
|
+
model: windsurf('MODEL_SWE_1_6_FAST'),
|
|
241
|
+
prompt: 'Analyze the project structure',
|
|
242
|
+
tools: {
|
|
243
|
+
tree: {
|
|
244
|
+
description: 'List directory tree',
|
|
245
|
+
parameters: {
|
|
246
|
+
type: 'object',
|
|
247
|
+
properties: {
|
|
248
|
+
path: { type: 'string' },
|
|
249
|
+
depth: { type: 'number' },
|
|
250
|
+
},
|
|
251
|
+
required: ['path'],
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
for await (const chunk of stream.textStream) {
|
|
258
|
+
process.stdout.write(chunk);
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Multi-turn Conversation
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
import windsurf from '@bxb1337/windsurf-fast-context';
|
|
266
|
+
import { generateText } from 'ai';
|
|
267
|
+
|
|
268
|
+
const result = await generateText({
|
|
269
|
+
model: windsurf('MODEL_SWE_1_6_FAST'),
|
|
270
|
+
messages: [
|
|
271
|
+
{ role: 'system', content: 'You are a code search assistant.' },
|
|
272
|
+
{ role: 'user', content: 'Find all API routes' },
|
|
273
|
+
{ role: 'assistant', content: 'I found routes in src/routes/' },
|
|
274
|
+
{ role: 'user', content: 'Show me the auth routes' },
|
|
275
|
+
],
|
|
276
|
+
tools: {
|
|
277
|
+
glob: {
|
|
278
|
+
description: 'Find files matching a pattern',
|
|
279
|
+
parameters: {
|
|
280
|
+
type: 'object',
|
|
281
|
+
properties: {
|
|
282
|
+
pattern: { type: 'string' },
|
|
283
|
+
},
|
|
284
|
+
required: ['pattern'],
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
## Troubleshooting
|
|
292
|
+
|
|
293
|
+
### "WINDSURF_API_KEY is required"
|
|
294
|
+
|
|
295
|
+
The API key was not provided. Either:
|
|
296
|
+
1. Pass `apiKey` to `createWindsurfProvider()`
|
|
297
|
+
2. Set the `WINDSURF_API_KEY` environment variable
|
|
298
|
+
|
|
299
|
+
### Authentication Errors
|
|
300
|
+
|
|
301
|
+
If you see authentication failures:
|
|
302
|
+
1. Verify your API key is valid and not expired
|
|
303
|
+
2. Check that the key starts with the expected prefix
|
|
304
|
+
3. Ensure no extra whitespace in the environment variable
|
|
305
|
+
|
|
306
|
+
### Tool Calls Not Executed
|
|
307
|
+
|
|
308
|
+
This is expected behavior. This provider exposes tool calls for you to execute. The `restricted_exec` and `answer` tools are returned as `tool-call` content parts. Your application is responsible for executing them.
|
|
309
|
+
|
|
310
|
+
### Integration Tests Skipped
|
|
311
|
+
|
|
312
|
+
Integration tests are gated by `WINDSURF_API_KEY`. To run them:
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
export WINDSURF_API_KEY="your-api-key"
|
|
316
|
+
pnpm test test/integration/
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### Custom Endpoint Issues
|
|
320
|
+
|
|
321
|
+
When using `baseURL`, ensure:
|
|
322
|
+
1. The URL includes the protocol (`https://`)
|
|
323
|
+
2. No trailing slash
|
|
324
|
+
3. The endpoint is accessible from your network
|
|
325
|
+
|
|
326
|
+
## What This Package Does NOT Do
|
|
327
|
+
|
|
328
|
+
Per the design scope:
|
|
329
|
+
|
|
330
|
+
- **No built-in tool execution**: Tools like `rg`, `readfile`, `tree`, `ls`, `glob` are exposed as tool calls, not executed
|
|
331
|
+
- **No MCP server**: This is an AI SDK provider, not an MCP server implementation
|
|
332
|
+
- **No local key extraction**: API keys must be provided explicitly via constructor or environment variable
|
|
333
|
+
- **No Chat/Completions API compatibility**: This is an AI SDK V3 provider, not an OpenAI-compatible API
|
|
334
|
+
|
|
335
|
+
## License
|
|
336
|
+
|
|
337
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
2
|
+
export declare const AUTH_BASE = "https://server.self-serve.windsurf.com/exa.auth_pb.AuthService";
|
|
3
|
+
export interface JwtManagerOptions {
|
|
4
|
+
fetch?: FetchLike;
|
|
5
|
+
authBase?: string;
|
|
6
|
+
now?: () => number;
|
|
7
|
+
}
|
|
8
|
+
export declare class JwtManager {
|
|
9
|
+
private readonly fetchFn;
|
|
10
|
+
private readonly authBase;
|
|
11
|
+
private readonly now;
|
|
12
|
+
private readonly cache;
|
|
13
|
+
private readonly inFlight;
|
|
14
|
+
constructor(options?: JwtManagerOptions);
|
|
15
|
+
getJwt(apiKey: string): Promise<string>;
|
|
16
|
+
private fetchJwt;
|
|
17
|
+
}
|
|
18
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveApiKey = resolveApiKey;
|
|
4
|
+
function resolveApiKey(options) {
|
|
5
|
+
if (options?.apiKey)
|
|
6
|
+
return options.apiKey;
|
|
7
|
+
if (process.env.WINDSURF_API_KEY)
|
|
8
|
+
return process.env.WINDSURF_API_KEY;
|
|
9
|
+
throw new Error('WINDSURF_API_KEY is required');
|
|
10
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const api_key_1 = require("./api-key");
|
|
5
|
+
(0, vitest_1.describe)('api key resolver', () => {
|
|
6
|
+
const ORIGINAL = process.env.WINDSURF_API_KEY;
|
|
7
|
+
(0, vitest_1.afterEach)(() => {
|
|
8
|
+
// restore original env to avoid leakage
|
|
9
|
+
if (ORIGINAL === undefined) {
|
|
10
|
+
delete process.env.WINDSURF_API_KEY;
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
process.env.WINDSURF_API_KEY = ORIGINAL;
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
(0, vitest_1.it)('constructor', () => {
|
|
17
|
+
const key = (0, api_key_1.resolveApiKey)({ apiKey: 'ctor-key' });
|
|
18
|
+
(0, vitest_1.expect)(key).toBe('ctor-key');
|
|
19
|
+
});
|
|
20
|
+
(0, vitest_1.it)('env', () => {
|
|
21
|
+
process.env.WINDSURF_API_KEY = 'test-key';
|
|
22
|
+
const key = (0, api_key_1.resolveApiKey)();
|
|
23
|
+
(0, vitest_1.expect)(key).toBe('test-key');
|
|
24
|
+
});
|
|
25
|
+
(0, vitest_1.it)('missing', () => {
|
|
26
|
+
delete process.env.WINDSURF_API_KEY;
|
|
27
|
+
(0, vitest_1.expect)(() => (0, api_key_1.resolveApiKey)()).toThrowError(new Error('WINDSURF_API_KEY is required'));
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.JwtManager = exports.AUTH_BASE = void 0;
|
|
4
|
+
const protobuf_js_1 = require("../protocol/protobuf.js");
|
|
5
|
+
exports.AUTH_BASE = 'https://server.self-serve.windsurf.com/exa.auth_pb.AuthService';
|
|
6
|
+
const WS_APP = 'windsurf';
|
|
7
|
+
const WS_APP_VER = process.env.WS_APP_VER ?? '1.48.2';
|
|
8
|
+
const WS_LS_VER = process.env.WS_LS_VER ?? '1.9544.35';
|
|
9
|
+
const JWT_PATTERN = /eyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*/;
|
|
10
|
+
class JwtManager {
|
|
11
|
+
fetchFn;
|
|
12
|
+
authBase;
|
|
13
|
+
now;
|
|
14
|
+
cache = new Map();
|
|
15
|
+
inFlight = new Map();
|
|
16
|
+
constructor(options = {}) {
|
|
17
|
+
this.fetchFn = options.fetch ?? fetch;
|
|
18
|
+
this.authBase = options.authBase ?? exports.AUTH_BASE;
|
|
19
|
+
this.now = options.now ?? Date.now;
|
|
20
|
+
}
|
|
21
|
+
async getJwt(apiKey) {
|
|
22
|
+
if (!apiKey) {
|
|
23
|
+
throw new Error('API key is required');
|
|
24
|
+
}
|
|
25
|
+
const nowSeconds = Math.floor(this.now() / 1000);
|
|
26
|
+
const cached = this.cache.get(apiKey);
|
|
27
|
+
if (cached && cached.expiresAt > nowSeconds + 60) {
|
|
28
|
+
return cached.token;
|
|
29
|
+
}
|
|
30
|
+
const inFlight = this.inFlight.get(apiKey);
|
|
31
|
+
if (inFlight) {
|
|
32
|
+
return inFlight;
|
|
33
|
+
}
|
|
34
|
+
const pending = this.fetchJwt(apiKey)
|
|
35
|
+
.then((token) => {
|
|
36
|
+
const expiresAt = getJwtExp(token) || Math.floor(this.now() / 1000) + 3600;
|
|
37
|
+
this.cache.set(apiKey, { token, expiresAt });
|
|
38
|
+
return token;
|
|
39
|
+
})
|
|
40
|
+
.finally(() => {
|
|
41
|
+
this.inFlight.delete(apiKey);
|
|
42
|
+
});
|
|
43
|
+
this.inFlight.set(apiKey, pending);
|
|
44
|
+
return pending;
|
|
45
|
+
}
|
|
46
|
+
async fetchJwt(apiKey) {
|
|
47
|
+
const metadata = new protobuf_js_1.ProtobufEncoder();
|
|
48
|
+
metadata.writeString(1, WS_APP);
|
|
49
|
+
metadata.writeString(2, WS_APP_VER);
|
|
50
|
+
metadata.writeString(3, apiKey);
|
|
51
|
+
metadata.writeString(4, 'zh-cn');
|
|
52
|
+
metadata.writeString(7, WS_LS_VER);
|
|
53
|
+
metadata.writeString(12, WS_APP);
|
|
54
|
+
metadata.writeBytes(30, Buffer.from([0x00, 0x01]));
|
|
55
|
+
const requestBody = new protobuf_js_1.ProtobufEncoder();
|
|
56
|
+
requestBody.writeMessage(1, metadata);
|
|
57
|
+
const response = await this.fetchFn(`${this.authBase}/GetUserJwt`, {
|
|
58
|
+
method: 'POST',
|
|
59
|
+
headers: {
|
|
60
|
+
'Content-Type': 'application/proto',
|
|
61
|
+
'Connect-Protocol-Version': '1',
|
|
62
|
+
'User-Agent': 'connect-go/1.18.1 (go1.25.5)',
|
|
63
|
+
},
|
|
64
|
+
body: requestBody.toBuffer(),
|
|
65
|
+
});
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
throw new Error(`HTTP ${response.status}`);
|
|
68
|
+
}
|
|
69
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
70
|
+
const token = extractJwt(bytes);
|
|
71
|
+
if (!token) {
|
|
72
|
+
throw new Error('Failed to extract JWT from GetUserJwt response');
|
|
73
|
+
}
|
|
74
|
+
return token;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
exports.JwtManager = JwtManager;
|
|
78
|
+
function extractJwt(value) {
|
|
79
|
+
const match = value.toString('utf8').match(JWT_PATTERN);
|
|
80
|
+
return match?.[0] ?? null;
|
|
81
|
+
}
|
|
82
|
+
function getJwtExp(jwt) {
|
|
83
|
+
try {
|
|
84
|
+
const payloadPart = jwt.split('.')[1];
|
|
85
|
+
if (!payloadPart) {
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
const payload = JSON.parse(Buffer.from(payloadPart, 'base64url').toString('utf8'));
|
|
89
|
+
return typeof payload.exp === 'number' ? payload.exp : 0;
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return 0;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const protobuf_js_1 = require("../protocol/protobuf.js");
|
|
5
|
+
const jwt_manager_js_1 = require("./jwt-manager.js");
|
|
6
|
+
function makeJwt(exp, tag) {
|
|
7
|
+
const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url');
|
|
8
|
+
const payload = Buffer.from(JSON.stringify({ exp, tag })).toString('base64url');
|
|
9
|
+
return `${header}.${payload}.signature`;
|
|
10
|
+
}
|
|
11
|
+
function makeJwtResponse(token) {
|
|
12
|
+
const encoder = new protobuf_js_1.ProtobufEncoder();
|
|
13
|
+
encoder.writeString(1, `prefix:${token}:suffix`);
|
|
14
|
+
return new Response(Uint8Array.from(encoder.toBuffer()), { status: 200 });
|
|
15
|
+
}
|
|
16
|
+
function bufferFromBody(body) {
|
|
17
|
+
if (body == null) {
|
|
18
|
+
return Buffer.alloc(0);
|
|
19
|
+
}
|
|
20
|
+
if (typeof body === 'string') {
|
|
21
|
+
return Buffer.from(body, 'utf8');
|
|
22
|
+
}
|
|
23
|
+
if (body instanceof ArrayBuffer) {
|
|
24
|
+
return Buffer.from(body);
|
|
25
|
+
}
|
|
26
|
+
if (ArrayBuffer.isView(body)) {
|
|
27
|
+
return Buffer.from(body.buffer, body.byteOffset, body.byteLength);
|
|
28
|
+
}
|
|
29
|
+
throw new Error(`Unsupported request body type: ${typeof body}`);
|
|
30
|
+
}
|
|
31
|
+
(0, vitest_1.describe)('jwt manager fetch', () => {
|
|
32
|
+
(0, vitest_1.it)('fetch exchanges api key to jwt and caches result', async () => {
|
|
33
|
+
const exp = 4_000_000_000;
|
|
34
|
+
const token = makeJwt(exp, 'first');
|
|
35
|
+
const calls = [];
|
|
36
|
+
const fakeFetch = async (input, init) => {
|
|
37
|
+
calls.push({ input, init });
|
|
38
|
+
return makeJwtResponse(token);
|
|
39
|
+
};
|
|
40
|
+
const manager = new jwt_manager_js_1.JwtManager({ fetch: fakeFetch, now: () => (exp - 3_600) * 1000 });
|
|
41
|
+
const jwt1 = await manager.getJwt('test-api-key');
|
|
42
|
+
const jwt2 = await manager.getJwt('test-api-key');
|
|
43
|
+
(0, vitest_1.expect)(jwt1).toBe(token);
|
|
44
|
+
(0, vitest_1.expect)(jwt2).toBe(token);
|
|
45
|
+
(0, vitest_1.expect)(calls).toHaveLength(1);
|
|
46
|
+
(0, vitest_1.expect)(calls[0]?.input).toBe(`${jwt_manager_js_1.AUTH_BASE}/GetUserJwt`);
|
|
47
|
+
(0, vitest_1.expect)(calls[0]?.init?.method).toBe('POST');
|
|
48
|
+
(0, vitest_1.expect)(new Headers(calls[0]?.init?.headers).get('content-type')).toBe('application/proto');
|
|
49
|
+
const requestBody = bufferFromBody(calls[0]?.init?.body);
|
|
50
|
+
(0, vitest_1.expect)(requestBody.length).toBeGreaterThan(0);
|
|
51
|
+
(0, vitest_1.expect)(requestBody.toString('utf8')).toContain('test-api-key');
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
(0, vitest_1.describe)('jwt manager expiry', () => {
|
|
55
|
+
(0, vitest_1.it)('expiry refreshes token when less than sixty seconds remain', async () => {
|
|
56
|
+
let nowMs = 0;
|
|
57
|
+
const token1 = makeJwt(10_000, 'first');
|
|
58
|
+
const token2 = makeJwt(20_000, 'second');
|
|
59
|
+
const queue = [makeJwtResponse(token1), makeJwtResponse(token2)];
|
|
60
|
+
let callCount = 0;
|
|
61
|
+
const fakeFetch = async () => {
|
|
62
|
+
callCount += 1;
|
|
63
|
+
const next = queue.shift();
|
|
64
|
+
if (!next) {
|
|
65
|
+
throw new Error('No fake fetch response queued');
|
|
66
|
+
}
|
|
67
|
+
return next;
|
|
68
|
+
};
|
|
69
|
+
const manager = new jwt_manager_js_1.JwtManager({ fetch: fakeFetch, now: () => nowMs });
|
|
70
|
+
const first = await manager.getJwt('exp-api-key');
|
|
71
|
+
nowMs = (10_000 - 59) * 1000;
|
|
72
|
+
const second = await manager.getJwt('exp-api-key');
|
|
73
|
+
(0, vitest_1.expect)(first).toBe(token1);
|
|
74
|
+
(0, vitest_1.expect)(second).toBe(token2);
|
|
75
|
+
(0, vitest_1.expect)(callCount).toBe(2);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
(0, vitest_1.describe)('jwt manager concurrent', () => {
|
|
79
|
+
(0, vitest_1.it)('concurrent getJwt calls share one in-flight fetch', async () => {
|
|
80
|
+
const token = makeJwt(50_000, 'concurrent');
|
|
81
|
+
let fetchCalls = 0;
|
|
82
|
+
let resolveFetch;
|
|
83
|
+
const pending = new Promise((resolve) => {
|
|
84
|
+
resolveFetch = resolve;
|
|
85
|
+
});
|
|
86
|
+
const fakeFetch = async () => {
|
|
87
|
+
fetchCalls += 1;
|
|
88
|
+
return pending;
|
|
89
|
+
};
|
|
90
|
+
const manager = new jwt_manager_js_1.JwtManager({ fetch: fakeFetch, now: () => 0 });
|
|
91
|
+
const p1 = manager.getJwt('same-key');
|
|
92
|
+
const p2 = manager.getJwt('same-key');
|
|
93
|
+
resolveFetch(makeJwtResponse(token));
|
|
94
|
+
const [jwt1, jwt2] = await Promise.all([p1, p2]);
|
|
95
|
+
(0, vitest_1.expect)(fetchCalls).toBe(1);
|
|
96
|
+
(0, vitest_1.expect)(jwt1).toBe(token);
|
|
97
|
+
(0, vitest_1.expect)(jwt2).toBe(token);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.convertPrompt = convertPrompt;
|
|
4
|
+
function toContentString(value) {
|
|
5
|
+
if (typeof value === 'string') {
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
return JSON.stringify(value);
|
|
9
|
+
}
|
|
10
|
+
function convertPrompt(prompt) {
|
|
11
|
+
const messages = [];
|
|
12
|
+
for (const message of prompt) {
|
|
13
|
+
if (message.role === 'system') {
|
|
14
|
+
messages.push({ role: 5, content: message.content });
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (message.role === 'user') {
|
|
18
|
+
const text = message.content
|
|
19
|
+
.filter((part) => part.type === 'text')
|
|
20
|
+
.map((part) => part.text)
|
|
21
|
+
.join('');
|
|
22
|
+
messages.push({ role: 1, content: text });
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (message.role === 'assistant') {
|
|
26
|
+
for (const part of message.content) {
|
|
27
|
+
if (part.type === 'text') {
|
|
28
|
+
messages.push({ role: 2, content: part.text });
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (part.type === 'tool-call') {
|
|
32
|
+
messages.push({
|
|
33
|
+
role: 2,
|
|
34
|
+
content: '',
|
|
35
|
+
metadata: {
|
|
36
|
+
toolCallId: part.toolCallId,
|
|
37
|
+
toolName: part.toolName,
|
|
38
|
+
toolArgsJson: JSON.stringify(part.args),
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
// Tool result messages - use refCallId to reference the original tool call
|
|
46
|
+
for (const part of message.content) {
|
|
47
|
+
messages.push({
|
|
48
|
+
role: 4,
|
|
49
|
+
content: toContentString(part.result),
|
|
50
|
+
metadata: {
|
|
51
|
+
refCallId: part.toolCallId, // This links back to the tool call
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return messages;
|
|
57
|
+
}
|