vix11 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/src/types.ts ADDED
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Vix11 SDK Type Definitions
3
+ *
4
+ * SDK-specific types for requests and responses when interacting
5
+ * with the Vix11 AI agent security platform API.
6
+ */
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Error
10
+ // ---------------------------------------------------------------------------
11
+
12
+ /** Error thrown by the Vix11 SDK on non-2xx responses. */
13
+ export class Vix11Error extends Error {
14
+ /** HTTP status code returned by the API. */
15
+ public readonly status: number;
16
+ /** Raw response body (if available). */
17
+ public readonly body: unknown;
18
+
19
+ constructor(message: string, status: number, body?: unknown) {
20
+ super(message);
21
+ this.name = 'Vix11Error';
22
+ this.status = status;
23
+ this.body = body;
24
+ }
25
+ }
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Detection
29
+ // ---------------------------------------------------------------------------
30
+
31
+ /** Layer-level score returned by the detection pipeline. */
32
+ export interface LayerScore {
33
+ id: string;
34
+ score: number;
35
+ reason: string;
36
+ durationMs: number;
37
+ }
38
+
39
+ /** Detection metadata returned by detect/shield endpoints. */
40
+ export interface DetectionMeta {
41
+ action: 'allow' | 'throttle' | 'shadow-ban' | 'block';
42
+ score: number;
43
+ totalDurationMs: number;
44
+ trustScore?: number;
45
+ trustTier?: TrustTier;
46
+ }
47
+
48
+ /** Body sent to POST /v1/detect. */
49
+ export interface DetectRequest {
50
+ agentId: string;
51
+ endpoint?: string;
52
+ payload?: string;
53
+ parameters?: Record<string, string>;
54
+ [key: string]: unknown;
55
+ }
56
+
57
+ /** Response from POST /v1/detect. */
58
+ export interface DetectResponse {
59
+ detection: {
60
+ action: 'allow' | 'throttle' | 'shadow-ban' | 'block';
61
+ score: number;
62
+ layers: LayerScore[];
63
+ totalDurationMs: number;
64
+ trustScore?: number;
65
+ trustTier?: TrustTier;
66
+ };
67
+ }
68
+
69
+ /** Body sent to POST /v1/shield. */
70
+ export interface ShieldRequest {
71
+ agentId: string;
72
+ passport?: SignedPassport;
73
+ endpoint?: string;
74
+ payload?: string;
75
+ parameters?: Record<string, string>;
76
+ [key: string]: unknown;
77
+ }
78
+
79
+ /** Response from POST /v1/shield. */
80
+ export interface ShieldResponse {
81
+ status: 'allowed' | 'throttled' | 'blocked';
82
+ retryAfterMs?: number;
83
+ error?: string;
84
+ data?: Record<string, unknown>;
85
+ meta: DetectionMeta;
86
+ }
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Passport
90
+ // ---------------------------------------------------------------------------
91
+
92
+ export interface PassportCapabilities {
93
+ maxTokensPerDay: number;
94
+ allowedEndpoints: string[];
95
+ maxRequestsPerMinute: number;
96
+ }
97
+
98
+ export interface PassportPayload {
99
+ passportId: string;
100
+ agentId: string;
101
+ ownerHash: string;
102
+ capabilities: PassportCapabilities;
103
+ issuedAt: number;
104
+ expiresAt: number;
105
+ version: 1;
106
+ }
107
+
108
+ export interface SignedPassport {
109
+ payload: PassportPayload;
110
+ signature: string;
111
+ publicKey: string;
112
+ }
113
+
114
+ // ---------------------------------------------------------------------------
115
+ // Analytics
116
+ // ---------------------------------------------------------------------------
117
+
118
+ /** Response from GET /v1/analytics/summary. */
119
+ export interface AnalyticsSummary {
120
+ from: number;
121
+ to: number;
122
+ totalRequests: number;
123
+ blockedRequests: number;
124
+ shadowBannedRequests: number;
125
+ distillationAttempts: number;
126
+ estimatedTokensSaved: number;
127
+ estimatedValueSaved: number;
128
+ topAgents: { agentId: string; attempts: number }[];
129
+ layerBreakdown: { layerId: string; avgScore: number; triggerCount: number }[];
130
+ }
131
+
132
+ /** Response from GET /v1/analytics/events. */
133
+ export interface PaginatedEvents {
134
+ events: DistillationEvent[];
135
+ page: number;
136
+ limit: number;
137
+ total: number;
138
+ }
139
+
140
+ export interface DistillationEvent {
141
+ id: string;
142
+ timestamp: number;
143
+ agentId: string;
144
+ action: 'block' | 'shadow-ban' | 'throttle';
145
+ score: number;
146
+ semanticScore: number;
147
+ intentScore: number;
148
+ estimatedTokens: number;
149
+ estimatedValue: number;
150
+ }
151
+
152
+ // ---------------------------------------------------------------------------
153
+ // Compliance
154
+ // ---------------------------------------------------------------------------
155
+
156
+ /** Response from GET /v1/compliance/report. */
157
+ export interface ComplianceReport {
158
+ status: string;
159
+ generatedAt: string;
160
+ [key: string]: unknown;
161
+ }
162
+
163
+ // ---------------------------------------------------------------------------
164
+ // Trust
165
+ // ---------------------------------------------------------------------------
166
+
167
+ /** Trust tier for an AI agent. */
168
+ export type TrustTier = 'untrusted' | 'low' | 'medium' | 'high' | 'verified';
169
+
170
+ /** Response from GET /v1/trust/:agentId. */
171
+ export interface TrustScore {
172
+ agentId: string;
173
+ trustScore: number;
174
+ trustTier: TrustTier;
175
+ totalRequests: number;
176
+ decisions: {
177
+ allow: number;
178
+ throttle: number;
179
+ shadowBan: number;
180
+ block: number;
181
+ };
182
+ lastAction: string;
183
+ lastDetectionScore: number;
184
+ lastUpdated: string;
185
+ createdAt: string;
186
+ }
187
+
188
+ // ---------------------------------------------------------------------------
189
+ // Health
190
+ // ---------------------------------------------------------------------------
191
+
192
+ /** KV probe result from health check. */
193
+ export interface HealthCheck {
194
+ name: string;
195
+ status: 'pass' | 'fail';
196
+ durationMs: number;
197
+ }
198
+
199
+ /** Response from GET /v1/health. */
200
+ export interface HealthResponse {
201
+ status: 'ok' | 'degraded';
202
+ service: string;
203
+ version: string;
204
+ timestamp: string;
205
+ environment: string;
206
+ pipeline: {
207
+ mode: string;
208
+ layers: number;
209
+ };
210
+ thresholds: {
211
+ throttle: number;
212
+ shadowBan: number;
213
+ block: number;
214
+ };
215
+ checks: Record<string, HealthCheck>;
216
+ totalDurationMs: number;
217
+ }
218
+
219
+ // ---------------------------------------------------------------------------
220
+ // Detection Stats
221
+ // ---------------------------------------------------------------------------
222
+
223
+ /** Response from GET /v1/detection/stats. */
224
+ export interface DetectionStats {
225
+ status: string;
226
+ layers: { id: string; status: string; version: string }[];
227
+ thresholds: {
228
+ throttle: number;
229
+ shadowBan: number;
230
+ block: number;
231
+ };
232
+ }
@@ -0,0 +1,280 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { Vix11Client } from '../src/client';
3
+ import { Vix11Error } from '../src/types';
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Helpers
7
+ // ---------------------------------------------------------------------------
8
+
9
+ function makeClient(overrides: Record<string, unknown> = {}) {
10
+ return new Vix11Client({
11
+ baseUrl: 'https://vix11.test',
12
+ agentId: 'agent-1',
13
+ apiKey: 'sk-test',
14
+ timeout: 5000,
15
+ ...overrides,
16
+ });
17
+ }
18
+
19
+ function mockFetchResponse(body: unknown, status = 200) {
20
+ return vi.fn().mockResolvedValue({
21
+ ok: status >= 200 && status < 300,
22
+ status,
23
+ json: () => Promise.resolve(body),
24
+ });
25
+ }
26
+
27
+ const DETECT_RESPONSE = {
28
+ detection: {
29
+ action: 'allow' as const,
30
+ score: 0.12,
31
+ layers: [{ id: 'velocity', score: 0.1, reason: 'ok', durationMs: 1 }],
32
+ totalDurationMs: 2,
33
+ },
34
+ };
35
+
36
+ const SHIELD_RESPONSE = {
37
+ status: 'allowed' as const,
38
+ meta: { action: 'allow' as const, score: 0.05, totalDurationMs: 3 },
39
+ };
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Tests
43
+ // ---------------------------------------------------------------------------
44
+
45
+ describe('Vix11Client', () => {
46
+ let originalFetch: typeof globalThis.fetch;
47
+
48
+ beforeEach(() => {
49
+ originalFetch = globalThis.fetch;
50
+ });
51
+
52
+ afterEach(() => {
53
+ globalThis.fetch = originalFetch;
54
+ });
55
+
56
+ // ---- Construction -------------------------------------------------------
57
+
58
+ it('should construct with config', () => {
59
+ const client = makeClient();
60
+ expect(client).toBeInstanceOf(Vix11Client);
61
+ });
62
+
63
+ it('should strip trailing slash from baseUrl', async () => {
64
+ const fetchMock = mockFetchResponse({ status: 'ok' });
65
+ globalThis.fetch = fetchMock;
66
+
67
+ const client = makeClient({ baseUrl: 'https://vix11.test/' });
68
+ await client.health();
69
+
70
+ expect(fetchMock).toHaveBeenCalledOnce();
71
+ const url = fetchMock.mock.calls[0][0] as string;
72
+ expect(url).toBe('https://vix11.test/v1/health');
73
+ });
74
+
75
+ // ---- Headers ------------------------------------------------------------
76
+
77
+ it('should include X-Vix11-Agent-Id header', async () => {
78
+ const fetchMock = mockFetchResponse({ status: 'ok' });
79
+ globalThis.fetch = fetchMock;
80
+
81
+ await makeClient().health();
82
+
83
+ const options = fetchMock.mock.calls[0][1] as RequestInit;
84
+ const headers = options.headers as Record<string, string>;
85
+ expect(headers['X-Vix11-Agent-Id']).toBe('agent-1');
86
+ });
87
+
88
+ it('should include Authorization header when apiKey is set', async () => {
89
+ const fetchMock = mockFetchResponse({ status: 'ok' });
90
+ globalThis.fetch = fetchMock;
91
+
92
+ await makeClient({ apiKey: 'sk-secret' }).health();
93
+
94
+ const options = fetchMock.mock.calls[0][1] as RequestInit;
95
+ const headers = options.headers as Record<string, string>;
96
+ expect(headers['Authorization']).toBe('Bearer sk-secret');
97
+ });
98
+
99
+ it('should omit Authorization header when apiKey is not set', async () => {
100
+ const fetchMock = mockFetchResponse({ status: 'ok' });
101
+ globalThis.fetch = fetchMock;
102
+
103
+ await makeClient({ apiKey: undefined }).health();
104
+
105
+ const options = fetchMock.mock.calls[0][1] as RequestInit;
106
+ const headers = options.headers as Record<string, string>;
107
+ expect(headers['Authorization']).toBeUndefined();
108
+ });
109
+
110
+ // ---- detect() -----------------------------------------------------------
111
+
112
+ it('should POST to /v1/detect with correct body', async () => {
113
+ const fetchMock = mockFetchResponse(DETECT_RESPONSE);
114
+ globalThis.fetch = fetchMock;
115
+
116
+ const result = await makeClient().detect({ agentId: 'agent-1', endpoint: '/chat' });
117
+
118
+ expect(fetchMock).toHaveBeenCalledOnce();
119
+ const [url, options] = fetchMock.mock.calls[0] as [string, RequestInit];
120
+ expect(url).toBe('https://vix11.test/v1/detect');
121
+ expect(options.method).toBe('POST');
122
+ expect(JSON.parse(options.body as string)).toEqual({ agentId: 'agent-1', endpoint: '/chat' });
123
+ expect(result).toEqual(DETECT_RESPONSE);
124
+ });
125
+
126
+ // ---- shield() -----------------------------------------------------------
127
+
128
+ it('should POST to /v1/shield with correct body', async () => {
129
+ const fetchMock = mockFetchResponse(SHIELD_RESPONSE);
130
+ globalThis.fetch = fetchMock;
131
+
132
+ const result = await makeClient().shield({ agentId: 'agent-1' });
133
+
134
+ const [url, options] = fetchMock.mock.calls[0] as [string, RequestInit];
135
+ expect(url).toBe('https://vix11.test/v1/shield');
136
+ expect(options.method).toBe('POST');
137
+ expect(result).toEqual(SHIELD_RESPONSE);
138
+ });
139
+
140
+ // ---- Analytics ----------------------------------------------------------
141
+
142
+ it('should GET /v1/analytics/summary with query params', async () => {
143
+ const summary = { from: 0, to: 1000, totalRequests: 5 };
144
+ const fetchMock = mockFetchResponse(summary);
145
+ globalThis.fetch = fetchMock;
146
+
147
+ const result = await makeClient().getAnalyticsSummary(100, 200);
148
+
149
+ const url = fetchMock.mock.calls[0][0] as string;
150
+ expect(url).toBe('https://vix11.test/v1/analytics/summary?from=100&to=200');
151
+ expect(result).toEqual(summary);
152
+ });
153
+
154
+ it('should GET /v1/analytics/summary without query params when omitted', async () => {
155
+ const fetchMock = mockFetchResponse({ from: 0, to: 1000 });
156
+ globalThis.fetch = fetchMock;
157
+
158
+ await makeClient().getAnalyticsSummary();
159
+
160
+ const url = fetchMock.mock.calls[0][0] as string;
161
+ expect(url).toBe('https://vix11.test/v1/analytics/summary');
162
+ });
163
+
164
+ it('should GET /v1/analytics/events with pagination', async () => {
165
+ const events = { events: [], page: 2, limit: 10, total: 0 };
166
+ const fetchMock = mockFetchResponse(events);
167
+ globalThis.fetch = fetchMock;
168
+
169
+ const result = await makeClient().getAnalyticsEvents(2, 10);
170
+
171
+ const url = fetchMock.mock.calls[0][0] as string;
172
+ expect(url).toBe('https://vix11.test/v1/analytics/events?page=2&limit=10');
173
+ expect(result).toEqual(events);
174
+ });
175
+
176
+ // ---- Detection stats ----------------------------------------------------
177
+
178
+ it('should GET /v1/detection/stats', async () => {
179
+ const stats = { status: 'ok', layers: [], thresholds: { throttle: 0.3, shadowBan: 0.6, block: 0.85 } };
180
+ const fetchMock = mockFetchResponse(stats);
181
+ globalThis.fetch = fetchMock;
182
+
183
+ const result = await makeClient().getDetectionStats();
184
+
185
+ const url = fetchMock.mock.calls[0][0] as string;
186
+ expect(url).toBe('https://vix11.test/v1/detection/stats');
187
+ expect(result).toEqual(stats);
188
+ });
189
+
190
+ // ---- Health -------------------------------------------------------------
191
+
192
+ it('should GET /v1/health', async () => {
193
+ const fetchMock = mockFetchResponse({ status: 'ok' });
194
+ globalThis.fetch = fetchMock;
195
+
196
+ const result = await makeClient().health();
197
+
198
+ expect(result).toEqual({ status: 'ok' });
199
+ });
200
+
201
+ // ---- Error handling -----------------------------------------------------
202
+
203
+ it('should throw Vix11Error on non-2xx response', async () => {
204
+ const fetchMock = mockFetchResponse({ error: 'agentId is required' }, 400);
205
+ globalThis.fetch = fetchMock;
206
+
207
+ await expect(makeClient().detect({ agentId: '' })).rejects.toThrow(Vix11Error);
208
+ });
209
+
210
+ it('should include status and message in Vix11Error', async () => {
211
+ const fetchMock = mockFetchResponse({ error: 'Forbidden' }, 403);
212
+ globalThis.fetch = fetchMock;
213
+
214
+ try {
215
+ await makeClient().detect({ agentId: 'x' });
216
+ expect.fail('should have thrown');
217
+ } catch (err) {
218
+ expect(err).toBeInstanceOf(Vix11Error);
219
+ const vixErr = err as Vix11Error;
220
+ expect(vixErr.status).toBe(403);
221
+ expect(vixErr.message).toBe('Forbidden');
222
+ expect(vixErr.body).toEqual({ error: 'Forbidden' });
223
+ }
224
+ });
225
+
226
+ it('should throw Vix11Error with generic message when response has no error field', async () => {
227
+ const fetchMock = mockFetchResponse(null, 500);
228
+ globalThis.fetch = fetchMock;
229
+
230
+ try {
231
+ await makeClient().health();
232
+ expect.fail('should have thrown');
233
+ } catch (err) {
234
+ const vixErr = err as Vix11Error;
235
+ expect(vixErr.status).toBe(500);
236
+ expect(vixErr.message).toBe('Request failed with status 500');
237
+ }
238
+ });
239
+
240
+ // ---- Timeout ------------------------------------------------------------
241
+
242
+ it('should throw Vix11Error on timeout', async () => {
243
+ globalThis.fetch = vi.fn().mockImplementation((_url: string, init: RequestInit) => {
244
+ return new Promise((_resolve, reject) => {
245
+ const onAbort = () => {
246
+ reject(new DOMException('The operation was aborted.', 'AbortError'));
247
+ };
248
+ if (init.signal) {
249
+ init.signal.addEventListener('abort', onAbort);
250
+ }
251
+ });
252
+ });
253
+
254
+ const client = makeClient({ timeout: 50 });
255
+
256
+ try {
257
+ await client.health();
258
+ expect.fail('should have thrown');
259
+ } catch (err) {
260
+ expect(err).toBeInstanceOf(Vix11Error);
261
+ const vixErr = err as Vix11Error;
262
+ expect(vixErr.status).toBe(0);
263
+ expect(vixErr.message).toContain('timed out');
264
+ }
265
+ });
266
+
267
+ // ---- Compliance ---------------------------------------------------------
268
+
269
+ it('should GET /v1/compliance/report', async () => {
270
+ const report = { status: 'compliant', generatedAt: '2025-01-01T00:00:00Z' };
271
+ const fetchMock = mockFetchResponse(report);
272
+ globalThis.fetch = fetchMock;
273
+
274
+ const result = await makeClient().getComplianceReport();
275
+
276
+ const url = fetchMock.mock.calls[0][0] as string;
277
+ expect(url).toBe('https://vix11.test/v1/compliance/report');
278
+ expect(result).toEqual(report);
279
+ });
280
+ });
@@ -0,0 +1,193 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { vix11Middleware } from '../src/middleware';
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Helpers
6
+ // ---------------------------------------------------------------------------
7
+
8
+ function makeReq(overrides: Record<string, unknown> = {}) {
9
+ return {
10
+ ip: '127.0.0.1',
11
+ path: '/api/chat',
12
+ method: 'POST',
13
+ body: { message: 'hello' },
14
+ headers: {} as Record<string, string | string[] | undefined>,
15
+ ...overrides,
16
+ };
17
+ }
18
+
19
+ function makeRes() {
20
+ const res = {
21
+ statusCode: 200,
22
+ body: null as unknown,
23
+ status(code: number) {
24
+ res.statusCode = code;
25
+ return res;
26
+ },
27
+ json(data: unknown) {
28
+ res.body = data;
29
+ },
30
+ };
31
+ return res;
32
+ }
33
+
34
+ const CONFIG = {
35
+ baseUrl: 'https://vix11.test',
36
+ agentId: 'agent-1',
37
+ apiKey: 'sk-test',
38
+ };
39
+
40
+ function mockDetectResponse(action: string) {
41
+ return {
42
+ detection: {
43
+ action,
44
+ score: action === 'allow' ? 0.05 : 0.9,
45
+ layers: [],
46
+ totalDurationMs: 1,
47
+ },
48
+ };
49
+ }
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // Tests
53
+ // ---------------------------------------------------------------------------
54
+
55
+ describe('vix11Middleware', () => {
56
+ let originalFetch: typeof globalThis.fetch;
57
+
58
+ beforeEach(() => {
59
+ originalFetch = globalThis.fetch;
60
+ });
61
+
62
+ afterEach(() => {
63
+ globalThis.fetch = originalFetch;
64
+ });
65
+
66
+ it('should pass clean requests through (action=allow)', async () => {
67
+ globalThis.fetch = vi.fn().mockResolvedValue({
68
+ ok: true,
69
+ status: 200,
70
+ json: () => Promise.resolve(mockDetectResponse('allow')),
71
+ });
72
+
73
+ const middleware = vix11Middleware(CONFIG);
74
+ const req = makeReq();
75
+ const res = makeRes();
76
+ const next = vi.fn();
77
+
78
+ await new Promise<void>((resolve) => {
79
+ middleware(req, res, (...args: unknown[]) => {
80
+ next(...args);
81
+ resolve();
82
+ });
83
+ });
84
+
85
+ expect(next).toHaveBeenCalledOnce();
86
+ expect(next).toHaveBeenCalledWith(); // no error argument
87
+ });
88
+
89
+ it('should block suspicious requests (action=block) with 403', async () => {
90
+ globalThis.fetch = vi.fn().mockResolvedValue({
91
+ ok: true,
92
+ status: 200,
93
+ json: () => Promise.resolve(mockDetectResponse('block')),
94
+ });
95
+
96
+ const middleware = vix11Middleware(CONFIG);
97
+ const req = makeReq();
98
+ const res = makeRes();
99
+ const next = vi.fn();
100
+
101
+ // Wait for the async middleware to finish.
102
+ await new Promise<void>((resolve) => {
103
+ const originalJson = res.json.bind(res);
104
+ res.json = (data: unknown) => {
105
+ originalJson(data);
106
+ resolve();
107
+ };
108
+ middleware(req, res, next);
109
+ });
110
+
111
+ expect(next).not.toHaveBeenCalled();
112
+ expect(res.statusCode).toBe(403);
113
+ expect(res.body).toEqual({
114
+ error: 'Request blocked by Vix11 security policy',
115
+ action: 'block',
116
+ });
117
+ });
118
+
119
+ it('should throttle requests (action=throttle) with 429', async () => {
120
+ globalThis.fetch = vi.fn().mockResolvedValue({
121
+ ok: true,
122
+ status: 200,
123
+ json: () => Promise.resolve(mockDetectResponse('throttle')),
124
+ });
125
+
126
+ const middleware = vix11Middleware(CONFIG);
127
+ const req = makeReq();
128
+ const res = makeRes();
129
+ const next = vi.fn();
130
+
131
+ await new Promise<void>((resolve) => {
132
+ const originalJson = res.json.bind(res);
133
+ res.json = (data: unknown) => {
134
+ originalJson(data);
135
+ resolve();
136
+ };
137
+ middleware(req, res, next);
138
+ });
139
+
140
+ expect(next).not.toHaveBeenCalled();
141
+ expect(res.statusCode).toBe(429);
142
+ expect(res.body).toEqual({
143
+ error: 'Request throttled by Vix11 security policy',
144
+ action: 'throttle',
145
+ retryAfterMs: 5000,
146
+ });
147
+ });
148
+
149
+ it('should attach detection result to req.vix11', async () => {
150
+ globalThis.fetch = vi.fn().mockResolvedValue({
151
+ ok: true,
152
+ status: 200,
153
+ json: () => Promise.resolve(mockDetectResponse('allow')),
154
+ });
155
+
156
+ const middleware = vix11Middleware(CONFIG);
157
+ const req = makeReq();
158
+ const res = makeRes();
159
+
160
+ await new Promise<void>((resolve) => {
161
+ middleware(req, res, () => {
162
+ resolve();
163
+ });
164
+ });
165
+
166
+ expect(req.vix11).toBeDefined();
167
+ expect(req.vix11).toEqual({
168
+ action: 'allow',
169
+ score: 0.05,
170
+ layers: [],
171
+ totalDurationMs: 1,
172
+ });
173
+ });
174
+
175
+ it('should call next with error when detection fails', async () => {
176
+ globalThis.fetch = vi.fn().mockRejectedValue(new Error('network error'));
177
+
178
+ const middleware = vix11Middleware(CONFIG);
179
+ const req = makeReq();
180
+ const res = makeRes();
181
+ const next = vi.fn();
182
+
183
+ await new Promise<void>((resolve) => {
184
+ middleware(req, res, (...args: unknown[]) => {
185
+ next(...args);
186
+ resolve();
187
+ });
188
+ });
189
+
190
+ expect(next).toHaveBeenCalledOnce();
191
+ expect(next.mock.calls[0][0]).toBeInstanceOf(Error);
192
+ });
193
+ });