mcp-proxy 5.11.2 → 5.12.1

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/jsr.json CHANGED
@@ -3,5 +3,5 @@
3
3
  "include": ["src/index.ts", "src/bin/mcp-proxy.ts"],
4
4
  "license": "MIT",
5
5
  "name": "@punkpeye/mcp-proxy",
6
- "version": "5.11.2"
6
+ "version": "5.12.1"
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-proxy",
3
- "version": "5.11.2",
3
+ "version": "5.12.1",
4
4
  "main": "dist/index.js",
5
5
  "scripts": {
6
6
  "build": "tsdown",
@@ -38,27 +38,27 @@
38
38
  ]
39
39
  },
40
40
  "devDependencies": {
41
- "@eslint/js": "^9.36.0",
42
- "@modelcontextprotocol/sdk": "^1.18.1",
43
- "@sebbo2002/semantic-release-jsr": "^3.0.1",
44
- "@tsconfig/node22": "^22.0.2",
45
- "@types/express": "^5.0.3",
46
- "@types/node": "^24.5.2",
47
- "@types/yargs": "^17.0.33",
48
- "eslint": "^9.36.0",
41
+ "@eslint/js": "^9.39.1",
42
+ "@modelcontextprotocol/sdk": "^1.24.3",
43
+ "@sebbo2002/semantic-release-jsr": "^3.1.0",
44
+ "@tsconfig/node22": "^22.0.5",
45
+ "@types/express": "^5.0.6",
46
+ "@types/node": "^24.10.1",
47
+ "@types/yargs": "^17.0.35",
48
+ "eslint": "^9.39.1",
49
49
  "eslint-config-prettier": "^10.1.8",
50
- "eslint-plugin-perfectionist": "^4.15.0",
51
- "eventsource": "^4.0.0",
52
- "express": "^5.0.1",
50
+ "eslint-plugin-perfectionist": "^4.15.1",
51
+ "eventsource": "^4.1.0",
52
+ "express": "^5.2.1",
53
53
  "get-port-please": "^3.2.0",
54
- "jiti": "^2.5.1",
54
+ "jiti": "^2.6.1",
55
55
  "jsr": "^0.13.5",
56
- "prettier": "^3.6.2",
57
- "semantic-release": "^24.2.8",
58
- "tsdown": "^0.15.2",
59
- "tsx": "^4.20.5",
60
- "typescript": "^5.9.2",
61
- "typescript-eslint": "^8.44.0",
56
+ "prettier": "^3.7.4",
57
+ "semantic-release": "^24.2.9",
58
+ "tsdown": "^0.15.12",
59
+ "tsx": "^4.21.0",
60
+ "typescript": "^5.9.3",
61
+ "typescript-eslint": "^8.48.1",
62
62
  "vitest": "^3.2.4",
63
63
  "yargs": "^18.0.0"
64
64
  },
