@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
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const provider_js_1 = require("./provider.js");
|
|
5
|
+
const devstral_language_model_js_1 = require("./model/devstral-language-model.js");
|
|
6
|
+
// Helper to narrow the returned model in tests without using `any`.
|
|
7
|
+
function providerShape(m) {
|
|
8
|
+
return m;
|
|
9
|
+
}
|
|
10
|
+
(0, vitest_1.describe)('provider factory', () => {
|
|
11
|
+
(0, vitest_1.it)('with-api-key: constructs model when apiKey passed explicitly', () => {
|
|
12
|
+
const factory = (0, provider_js_1.createWindsurfProvider)({ apiKey: 'explicit-key' });
|
|
13
|
+
const model = factory('MODEL_SWE_1_6_FAST');
|
|
14
|
+
(0, vitest_1.expect)(model).toBeInstanceOf(devstral_language_model_js_1.DevstralLanguageModel);
|
|
15
|
+
(0, vitest_1.expect)(providerShape(model).apiKey).toBe('explicit-key');
|
|
16
|
+
});
|
|
17
|
+
(0, vitest_1.it)('env-var: uses WINDSURF_API_KEY when apiKey not provided', () => {
|
|
18
|
+
const old = process.env.WINDSURF_API_KEY;
|
|
19
|
+
try {
|
|
20
|
+
process.env.WINDSURF_API_KEY = 'env-key';
|
|
21
|
+
const factory = (0, provider_js_1.createWindsurfProvider)();
|
|
22
|
+
const model = factory('MODEL_SWE_1_6_FAST');
|
|
23
|
+
(0, vitest_1.expect)(providerShape(model).apiKey).toBe('env-key');
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
if (old === undefined)
|
|
27
|
+
delete process.env.WINDSURF_API_KEY;
|
|
28
|
+
else
|
|
29
|
+
process.env.WINDSURF_API_KEY = old;
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
(0, vitest_1.it)('no-key: throws when no api key available', () => {
|
|
33
|
+
const old = process.env.WINDSURF_API_KEY;
|
|
34
|
+
try {
|
|
35
|
+
delete process.env.WINDSURF_API_KEY;
|
|
36
|
+
const factory = (0, provider_js_1.createWindsurfProvider)();
|
|
37
|
+
(0, vitest_1.expect)(() => factory('MODEL_SWE_1_6_FAST')).toThrow('WINDSURF_API_KEY is required');
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
if (old === undefined)
|
|
41
|
+
delete process.env.WINDSURF_API_KEY;
|
|
42
|
+
else
|
|
43
|
+
process.env.WINDSURF_API_KEY = old;
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
(0, vitest_1.it)('custom-baseurl: passes baseURL and trims trailing slash', () => {
|
|
47
|
+
const factory = (0, provider_js_1.createWindsurfProvider)({ apiKey: 'k', baseURL: 'https://example.com/' });
|
|
48
|
+
const model = factory('MODEL_SWE_1_6_FAST');
|
|
49
|
+
(0, vitest_1.expect)(providerShape(model).baseURL).toBe('https://example.com');
|
|
50
|
+
});
|
|
51
|
+
(0, vitest_1.it)('custom-headers: passes headers through to model', () => {
|
|
52
|
+
const headers = { 'x-foo': 'bar' };
|
|
53
|
+
const factory = (0, provider_js_1.createWindsurfProvider)({ apiKey: 'k', headers });
|
|
54
|
+
const model = factory('MODEL_SWE_1_6_FAST');
|
|
55
|
+
(0, vitest_1.expect)(providerShape(model).headers['x-foo']).toBe('bar');
|
|
56
|
+
});
|
|
57
|
+
(0, vitest_1.it)('exports: named factory and default windsurf', () => {
|
|
58
|
+
(0, vitest_1.expect)(typeof provider_js_1.createWindsurfProvider).toBe('function');
|
|
59
|
+
(0, vitest_1.expect)(provider_js_1.windsurf).toBeDefined();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DevstralTransport = exports.DevstralTransportError = void 0;
|
|
4
|
+
class DevstralTransportError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
status;
|
|
7
|
+
constructor(code, message, status, options) {
|
|
8
|
+
super(message, options);
|
|
9
|
+
this.name = 'DevstralTransportError';
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.status = status;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
exports.DevstralTransportError = DevstralTransportError;
|
|
15
|
+
class DevstralTransport {
|
|
16
|
+
fetchFn;
|
|
17
|
+
maxAttempts;
|
|
18
|
+
constructor(options = {}) {
|
|
19
|
+
this.fetchFn = options.fetch ?? fetch;
|
|
20
|
+
this.maxAttempts = Math.max(1, options.maxAttempts ?? 3);
|
|
21
|
+
}
|
|
22
|
+
postUnary(url, body, headers) {
|
|
23
|
+
return this.postUnaryRequest(url, body, headers);
|
|
24
|
+
}
|
|
25
|
+
postStreaming(url, body, headers, signal) {
|
|
26
|
+
return this.postStreamingRequest(url, body, headers, signal);
|
|
27
|
+
}
|
|
28
|
+
async postUnaryRequest(url, body, headers) {
|
|
29
|
+
const response = await this.post(url, body, headers);
|
|
30
|
+
return Buffer.from(await response.arrayBuffer());
|
|
31
|
+
}
|
|
32
|
+
async postStreamingRequest(url, body, headers, signal) {
|
|
33
|
+
const response = await this.post(url, body, headers, signal);
|
|
34
|
+
if (response.body == null) {
|
|
35
|
+
throw new DevstralTransportError('NETWORK_ERROR', 'Streaming response body is unavailable', response.status);
|
|
36
|
+
}
|
|
37
|
+
return response.body;
|
|
38
|
+
}
|
|
39
|
+
async post(url, body, headers, signal) {
|
|
40
|
+
for (let attempt = 1; attempt <= this.maxAttempts; attempt += 1) {
|
|
41
|
+
try {
|
|
42
|
+
const response = await this.fetchFn(url, {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
headers,
|
|
45
|
+
body,
|
|
46
|
+
signal,
|
|
47
|
+
});
|
|
48
|
+
if (response.ok) {
|
|
49
|
+
return response;
|
|
50
|
+
}
|
|
51
|
+
if (response.status === 403) {
|
|
52
|
+
throw new DevstralTransportError('AUTH_ERROR', 'HTTP 403', response.status);
|
|
53
|
+
}
|
|
54
|
+
if (response.status === 429) {
|
|
55
|
+
throw new DevstralTransportError('RATE_LIMITED', 'HTTP 429', response.status);
|
|
56
|
+
}
|
|
57
|
+
if (response.status >= 500 && response.status < 600) {
|
|
58
|
+
if (attempt < this.maxAttempts) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
throw new DevstralTransportError('NETWORK_ERROR', `HTTP ${response.status}`, response.status);
|
|
62
|
+
}
|
|
63
|
+
throw new DevstralTransportError('NETWORK_ERROR', `HTTP ${response.status}`, response.status);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (isAbortError(error)) {
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
if (error instanceof DevstralTransportError) {
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
throw new DevstralTransportError('NETWORK_ERROR', 'Network request failed', undefined, {
|
|
73
|
+
cause: error,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
throw new DevstralTransportError('NETWORK_ERROR', 'Network request failed');
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
exports.DevstralTransport = DevstralTransport;
|
|
81
|
+
function isAbortError(error) {
|
|
82
|
+
return error instanceof Error && error.name === 'AbortError';
|
|
83
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const http_js_1 = require("./http.js");
|
|
5
|
+
const requestBytes = Buffer.from([1, 2, 3, 4]);
|
|
6
|
+
const responseBytes = Buffer.from([9, 8, 7, 6]);
|
|
7
|
+
function bufferFromBody(body) {
|
|
8
|
+
if (body == null) {
|
|
9
|
+
return Buffer.alloc(0);
|
|
10
|
+
}
|
|
11
|
+
if (typeof body === 'string') {
|
|
12
|
+
return Buffer.from(body, 'utf8');
|
|
13
|
+
}
|
|
14
|
+
if (body instanceof ArrayBuffer) {
|
|
15
|
+
return Buffer.from(body);
|
|
16
|
+
}
|
|
17
|
+
if (ArrayBuffer.isView(body)) {
|
|
18
|
+
return Buffer.from(body.buffer, body.byteOffset, body.byteLength);
|
|
19
|
+
}
|
|
20
|
+
throw new Error(`Unsupported request body type: ${typeof body}`);
|
|
21
|
+
}
|
|
22
|
+
function makeResponse(status, body) {
|
|
23
|
+
return new Response(Uint8Array.from(body), { status });
|
|
24
|
+
}
|
|
25
|
+
function makeChunkStream(chunks) {
|
|
26
|
+
let index = 0;
|
|
27
|
+
return new ReadableStream({
|
|
28
|
+
pull(controller) {
|
|
29
|
+
const chunk = chunks[index];
|
|
30
|
+
if (!chunk) {
|
|
31
|
+
controller.close();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
controller.enqueue(chunk);
|
|
35
|
+
index += 1;
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
async function readStreamBytes(stream) {
|
|
40
|
+
const reader = stream.getReader();
|
|
41
|
+
const chunks = [];
|
|
42
|
+
while (true) {
|
|
43
|
+
const next = await reader.read();
|
|
44
|
+
if (next.done) {
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
chunks.push(Buffer.from(next.value));
|
|
48
|
+
}
|
|
49
|
+
return Buffer.concat(chunks);
|
|
50
|
+
}
|
|
51
|
+
function makeFakeFetch(sequence) {
|
|
52
|
+
const calls = [];
|
|
53
|
+
const queue = [...sequence];
|
|
54
|
+
const fakeFetch = async (input, init) => {
|
|
55
|
+
calls.push({ input, init });
|
|
56
|
+
const next = queue.shift();
|
|
57
|
+
if (!next) {
|
|
58
|
+
throw new Error('No fake fetch result configured');
|
|
59
|
+
}
|
|
60
|
+
if (next instanceof Error) {
|
|
61
|
+
throw next;
|
|
62
|
+
}
|
|
63
|
+
return next;
|
|
64
|
+
};
|
|
65
|
+
return { fakeFetch, calls };
|
|
66
|
+
}
|
|
67
|
+
(0, vitest_1.describe)('unary transport', () => {
|
|
68
|
+
(0, vitest_1.it)('unary posts bytes and returns response bytes', async () => {
|
|
69
|
+
const { fakeFetch, calls } = makeFakeFetch([makeResponse(200, responseBytes)]);
|
|
70
|
+
const transport = new http_js_1.DevstralTransport({ fetch: fakeFetch });
|
|
71
|
+
const headers = new Headers({ 'content-type': 'application/grpc+proto' });
|
|
72
|
+
const output = await transport.postUnary('https://api.test/unary', requestBytes, headers);
|
|
73
|
+
(0, vitest_1.expect)(output).toEqual(responseBytes);
|
|
74
|
+
(0, vitest_1.expect)(calls).toHaveLength(1);
|
|
75
|
+
(0, vitest_1.expect)(calls[0]?.input).toBe('https://api.test/unary');
|
|
76
|
+
(0, vitest_1.expect)(calls[0]?.init?.method).toBe('POST');
|
|
77
|
+
(0, vitest_1.expect)(calls[0]?.init?.headers).toBe(headers);
|
|
78
|
+
(0, vitest_1.expect)(bufferFromBody(calls[0]?.init?.body)).toEqual(requestBytes);
|
|
79
|
+
});
|
|
80
|
+
(0, vitest_1.it)('unary retries 5xx responses and eventually succeeds', async () => {
|
|
81
|
+
const { fakeFetch, calls } = makeFakeFetch([
|
|
82
|
+
makeResponse(500, Buffer.from('first-failure')),
|
|
83
|
+
makeResponse(502, Buffer.from('second-failure')),
|
|
84
|
+
makeResponse(200, responseBytes),
|
|
85
|
+
]);
|
|
86
|
+
const transport = new http_js_1.DevstralTransport({ fetch: fakeFetch });
|
|
87
|
+
const output = await transport.postUnary('https://api.test/unary', requestBytes, new Headers());
|
|
88
|
+
(0, vitest_1.expect)(output).toEqual(responseBytes);
|
|
89
|
+
(0, vitest_1.expect)(calls).toHaveLength(3);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
(0, vitest_1.describe)('streaming transport', () => {
|
|
93
|
+
(0, vitest_1.it)('streaming posts bytes and returns response stream without pre-buffering', async () => {
|
|
94
|
+
let arrayBufferCalled = false;
|
|
95
|
+
const streamingBody = makeChunkStream([
|
|
96
|
+
Uint8Array.from([9, 8]),
|
|
97
|
+
Uint8Array.from([7, 6]),
|
|
98
|
+
]);
|
|
99
|
+
const streamingResponse = {
|
|
100
|
+
ok: true,
|
|
101
|
+
status: 200,
|
|
102
|
+
body: streamingBody,
|
|
103
|
+
arrayBuffer: async () => {
|
|
104
|
+
arrayBufferCalled = true;
|
|
105
|
+
throw new Error('arrayBuffer should not be called for streaming responses');
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
const { fakeFetch, calls } = makeFakeFetch([streamingResponse]);
|
|
109
|
+
const transport = new http_js_1.DevstralTransport({ fetch: fakeFetch });
|
|
110
|
+
const headers = new Headers({ accept: 'application/connect+proto' });
|
|
111
|
+
const output = await transport.postStreaming('https://api.test/stream', requestBytes, headers);
|
|
112
|
+
(0, vitest_1.expect)(output).toBe(streamingBody);
|
|
113
|
+
(0, vitest_1.expect)(await readStreamBytes(output)).toEqual(responseBytes);
|
|
114
|
+
(0, vitest_1.expect)(arrayBufferCalled).toBe(false);
|
|
115
|
+
(0, vitest_1.expect)(calls).toHaveLength(1);
|
|
116
|
+
(0, vitest_1.expect)(calls[0]?.input).toBe('https://api.test/stream');
|
|
117
|
+
(0, vitest_1.expect)(calls[0]?.init?.method).toBe('POST');
|
|
118
|
+
(0, vitest_1.expect)(calls[0]?.init?.headers).toBe(headers);
|
|
119
|
+
(0, vitest_1.expect)(bufferFromBody(calls[0]?.init?.body)).toEqual(requestBytes);
|
|
120
|
+
});
|
|
121
|
+
(0, vitest_1.it)('streaming throws NETWORK_ERROR when response body stream is missing', async () => {
|
|
122
|
+
const responseWithoutBody = {
|
|
123
|
+
ok: true,
|
|
124
|
+
status: 200,
|
|
125
|
+
body: null,
|
|
126
|
+
arrayBuffer: async () => Buffer.alloc(0),
|
|
127
|
+
};
|
|
128
|
+
const { fakeFetch } = makeFakeFetch([responseWithoutBody]);
|
|
129
|
+
const transport = new http_js_1.DevstralTransport({ fetch: fakeFetch });
|
|
130
|
+
await (0, vitest_1.expect)(transport.postStreaming('https://api.test/stream', requestBytes, new Headers())).rejects.toMatchObject({
|
|
131
|
+
code: 'NETWORK_ERROR',
|
|
132
|
+
message: 'Streaming response body is unavailable',
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
(0, vitest_1.it)('streaming uses AbortSignal to cancel request', async () => {
|
|
136
|
+
const controller = new AbortController();
|
|
137
|
+
const abortedError = new Error('Request aborted');
|
|
138
|
+
abortedError.name = 'AbortError';
|
|
139
|
+
const calls = [];
|
|
140
|
+
const fakeFetch = async (input, init) => {
|
|
141
|
+
calls.push({ input, init });
|
|
142
|
+
if (init?.signal?.aborted) {
|
|
143
|
+
throw abortedError;
|
|
144
|
+
}
|
|
145
|
+
return makeResponse(200, responseBytes);
|
|
146
|
+
};
|
|
147
|
+
const transport = new http_js_1.DevstralTransport({ fetch: fakeFetch });
|
|
148
|
+
controller.abort();
|
|
149
|
+
await (0, vitest_1.expect)(transport.postStreaming('https://api.test/stream', requestBytes, new Headers(), controller.signal)).rejects.toMatchObject({ name: 'AbortError' });
|
|
150
|
+
(0, vitest_1.expect)(calls).toHaveLength(1);
|
|
151
|
+
(0, vitest_1.expect)(calls[0]?.init?.signal).toBe(controller.signal);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
(0, vitest_1.describe)('error classification', () => {
|
|
155
|
+
(0, vitest_1.it)('error maps 403 to AUTH_ERROR without retrying', async () => {
|
|
156
|
+
const { fakeFetch, calls } = makeFakeFetch([makeResponse(403, Buffer.from('forbidden'))]);
|
|
157
|
+
const transport = new http_js_1.DevstralTransport({ fetch: fakeFetch });
|
|
158
|
+
await (0, vitest_1.expect)(transport.postUnary('https://api.test/unary', requestBytes, new Headers())).rejects.toMatchObject({ code: 'AUTH_ERROR' });
|
|
159
|
+
(0, vitest_1.expect)(calls).toHaveLength(1);
|
|
160
|
+
});
|
|
161
|
+
(0, vitest_1.it)('error maps 429 to RATE_LIMITED without retrying', async () => {
|
|
162
|
+
const { fakeFetch, calls } = makeFakeFetch([makeResponse(429, Buffer.from('limited'))]);
|
|
163
|
+
const transport = new http_js_1.DevstralTransport({ fetch: fakeFetch });
|
|
164
|
+
await (0, vitest_1.expect)(transport.postUnary('https://api.test/unary', requestBytes, new Headers())).rejects.toMatchObject({ code: 'RATE_LIMITED' });
|
|
165
|
+
(0, vitest_1.expect)(calls).toHaveLength(1);
|
|
166
|
+
});
|
|
167
|
+
(0, vitest_1.it)('error maps network failures to NETWORK_ERROR', async () => {
|
|
168
|
+
const { fakeFetch, calls } = makeFakeFetch([new Error('socket hang up')]);
|
|
169
|
+
const transport = new http_js_1.DevstralTransport({ fetch: fakeFetch });
|
|
170
|
+
await (0, vitest_1.expect)(transport.postUnary('https://api.test/unary', requestBytes, new Headers())).rejects.toMatchObject({ code: 'NETWORK_ERROR' });
|
|
171
|
+
(0, vitest_1.expect)(calls).toHaveLength(1);
|
|
172
|
+
});
|
|
173
|
+
(0, vitest_1.it)('error does not downgrade https requests on TLS failures', async () => {
|
|
174
|
+
const calls = [];
|
|
175
|
+
const fakeFetch = async (input, init) => {
|
|
176
|
+
calls.push({ input, init });
|
|
177
|
+
throw new Error('TLS certificate verify failed');
|
|
178
|
+
};
|
|
179
|
+
const transport = new http_js_1.DevstralTransport({ fetch: fakeFetch });
|
|
180
|
+
const request = transport.postUnary('https://api.test/unary', requestBytes, new Headers());
|
|
181
|
+
await (0, vitest_1.expect)(request).rejects.toBeInstanceOf(http_js_1.DevstralTransportError);
|
|
182
|
+
await (0, vitest_1.expect)(request).rejects.toMatchObject({ code: 'NETWORK_ERROR' });
|
|
183
|
+
(0, vitest_1.expect)(calls).toHaveLength(1);
|
|
184
|
+
(0, vitest_1.expect)(calls[0]?.input).toBe('https://api.test/unary');
|
|
185
|
+
});
|
|
186
|
+
(0, vitest_1.it)('error retries 5xx responses before throwing NETWORK_ERROR', async () => {
|
|
187
|
+
const { fakeFetch, calls } = makeFakeFetch([
|
|
188
|
+
makeResponse(500, Buffer.from('fail-1')),
|
|
189
|
+
makeResponse(500, Buffer.from('fail-2')),
|
|
190
|
+
makeResponse(503, Buffer.from('fail-3')),
|
|
191
|
+
]);
|
|
192
|
+
const transport = new http_js_1.DevstralTransport({ fetch: fakeFetch });
|
|
193
|
+
await (0, vitest_1.expect)(transport.postUnary('https://api.test/unary', requestBytes, new Headers())).rejects.toMatchObject({ code: 'NETWORK_ERROR' });
|
|
194
|
+
(0, vitest_1.expect)(calls).toHaveLength(3);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { DevstralMessage } from '../types/index.js';
|
|
2
|
+
export type LanguageModelV3Prompt = Array<{
|
|
3
|
+
role: 'system';
|
|
4
|
+
content: string;
|
|
5
|
+
} | {
|
|
6
|
+
role: 'user';
|
|
7
|
+
content: Array<{
|
|
8
|
+
type: 'text';
|
|
9
|
+
text: string;
|
|
10
|
+
} | {
|
|
11
|
+
type: 'file';
|
|
12
|
+
data: string;
|
|
13
|
+
mediaType: string;
|
|
14
|
+
} | {
|
|
15
|
+
type: 'image';
|
|
16
|
+
image: string;
|
|
17
|
+
}>;
|
|
18
|
+
} | {
|
|
19
|
+
role: 'assistant';
|
|
20
|
+
content: Array<{
|
|
21
|
+
type: 'text';
|
|
22
|
+
text: string;
|
|
23
|
+
} | {
|
|
24
|
+
type: 'tool-call';
|
|
25
|
+
toolCallId: string;
|
|
26
|
+
toolName: string;
|
|
27
|
+
args: unknown;
|
|
28
|
+
} | {
|
|
29
|
+
type: 'file';
|
|
30
|
+
data: string;
|
|
31
|
+
mediaType: string;
|
|
32
|
+
} | {
|
|
33
|
+
type: 'image';
|
|
34
|
+
image: string;
|
|
35
|
+
} | {
|
|
36
|
+
type: 'reasoning';
|
|
37
|
+
text: string;
|
|
38
|
+
}>;
|
|
39
|
+
} | {
|
|
40
|
+
role: 'tool';
|
|
41
|
+
content: Array<{
|
|
42
|
+
type: 'tool-result';
|
|
43
|
+
toolCallId: string;
|
|
44
|
+
toolName: string;
|
|
45
|
+
result: unknown;
|
|
46
|
+
isError?: boolean;
|
|
47
|
+
}>;
|
|
48
|
+
}>;
|
|
49
|
+
export declare function convertPrompt(prompt: LanguageModelV3Prompt): DevstralMessage[];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface TextPart {
|
|
2
|
+
type: 'text';
|
|
3
|
+
text: string;
|
|
4
|
+
}
|
|
5
|
+
export interface ToolCallPart {
|
|
6
|
+
type: 'tool-call';
|
|
7
|
+
toolCallId: string;
|
|
8
|
+
toolName: string;
|
|
9
|
+
args: unknown;
|
|
10
|
+
}
|
|
11
|
+
export type LanguageModelV3Content = TextPart | ToolCallPart;
|
|
12
|
+
export declare function convertResponse(buffer: Buffer): LanguageModelV3Content[];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
2
|
+
import { resolveApiKey } from './api-key';
|
|
3
|
+
describe('api key resolver', () => {
|
|
4
|
+
const ORIGINAL = process.env.WINDSURF_API_KEY;
|
|
5
|
+
afterEach(() => {
|
|
6
|
+
// restore original env to avoid leakage
|
|
7
|
+
if (ORIGINAL === undefined) {
|
|
8
|
+
delete process.env.WINDSURF_API_KEY;
|
|
9
|
+
}
|
|
10
|
+
else {
|
|
11
|
+
process.env.WINDSURF_API_KEY = ORIGINAL;
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
it('constructor', () => {
|
|
15
|
+
const key = resolveApiKey({ apiKey: 'ctor-key' });
|
|
16
|
+
expect(key).toBe('ctor-key');
|
|
17
|
+
});
|
|
18
|
+
it('env', () => {
|
|
19
|
+
process.env.WINDSURF_API_KEY = 'test-key';
|
|
20
|
+
const key = resolveApiKey();
|
|
21
|
+
expect(key).toBe('test-key');
|
|
22
|
+
});
|
|
23
|
+
it('missing', () => {
|
|
24
|
+
delete process.env.WINDSURF_API_KEY;
|
|
25
|
+
expect(() => resolveApiKey()).toThrowError(new Error('WINDSURF_API_KEY is required'));
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { ProtobufEncoder } from '../protocol/protobuf.js';
|
|
2
|
+
export const AUTH_BASE = 'https://server.self-serve.windsurf.com/exa.auth_pb.AuthService';
|
|
3
|
+
const WS_APP = 'windsurf';
|
|
4
|
+
const WS_APP_VER = process.env.WS_APP_VER ?? '1.48.2';
|
|
5
|
+
const WS_LS_VER = process.env.WS_LS_VER ?? '1.9544.35';
|
|
6
|
+
const JWT_PATTERN = /eyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*/;
|
|
7
|
+
export class JwtManager {
|
|
8
|
+
fetchFn;
|
|
9
|
+
authBase;
|
|
10
|
+
now;
|
|
11
|
+
cache = new Map();
|
|
12
|
+
inFlight = new Map();
|
|
13
|
+
constructor(options = {}) {
|
|
14
|
+
this.fetchFn = options.fetch ?? fetch;
|
|
15
|
+
this.authBase = options.authBase ?? AUTH_BASE;
|
|
16
|
+
this.now = options.now ?? Date.now;
|
|
17
|
+
}
|
|
18
|
+
async getJwt(apiKey) {
|
|
19
|
+
if (!apiKey) {
|
|
20
|
+
throw new Error('API key is required');
|
|
21
|
+
}
|
|
22
|
+
const nowSeconds = Math.floor(this.now() / 1000);
|
|
23
|
+
const cached = this.cache.get(apiKey);
|
|
24
|
+
if (cached && cached.expiresAt > nowSeconds + 60) {
|
|
25
|
+
return cached.token;
|
|
26
|
+
}
|
|
27
|
+
const inFlight = this.inFlight.get(apiKey);
|
|
28
|
+
if (inFlight) {
|
|
29
|
+
return inFlight;
|
|
30
|
+
}
|
|
31
|
+
const pending = this.fetchJwt(apiKey)
|
|
32
|
+
.then((token) => {
|
|
33
|
+
const expiresAt = getJwtExp(token) || Math.floor(this.now() / 1000) + 3600;
|
|
34
|
+
this.cache.set(apiKey, { token, expiresAt });
|
|
35
|
+
return token;
|
|
36
|
+
})
|
|
37
|
+
.finally(() => {
|
|
38
|
+
this.inFlight.delete(apiKey);
|
|
39
|
+
});
|
|
40
|
+
this.inFlight.set(apiKey, pending);
|
|
41
|
+
return pending;
|
|
42
|
+
}
|
|
43
|
+
async fetchJwt(apiKey) {
|
|
44
|
+
const metadata = new ProtobufEncoder();
|
|
45
|
+
metadata.writeString(1, WS_APP);
|
|
46
|
+
metadata.writeString(2, WS_APP_VER);
|
|
47
|
+
metadata.writeString(3, apiKey);
|
|
48
|
+
metadata.writeString(4, 'zh-cn');
|
|
49
|
+
metadata.writeString(7, WS_LS_VER);
|
|
50
|
+
metadata.writeString(12, WS_APP);
|
|
51
|
+
metadata.writeBytes(30, Buffer.from([0x00, 0x01]));
|
|
52
|
+
const requestBody = new ProtobufEncoder();
|
|
53
|
+
requestBody.writeMessage(1, metadata);
|
|
54
|
+
const response = await this.fetchFn(`${this.authBase}/GetUserJwt`, {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
headers: {
|
|
57
|
+
'Content-Type': 'application/proto',
|
|
58
|
+
'Connect-Protocol-Version': '1',
|
|
59
|
+
'User-Agent': 'connect-go/1.18.1 (go1.25.5)',
|
|
60
|
+
},
|
|
61
|
+
body: requestBody.toBuffer(),
|
|
62
|
+
});
|
|
63
|
+
if (!response.ok) {
|
|
64
|
+
throw new Error(`HTTP ${response.status}`);
|
|
65
|
+
}
|
|
66
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
67
|
+
const token = extractJwt(bytes);
|
|
68
|
+
if (!token) {
|
|
69
|
+
throw new Error('Failed to extract JWT from GetUserJwt response');
|
|
70
|
+
}
|
|
71
|
+
return token;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function extractJwt(value) {
|
|
75
|
+
const match = value.toString('utf8').match(JWT_PATTERN);
|
|
76
|
+
return match?.[0] ?? null;
|
|
77
|
+
}
|
|
78
|
+
function getJwtExp(jwt) {
|
|
79
|
+
try {
|
|
80
|
+
const payloadPart = jwt.split('.')[1];
|
|
81
|
+
if (!payloadPart) {
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
const payload = JSON.parse(Buffer.from(payloadPart, 'base64url').toString('utf8'));
|
|
85
|
+
return typeof payload.exp === 'number' ? payload.exp : 0;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { ProtobufEncoder } from '../protocol/protobuf.js';
|
|
3
|
+
import { AUTH_BASE, JwtManager } from './jwt-manager.js';
|
|
4
|
+
function makeJwt(exp, tag) {
|
|
5
|
+
const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url');
|
|
6
|
+
const payload = Buffer.from(JSON.stringify({ exp, tag })).toString('base64url');
|
|
7
|
+
return `${header}.${payload}.signature`;
|
|
8
|
+
}
|
|
9
|
+
function makeJwtResponse(token) {
|
|
10
|
+
const encoder = new ProtobufEncoder();
|
|
11
|
+
encoder.writeString(1, `prefix:${token}:suffix`);
|
|
12
|
+
return new Response(Uint8Array.from(encoder.toBuffer()), { status: 200 });
|
|
13
|
+
}
|
|
14
|
+
function bufferFromBody(body) {
|
|
15
|
+
if (body == null) {
|
|
16
|
+
return Buffer.alloc(0);
|
|
17
|
+
}
|
|
18
|
+
if (typeof body === 'string') {
|
|
19
|
+
return Buffer.from(body, 'utf8');
|
|
20
|
+
}
|
|
21
|
+
if (body instanceof ArrayBuffer) {
|
|
22
|
+
return Buffer.from(body);
|
|
23
|
+
}
|
|
24
|
+
if (ArrayBuffer.isView(body)) {
|
|
25
|
+
return Buffer.from(body.buffer, body.byteOffset, body.byteLength);
|
|
26
|
+
}
|
|
27
|
+
throw new Error(`Unsupported request body type: ${typeof body}`);
|
|
28
|
+
}
|
|
29
|
+
describe('jwt manager fetch', () => {
|
|
30
|
+
it('fetch exchanges api key to jwt and caches result', async () => {
|
|
31
|
+
const exp = 4_000_000_000;
|
|
32
|
+
const token = makeJwt(exp, 'first');
|
|
33
|
+
const calls = [];
|
|
34
|
+
const fakeFetch = async (input, init) => {
|
|
35
|
+
calls.push({ input, init });
|
|
36
|
+
return makeJwtResponse(token);
|
|
37
|
+
};
|
|
38
|
+
const manager = new JwtManager({ fetch: fakeFetch, now: () => (exp - 3_600) * 1000 });
|
|
39
|
+
const jwt1 = await manager.getJwt('test-api-key');
|
|
40
|
+
const jwt2 = await manager.getJwt('test-api-key');
|
|
41
|
+
expect(jwt1).toBe(token);
|
|
42
|
+
expect(jwt2).toBe(token);
|
|
43
|
+
expect(calls).toHaveLength(1);
|
|
44
|
+
expect(calls[0]?.input).toBe(`${AUTH_BASE}/GetUserJwt`);
|
|
45
|
+
expect(calls[0]?.init?.method).toBe('POST');
|
|
46
|
+
expect(new Headers(calls[0]?.init?.headers).get('content-type')).toBe('application/proto');
|
|
47
|
+
const requestBody = bufferFromBody(calls[0]?.init?.body);
|
|
48
|
+
expect(requestBody.length).toBeGreaterThan(0);
|
|
49
|
+
expect(requestBody.toString('utf8')).toContain('test-api-key');
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
describe('jwt manager expiry', () => {
|
|
53
|
+
it('expiry refreshes token when less than sixty seconds remain', async () => {
|
|
54
|
+
let nowMs = 0;
|
|
55
|
+
const token1 = makeJwt(10_000, 'first');
|
|
56
|
+
const token2 = makeJwt(20_000, 'second');
|
|
57
|
+
const queue = [makeJwtResponse(token1), makeJwtResponse(token2)];
|
|
58
|
+
let callCount = 0;
|
|
59
|
+
const fakeFetch = async () => {
|
|
60
|
+
callCount += 1;
|
|
61
|
+
const next = queue.shift();
|
|
62
|
+
if (!next) {
|
|
63
|
+
throw new Error('No fake fetch response queued');
|
|
64
|
+
}
|
|
65
|
+
return next;
|
|
66
|
+
};
|
|
67
|
+
const manager = new JwtManager({ fetch: fakeFetch, now: () => nowMs });
|
|
68
|
+
const first = await manager.getJwt('exp-api-key');
|
|
69
|
+
nowMs = (10_000 - 59) * 1000;
|
|
70
|
+
const second = await manager.getJwt('exp-api-key');
|
|
71
|
+
expect(first).toBe(token1);
|
|
72
|
+
expect(second).toBe(token2);
|
|
73
|
+
expect(callCount).toBe(2);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
describe('jwt manager concurrent', () => {
|
|
77
|
+
it('concurrent getJwt calls share one in-flight fetch', async () => {
|
|
78
|
+
const token = makeJwt(50_000, 'concurrent');
|
|
79
|
+
let fetchCalls = 0;
|
|
80
|
+
let resolveFetch;
|
|
81
|
+
const pending = new Promise((resolve) => {
|
|
82
|
+
resolveFetch = resolve;
|
|
83
|
+
});
|
|
84
|
+
const fakeFetch = async () => {
|
|
85
|
+
fetchCalls += 1;
|
|
86
|
+
return pending;
|
|
87
|
+
};
|
|
88
|
+
const manager = new JwtManager({ fetch: fakeFetch, now: () => 0 });
|
|
89
|
+
const p1 = manager.getJwt('same-key');
|
|
90
|
+
const p2 = manager.getJwt('same-key');
|
|
91
|
+
resolveFetch(makeJwtResponse(token));
|
|
92
|
+
const [jwt1, jwt2] = await Promise.all([p1, p2]);
|
|
93
|
+
expect(fetchCalls).toBe(1);
|
|
94
|
+
expect(jwt1).toBe(token);
|
|
95
|
+
expect(jwt2).toBe(token);
|
|
96
|
+
});
|
|
97
|
+
});
|