@@ -8,7 +8,7 @@ describe("InMemoryEventStore", () => {
8
8
  it("stores events and replays them after a specific event ID", async () => {
9
9
  const store = new InMemoryEventStore();
10
10
  const streamId = "test-stream-123";
11
-
11
+
12
12
  // Create test messages
13
13
  const messages: JSONRPCMessage[] = [
14
14
  { id: 1, jsonrpc: "2.0", method: "initialize" },
@@ -17,7 +17,7 @@ describe("InMemoryEventStore", () => {
17
17
  { id: 3, jsonrpc: "2.0", result: { success: true } },
18
18
  { id: 4, jsonrpc: "2.0", method: "shutdown" },
19
19
  ];
20
-
20
+
21
21
  // Store all events and keep track of event IDs
22
22
  // Add small delays to ensure different timestamps for proper ordering
23
23
  const eventIds: string[] = [];
@@ -26,32 +26,33 @@ describe("InMemoryEventStore", () => {
26
26
  expect(eventId).toContain(streamId);
27
27
  eventIds.push(eventId);
28
28
  // Small delay to ensure different timestamps
29
- await new Promise(resolve => setTimeout(resolve, 1));
29
+ await new Promise((resolve) => setTimeout(resolve, 1));
30
30
  }
31
-
31
+
32
32
  // Test replaying events after the second event
33
- const replayedEvents: Array<{ eventId: string; message: JSONRPCMessage }> = [];
33
+ const replayedEvents: Array<{ eventId: string; message: JSONRPCMessage }> =
34
+ [];
34
35
  const sendMock = vi.fn(async (eventId: string, message: JSONRPCMessage) => {
35
36
  replayedEvents.push({ eventId, message });
36
37
  });
37
-
38
+
38
39
  const returnedStreamId = await store.replayEventsAfter(
39
40
  eventIds[1], // Replay after the second event
40
- { send: sendMock }
41
+ { send: sendMock },
41
42
  );
42
-
43
+
43
44
  // Verify the correct stream ID was returned
44
45
  expect(returnedStreamId).toBe(streamId);
45
-
46
+
46
47
  // Verify that events 3, 4, and 5 were replayed (after event 2)
47
48
  expect(replayedEvents).toHaveLength(3);
48
49
  expect(sendMock).toHaveBeenCalledTimes(3);
49
-
50
+
50
51
  // Verify the replayed messages are correct and in order
51
52
  expect(replayedEvents[0].message).toEqual(messages[2]);
52
53
  expect(replayedEvents[1].message).toEqual(messages[3]);
53
54
  expect(replayedEvents[2].message).toEqual(messages[4]);
54
-
55
+
55
56
  // Verify event IDs are preserved
56
57
  expect(replayedEvents[0].eventId).toBe(eventIds[2]);
57
58
  expect(replayedEvents[1].eventId).toBe(eventIds[3]);
@@ -62,99 +63,106 @@ describe("InMemoryEventStore", () => {
62
63
  const store = new InMemoryEventStore();
63
64
  const streamId1 = "stream-alpha";
64
65
  const streamId2 = "stream-beta";
65
-
66
+
66
67
  // Create messages for two different streams
67
68
  const stream1Messages: JSONRPCMessage[] = [
68
69
  { id: 1, jsonrpc: "2.0", method: "stream1.init" },
69
70
  { id: 2, jsonrpc: "2.0", method: "stream1.process" },
70
71
  { id: 3, jsonrpc: "2.0", method: "stream1.complete" },
71
72
  ];
72
-
73
+
73
74
  const stream2Messages: JSONRPCMessage[] = [
74
75
  { id: 10, jsonrpc: "2.0", method: "stream2.init" },
75
76
  { id: 20, jsonrpc: "2.0", method: "stream2.process" },
76
77
  { id: 30, jsonrpc: "2.0", method: "stream2.complete" },
77
78
  ];
78
-
79
+
79
80
  // Interleave storing events from both streams with small delays
80
81
  const stream1EventIds: string[] = [];
81
82
  const stream2EventIds: string[] = [];
82
-
83
+
83
84
  // Store first event from each stream
84
85
  stream1EventIds.push(await store.storeEvent(streamId1, stream1Messages[0]));
85
- await new Promise(resolve => setTimeout(resolve, 1));
86
+ await new Promise((resolve) => setTimeout(resolve, 1));
86
87
  stream2EventIds.push(await store.storeEvent(streamId2, stream2Messages[0]));
87
- await new Promise(resolve => setTimeout(resolve, 1));
88
-
88
+ await new Promise((resolve) => setTimeout(resolve, 1));
89
+
89
90
  // Store second event from each stream
90
91
  stream1EventIds.push(await store.storeEvent(streamId1, stream1Messages[1]));
91
- await new Promise(resolve => setTimeout(resolve, 1));
92
+ await new Promise((resolve) => setTimeout(resolve, 1));
92
93
  stream2EventIds.push(await store.storeEvent(streamId2, stream2Messages[1]));
93
- await new Promise(resolve => setTimeout(resolve, 1));
94
-
94
+ await new Promise((resolve) => setTimeout(resolve, 1));
95
+
95
96
  // Store third event from each stream
96
97
  stream1EventIds.push(await store.storeEvent(streamId1, stream1Messages[2]));
97
- await new Promise(resolve => setTimeout(resolve, 1));
98
+ await new Promise((resolve) => setTimeout(resolve, 1));
98
99
  stream2EventIds.push(await store.storeEvent(streamId2, stream2Messages[2]));
99
-
100
+
100
101
  // Replay events from stream 1 after its first event
101
- const stream1ReplayedEvents: Array<{ eventId: string; message: JSONRPCMessage }> = [];
102
- const stream1SendMock = vi.fn(async (eventId: string, message: JSONRPCMessage) => {
103
- stream1ReplayedEvents.push({ eventId, message });
104
- });
105
-
102
+ const stream1ReplayedEvents: Array<{
103
+ eventId: string;
104
+ message: JSONRPCMessage;
105
+ }> = [];
106
+ const stream1SendMock = vi.fn(
107
+ async (eventId: string, message: JSONRPCMessage) => {
108
+ stream1ReplayedEvents.push({ eventId, message });
109
+ },
110
+ );
111
+
106
112
  const returnedStreamId1 = await store.replayEventsAfter(
107
113
  stream1EventIds[0],
108
- { send: stream1SendMock }
114
+ { send: stream1SendMock },
109
115
  );
110
-
116
+
111
117
  // Verify only stream 1 events were replayed
112
118
  expect(returnedStreamId1).toBe(streamId1);
113
119
  expect(stream1ReplayedEvents).toHaveLength(2);
114
120
  expect(stream1ReplayedEvents[0].message).toEqual(stream1Messages[1]);
115
121
  expect(stream1ReplayedEvents[1].message).toEqual(stream1Messages[2]);
116
-
122
+
117
123
  // Verify no stream 2 events were included
118
124
  for (const event of stream1ReplayedEvents) {
119
125
  expect(event.eventId).toContain(streamId1);
120
126
  expect(event.eventId).not.toContain(streamId2);
121
127
  }
122
-
128
+
123
129
  // Now replay events from stream 2 after its first event
124
- const stream2ReplayedEvents: Array<{ eventId: string; message: JSONRPCMessage }> = [];
125
- const stream2SendMock = vi.fn(async (eventId: string, message: JSONRPCMessage) => {
126
- stream2ReplayedEvents.push({ eventId, message });
127
- });
128
-
130
+ const stream2ReplayedEvents: Array<{
131
+ eventId: string;
132
+ message: JSONRPCMessage;
133
+ }> = [];
134
+ const stream2SendMock = vi.fn(
135
+ async (eventId: string, message: JSONRPCMessage) => {
136
+ stream2ReplayedEvents.push({ eventId, message });
137
+ },
138
+ );
139
+
129
140
  const returnedStreamId2 = await store.replayEventsAfter(
130
141
  stream2EventIds[0],
131
- { send: stream2SendMock }
142
+ { send: stream2SendMock },
132
143
  );
133
-
144
+
134
145
  // Verify only stream 2 events were replayed
135
146
  expect(returnedStreamId2).toBe(streamId2);
136
147
  expect(stream2ReplayedEvents).toHaveLength(2);
137
148
  expect(stream2ReplayedEvents[0].message).toEqual(stream2Messages[1]);
138
149
  expect(stream2ReplayedEvents[1].message).toEqual(stream2Messages[2]);
139
-
150
+
140
151
  // Verify no stream 1 events were included
141
152
  for (const event of stream2ReplayedEvents) {
142
153
  expect(event.eventId).toContain(streamId2);
143
154
  expect(event.eventId).not.toContain(streamId1);
144
155
  }
145
-
156
+
146
157
  // Test edge case: replay with non-existent event ID returns empty string
147
158
  const invalidResult = await store.replayEventsAfter(
148
159
  "non-existent-event-id",
149
- { send: vi.fn() }
160
+ { send: vi.fn() },
150
161
  );
151
162
  expect(invalidResult).toBe("");
152
-
163
+
153
164
  // Test edge case: replay with empty event ID returns empty string
154
- const emptyResult = await store.replayEventsAfter(
155
- "",
156
- { send: vi.fn() }
157
- );
165
+ const emptyResult = await store.replayEventsAfter("", { send: vi.fn() });
158
166
  expect(emptyResult).toBe("");
159
167
  });
160
168
 
@@ -188,12 +196,12 @@ describe("InMemoryEventStore", () => {
188
196
 
189
197
  const timestampParts = parts.map(([, timestamp]) => timestamp);
190
198
  expect(timestampParts).toEqual(
191
- Array(messages.length).fill(fixedTimestamp.toString())
199
+ Array(messages.length).fill(fixedTimestamp.toString()),
192
200
  );
193
201
 
194
202
  const counterSuffixes = parts.map(([, , counter]) => counter);
195
203
  expect(counterSuffixes).toEqual(
196
- messages.map((_, index) => (index).toString(36).padStart(4, "0"))
204
+ messages.map((_, index) => index.toString(36).padStart(4, "0")),
197
205
  );
198
206
 
199
207
  // Random parts should be 3 base36 characters each (due to substring(2, 5))
@@ -229,4 +237,4 @@ describe("InMemoryEventStore", () => {
229
237
  secondSpy.mockRestore();
230
238
  }
231
239
  });
232
- });
240
+ });
@@ -81,13 +81,12 @@ export class InMemoryEventStore implements EventStore {
81
81
  }
82
82
 
83
83
  /**
84
- * Generates a monotonic unique event ID in
84
+ * Generates a monotonic unique event ID in
85
85
  * `${streamId}_${timestamp}_${counter}_${random}` format.
86
86
  */
87
87
  private generateEventId(streamId: string): string {
88
-
89
88
  const now = Date.now();
90
-
89
+
91
90
  if (now === this.lastTimestamp) {
92
91
  this.lastTimestampCounter++;
93
92
  } else {
@@ -4,7 +4,9 @@ import { describe, expect, it } from "vitest";
4
4
  import { AuthenticationMiddleware } from "./authentication.js";
5
5
 
6
6
  describe("AuthenticationMiddleware", () => {
7
- const createMockRequest = (headers: Record<string, string> = {}): IncomingMessage => {
7
+ const createMockRequest = (
8
+ headers: Record<string, string> = {},
9
+ ): IncomingMessage => {
8
10
  // Simulate Node.js http module behavior which converts all header names to lowercase
9
11
  const lowercaseHeaders: Record<string, string> = {};
10
12
  for (const [key, value] of Object.entries(headers)) {
@@ -98,7 +100,9 @@ describe("AuthenticationMiddleware", () => {
98
100
 
99
101
  const body = JSON.parse(response.body);
100
102
  expect(body.error.code).toBe(401);
101
- expect(body.error.message).toBe("Unauthorized: Invalid or missing API key");
103
+ expect(body.error.message).toBe(
104
+ "Unauthorized: Invalid or missing API key",
105
+ );
102
106
  expect(body.jsonrpc).toBe("2.0");
103
107
  expect(body.id).toBe(null);
104
108
  });
@@ -207,8 +211,12 @@ describe("AuthenticationMiddleware", () => {
207
211
  });
208
212
  const response = middleware.getUnauthorizedResponse();
209
213
 
210
- expect(response.headers["WWW-Authenticate"]).toContain('realm="example-realm"');
211
- expect(response.headers["WWW-Authenticate"]).toContain('resource_metadata="https://example.com/.well-known/oauth-protected-resource"');
214
+ expect(response.headers["WWW-Authenticate"]).toContain(
215
+ 'realm="example-realm"',
216
+ );
217
+ expect(response.headers["WWW-Authenticate"]).toContain(
218
+ 'resource_metadata="https://example.com/.well-known/oauth-protected-resource"',
219
+ );
212
220
  });
213
221
 
214
222
  it("should include custom error in WWW-Authenticate header via options", () => {
@@ -225,8 +233,12 @@ describe("AuthenticationMiddleware", () => {
225
233
  error_description: "The request requires higher privileges",
226
234
  });
227
235
 
228
- expect(response.headers["WWW-Authenticate"]).toContain('error="insufficient_scope"');
229
- expect(response.headers["WWW-Authenticate"]).toContain('error_description="The request requires higher privileges"');
236
+ expect(response.headers["WWW-Authenticate"]).toContain(
237
+ 'error="insufficient_scope"',
238
+ );
239
+ expect(response.headers["WWW-Authenticate"]).toContain(
240
+ 'error_description="The request requires higher privileges"',
241
+ );
230
242
  });
231
243
 
232
244
  it("should include scope in WWW-Authenticate header", () => {
@@ -241,7 +253,9 @@ describe("AuthenticationMiddleware", () => {
241
253
  });
242
254
  const response = middleware.getUnauthorizedResponse();
243
255
 
244
- expect(response.headers["WWW-Authenticate"]).toContain('scope="read write"');
256
+ expect(response.headers["WWW-Authenticate"]).toContain(
257
+ 'scope="read write"',
258
+ );
245
259
  });
246
260
 
247
261
  it("should override config error with options error", () => {
@@ -260,10 +274,18 @@ describe("AuthenticationMiddleware", () => {
260
274
  error_description: "Options error description",
261
275
  });
262
276
 
263
- expect(response.headers["WWW-Authenticate"]).toContain('error="invalid_token"');
264
- expect(response.headers["WWW-Authenticate"]).toContain('error_description="Options error description"');
265
- expect(response.headers["WWW-Authenticate"]).not.toContain("invalid_request");
266
- expect(response.headers["WWW-Authenticate"]).not.toContain("Config error description");
277
+ expect(response.headers["WWW-Authenticate"]).toContain(
278
+ 'error="invalid_token"',
279
+ );
280
+ expect(response.headers["WWW-Authenticate"]).toContain(
281
+ 'error_description="Options error description"',
282
+ );
283
+ expect(response.headers["WWW-Authenticate"]).not.toContain(
284
+ "invalid_request",
285
+ );
286
+ expect(response.headers["WWW-Authenticate"]).not.toContain(
287
+ "Config error description",
288
+ );
267
289
  });
268
290
 
269
291
  it("should include error_uri in WWW-Authenticate header", () => {
@@ -278,7 +300,9 @@ describe("AuthenticationMiddleware", () => {
278
300
  });
279
301
  const response = middleware.getUnauthorizedResponse();
280
302
 
281
- expect(response.headers["WWW-Authenticate"]).toContain('error_uri="https://example.com/errors/auth"');
303
+ expect(response.headers["WWW-Authenticate"]).toContain(
304
+ 'error_uri="https://example.com/errors/auth"',
305
+ );
282
306
  });
283
307
 
284
308
  it("should properly escape quotes in error_description", () => {
@@ -294,7 +318,9 @@ describe("AuthenticationMiddleware", () => {
294
318
  error_description: 'Token "abc123" is invalid',
295
319
  });
296
320
 
297
- expect(response.headers["WWW-Authenticate"]).toContain('error_description="Token \\"abc123\\" is invalid"');
321
+ expect(response.headers["WWW-Authenticate"]).toContain(
322
+ 'error_description="Token \\"abc123\\" is invalid"',
323
+ );
298
324
  });
299
325
 
300
326
  it("should include all parameters in correct order", () => {
@@ -315,16 +341,247 @@ describe("AuthenticationMiddleware", () => {
315
341
 
316
342
  const header = response.headers["WWW-Authenticate"];
317
343
  expect(header).toContain('realm="my-realm"');
318
- expect(header).toContain('resource_metadata="https://example.com/.well-known/oauth-protected-resource"');
344
+ expect(header).toContain(
345
+ 'resource_metadata="https://example.com/.well-known/oauth-protected-resource"',
346
+ );
319
347
  expect(header).toContain('error="invalid_token"');
320
348
  expect(header).toContain('error_description="Token expired"');
321
349
  expect(header).toContain('error_uri="https://example.com/errors"');
322
350
  expect(header).toContain('scope="read write"');
323
351
 
324
352
  // Check order: realm, resource_metadata, error, error_description, error_uri, scope
325
- expect(header.indexOf('realm=')).toBeLessThan(header.indexOf('resource_metadata='));
326
- expect(header.indexOf('resource_metadata=')).toBeLessThan(header.indexOf('error='));
327
- expect(header.indexOf('error=')).toBeLessThan(header.indexOf('error_description='));
353
+ expect(header.indexOf("realm=")).toBeLessThan(
354
+ header.indexOf("resource_metadata="),
355
+ );
356
+ expect(header.indexOf("resource_metadata=")).toBeLessThan(
357
+ header.indexOf("error="),
358
+ );
359
+ expect(header.indexOf("error=")).toBeLessThan(
360
+ header.indexOf("error_description="),
361
+ );
362
+ });
363
+ });
364
+
365
+ describe("getScopeChallengeResponse", () => {
366
+ it("should return 403 status code", () => {
367
+ const middleware = new AuthenticationMiddleware({
368
+ oauth: {
369
+ protectedResource: {
370
+ resource: "https://example.com",
371
+ },
372
+ },
373
+ });
374
+ const response = middleware.getScopeChallengeResponse(["read", "write"]);
375
+
376
+ expect(response.statusCode).toBe(403);
377
+ });
378
+
379
+ it("should include required scopes in WWW-Authenticate header", () => {
380
+ const middleware = new AuthenticationMiddleware({
381
+ oauth: {
382
+ protectedResource: {
383
+ resource: "https://example.com",
384
+ },
385
+ },
386
+ });
387
+ const response = middleware.getScopeChallengeResponse(["read", "write"]);
388
+
389
+ expect(response.headers["WWW-Authenticate"]).toContain(
390
+ 'error="insufficient_scope"',
391
+ );
392
+ expect(response.headers["WWW-Authenticate"]).toContain(
393
+ 'scope="read write"',
394
+ );
395
+ expect(response.headers["WWW-Authenticate"]).toContain(
396
+ 'resource_metadata="https://example.com/.well-known/oauth-protected-resource"',
397
+ );
398
+ });
399
+
400
+ it("should include error_description in WWW-Authenticate header", () => {
401
+ const middleware = new AuthenticationMiddleware({
402
+ oauth: {
403
+ protectedResource: {
404
+ resource: "https://example.com",
405
+ },
406
+ },
407
+ });
408
+ const response = middleware.getScopeChallengeResponse(
409
+ ["admin"],
410
+ "Admin access required",
411
+ );
412
+
413
+ expect(response.headers["WWW-Authenticate"]).toContain(
414
+ 'error_description="Admin access required"',
415
+ );
416
+ });
417
+
418
+ it("should escape quotes in error_description", () => {
419
+ const middleware = new AuthenticationMiddleware({
420
+ oauth: {
421
+ protectedResource: {
422
+ resource: "https://example.com",
423
+ },
424
+ },
425
+ });
426
+ const response = middleware.getScopeChallengeResponse(
427
+ ["admin"],
428
+ 'Requires "admin" scope',
429
+ );
430
+
431
+ expect(response.headers["WWW-Authenticate"]).toContain(
432
+ 'error_description="Requires \\"admin\\" scope"',
433
+ );
434
+ });
435
+
436
+ it("should include request ID in response body", () => {
437
+ const middleware = new AuthenticationMiddleware({
438
+ oauth: {
439
+ protectedResource: {
440
+ resource: "https://example.com",
441
+ },
442
+ },
443
+ });
444
+ const response = middleware.getScopeChallengeResponse(
445
+ ["read"],
446
+ undefined,
447
+ 123,
448
+ );
449
+
450
+ const body = JSON.parse(response.body);
451
+ expect(body.id).toBe(123);
452
+ });
453
+
454
+ it("should include required scopes in response body data", () => {
455
+ const middleware = new AuthenticationMiddleware({
456
+ oauth: {
457
+ protectedResource: {
458
+ resource: "https://example.com",
459
+ },
460
+ },
461
+ });
462
+ const response = middleware.getScopeChallengeResponse(["read", "write"]);
463
+
464
+ const body = JSON.parse(response.body);
465
+ expect(body.error.code).toBe(-32001);
466
+ expect(body.error.message).toBe("Insufficient scope");
467
+ expect(body.error.data.error).toBe("insufficient_scope");
468
+ expect(body.error.data.required_scopes).toEqual(["read", "write"]);
469
+ });
470
+
471
+ it("should use custom error_description in response body message", () => {
472
+ const middleware = new AuthenticationMiddleware({
473
+ oauth: {
474
+ protectedResource: {
475
+ resource: "https://example.com",
476
+ },
477
+ },
478
+ });
479
+ const response = middleware.getScopeChallengeResponse(
480
+ ["admin"],
481
+ "Admin privileges required",
482
+ );
483
+
484
+ const body = JSON.parse(response.body);
485
+ expect(body.error.message).toBe("Admin privileges required");
486
+ });
487
+
488
+ it("should handle single scope", () => {
489
+ const middleware = new AuthenticationMiddleware({
490
+ oauth: {
491
+ protectedResource: {
492
+ resource: "https://example.com",
493
+ },
494
+ },
495
+ });
496
+ const response = middleware.getScopeChallengeResponse(["admin"]);
497
+
498
+ expect(response.headers["WWW-Authenticate"]).toContain('scope="admin"');
499
+ const body = JSON.parse(response.body);
500
+ expect(body.error.data.required_scopes).toEqual(["admin"]);
501
+ });
502
+
503
+ it("should handle multiple scopes", () => {
504
+ const middleware = new AuthenticationMiddleware({
505
+ oauth: {
506
+ protectedResource: {
507
+ resource: "https://example.com",
508
+ },
509
+ },
510
+ });
511
+ const response = middleware.getScopeChallengeResponse([
512
+ "read",
513
+ "write",
514
+ "admin",
515
+ ]);
516
+
517
+ expect(response.headers["WWW-Authenticate"]).toContain(
518
+ 'scope="read write admin"',
519
+ );
520
+ const body = JSON.parse(response.body);
521
+ expect(body.error.data.required_scopes).toEqual([
522
+ "read",
523
+ "write",
524
+ "admin",
525
+ ]);
526
+ });
527
+
528
+ it("should not include WWW-Authenticate header without OAuth config", () => {
529
+ const middleware = new AuthenticationMiddleware({});
530
+ const response = middleware.getScopeChallengeResponse(["admin"]);
531
+
532
+ expect(response.headers["WWW-Authenticate"]).toBeUndefined();
533
+ expect(response.headers["Content-Type"]).toBe("application/json");
534
+ });
535
+
536
+ it("should return proper JSON-RPC 2.0 format", () => {
537
+ const middleware = new AuthenticationMiddleware({
538
+ oauth: {
539
+ protectedResource: {
540
+ resource: "https://example.com",
541
+ },
542
+ },
543
+ });
544
+ const response = middleware.getScopeChallengeResponse(
545
+ ["read"],
546
+ "Description",
547
+ "req-123",
548
+ );
549
+
550
+ const body = JSON.parse(response.body);
551
+ expect(body.jsonrpc).toBe("2.0");
552
+ expect(body.id).toBe("req-123");
553
+ expect(body.error).toBeDefined();
554
+ expect(body.error.code).toBe(-32001);
555
+ expect(body.error.message).toBe("Description");
556
+ expect(body.error.data).toBeDefined();
557
+ });
558
+
559
+ it("should include Content-Type header", () => {
560
+ const middleware = new AuthenticationMiddleware({
561
+ oauth: {
562
+ protectedResource: {
563
+ resource: "https://example.com",
564
+ },
565
+ },
566
+ });
567
+ const response = middleware.getScopeChallengeResponse(["read"]);
568
+
569
+ expect(response.headers["Content-Type"]).toBe("application/json");
570
+ });
571
+
572
+ it("should handle empty scopes array", () => {
573
+ const middleware = new AuthenticationMiddleware({
574
+ oauth: {
575
+ protectedResource: {
576
+ resource: "https://example.com",
577
+ },
578
+ },
579
+ });
580
+ const response = middleware.getScopeChallengeResponse([]);
581
+
582
+ expect(response.headers["WWW-Authenticate"]).toContain('scope=""');
583
+ const body = JSON.parse(response.body);
584
+ expect(body.error.data.required_scopes).toEqual([]);
328
585
  });
329
586
  });
330
- });
587
+ });