@loop_ouroboros/mcp-hub-lite 1.2.5 → 1.2.7

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.
Files changed (107) hide show
  1. package/CHANGELOG.md +659 -626
  2. package/dist/client/assets/HomeView-CgEri1kD.js +1 -0
  3. package/dist/client/assets/{ResourceDetailView-BGBtmsyc.js → ResourceDetailView-B8Qo1_jK.js} +1 -1
  4. package/dist/client/assets/ResourcesView-B12FzUdo.js +1 -0
  5. package/dist/client/assets/ServerDashboard-B3O-crvv.js +1 -0
  6. package/dist/client/assets/ServerDetail-Bz5_9yOY.js +2 -0
  7. package/dist/client/assets/{ServerDetail-CtnNKJGx.css → ServerDetail-CXg8rI3q.css} +1 -1
  8. package/dist/client/assets/{ServerListView-C7kcd4GC.js → ServerListView-SlZN8ppC.js} +2 -2
  9. package/dist/client/assets/{ServerStatusTags.vue_vue_type_script_setup_true_lang-BHhwEuGe.js → ServerStatusTags.vue_vue_type_script_setup_true_lang-DmGg4uuV.js} +1 -1
  10. package/dist/client/assets/SettingsView-D8fiOG0O.js +1 -0
  11. package/dist/client/assets/ToolCallDialog-DYEdhnCk.js +1 -0
  12. package/dist/client/assets/ToolsView-BreAl-yn.js +1 -0
  13. package/dist/client/assets/{_baseClone-kbJbcBJT.js → _baseClone-BYxCbA_9.js} +1 -1
  14. package/dist/client/assets/{el-form-item-iQ0G8e97.js → el-form-item-ySymCPMr.js} +2 -2
  15. package/dist/client/assets/el-input-C85p6Nqj.js +1 -0
  16. package/dist/client/assets/el-loading-DIjKEx81.js +1 -0
  17. package/dist/client/assets/{el-overlay-Cy5xg31y.js → el-overlay-B_CxiSem.js} +1 -1
  18. package/dist/client/assets/el-radio-group-BjkTCPRf.js +1 -0
  19. package/dist/client/assets/el-skeleton-item-CupTKK6n.js +1 -0
  20. package/dist/client/assets/{el-switch-KpjV93lm.js → el-switch-BosIJ9jf.js} +1 -1
  21. package/dist/client/assets/el-tab-pane-BydxdJoc.js +1 -0
  22. package/dist/client/assets/{el-table-column-fofd_2n-.js → el-table-column-DV5TZOUW.js} +1 -1
  23. package/dist/client/assets/index-kC4mf0Vo.js +2 -0
  24. package/dist/client/assets/{index-DpH6ZSbs.css → index-xJkq2euk.css} +1 -1
  25. package/dist/client/assets/omit-DxDGRttI.js +1 -0
  26. package/dist/client/assets/{raf-MWAHt9ca.js → raf-Y9AoxecD.js} +1 -1
  27. package/dist/client/assets/{vue-vendor-CbgVSHIh.js → vue-vendor-Dwcr0jep.js} +1 -1
  28. package/dist/client/index.html +3 -3
  29. package/dist/server/shared/types/websocket.types.d.ts +28 -19
  30. package/dist/server/shared/types/websocket.types.d.ts.map +1 -1
  31. package/dist/server/shared/types/websocket.types.js +3 -2
  32. package/dist/server/src/api/ws/ws-handler.d.ts.map +1 -1
  33. package/dist/server/src/api/ws/ws-handler.js +4 -3
  34. package/dist/server/src/app.d.ts.map +1 -1
  35. package/dist/server/src/app.js +46 -0
  36. package/dist/server/src/models/event.model.d.ts +0 -13
  37. package/dist/server/src/models/event.model.d.ts.map +1 -1
  38. package/dist/server/src/models/server.model.d.ts +0 -29
  39. package/dist/server/src/models/server.model.d.ts.map +1 -1
  40. package/dist/server/src/models/types.d.ts +1 -70
  41. package/dist/server/src/models/types.d.ts.map +1 -1
  42. package/dist/server/src/models/types.js +1 -67
  43. package/dist/server/src/server/dev-server.js +24 -6
  44. package/dist/server/src/services/connection/connection-manager.d.ts +19 -0
  45. package/dist/server/src/services/connection/connection-manager.d.ts.map +1 -1
  46. package/dist/server/src/services/connection/connection-manager.js +145 -5
  47. package/dist/server/src/services/event-bus.service.d.ts +0 -4
  48. package/dist/server/src/services/event-bus.service.d.ts.map +1 -1
  49. package/dist/server/src/services/event-bus.service.js +1 -6
  50. package/dist/server/src/services/gateway/request-handlers/initialize-handler.d.ts.map +1 -1
  51. package/dist/server/src/services/gateway/request-handlers/initialize-handler.js +1 -0
  52. package/dist/server/src/services/hub-tools/instance-selector.d.ts +8 -1
  53. package/dist/server/src/services/hub-tools/instance-selector.d.ts.map +1 -1
  54. package/dist/server/src/services/hub-tools/instance-selector.js +24 -10
  55. package/dist/server/src/services/hub-tools/resource-generator.d.ts.map +1 -1
  56. package/dist/server/src/services/hub-tools/resource-generator.js +4 -19
  57. package/dist/server/src/services/hub-tools.service.d.ts.map +1 -1
  58. package/dist/server/src/services/hub-tools.service.js +22 -5
  59. package/dist/server/src/services/mcp-oauth/index.d.ts +5 -0
  60. package/dist/server/src/services/mcp-oauth/index.d.ts.map +1 -0
  61. package/dist/server/src/services/mcp-oauth/index.js +3 -0
  62. package/dist/server/src/services/mcp-oauth/oauth-callback-server.d.ts +19 -0
  63. package/dist/server/src/services/mcp-oauth/oauth-callback-server.d.ts.map +1 -0
  64. package/dist/server/src/services/mcp-oauth/oauth-callback-server.js +100 -0
  65. package/dist/server/src/services/mcp-oauth/oauth-provider.d.ts +42 -0
  66. package/dist/server/src/services/mcp-oauth/oauth-provider.d.ts.map +1 -0
  67. package/dist/server/src/services/mcp-oauth/oauth-provider.js +121 -0
  68. package/dist/server/src/services/mcp-oauth/oauth-token-storage.d.ts +23 -0
  69. package/dist/server/src/services/mcp-oauth/oauth-token-storage.d.ts.map +1 -0
  70. package/dist/server/src/services/mcp-oauth/oauth-token-storage.js +92 -0
  71. package/dist/server/src/services/mcp-oauth/oauth-types.d.ts +21 -0
  72. package/dist/server/src/services/mcp-oauth/oauth-types.d.ts.map +1 -0
  73. package/dist/server/src/services/mcp-oauth/oauth-types.js +4 -0
  74. package/dist/server/src/utils/network-security.d.ts +33 -0
  75. package/dist/server/src/utils/network-security.d.ts.map +1 -0
  76. package/dist/server/src/utils/network-security.js +83 -0
  77. package/dist/server/src/utils/transports/streamable-http-transport.d.ts +12 -1
  78. package/dist/server/src/utils/transports/streamable-http-transport.d.ts.map +1 -1
  79. package/dist/server/src/utils/transports/streamable-http-transport.js +20 -1
  80. package/dist/server/src/utils/transports/transport-factory.d.ts +2 -0
  81. package/dist/server/src/utils/transports/transport-factory.d.ts.map +1 -1
  82. package/dist/server/src/utils/transports/transport-factory.js +17 -2
  83. package/dist/server/src/utils/transports/transport.interface.d.ts +2 -0
  84. package/dist/server/src/utils/transports/transport.interface.d.ts.map +1 -1
  85. package/dist/server/tests/unit/services/hub-tools/instance-selector.test.js +21 -16
  86. package/dist/server/tests/unit/services/hub-tools.service.test.js +36 -35
  87. package/dist/server/tests/unit/utils/network-security.test.d.ts +2 -0
  88. package/dist/server/tests/unit/utils/network-security.test.d.ts.map +1 -0
  89. package/dist/server/tests/unit/utils/network-security.test.js +123 -0
  90. package/dist/server/vite.config.js +1 -1
  91. package/package.json +111 -111
  92. package/dist/client/assets/HomeView-BnO4yIT9.js +0 -1
  93. package/dist/client/assets/ResourcesView-B5Xg0ynh.js +0 -1
  94. package/dist/client/assets/ServerDashboard-DYAVrrUk.js +0 -1
  95. package/dist/client/assets/ServerDetail-q94ZFfjL.js +0 -2
  96. package/dist/client/assets/SettingsView-BM6P5yrT.js +0 -1
  97. package/dist/client/assets/ToolCallDialog-BoAGxlB5.js +0 -1
  98. package/dist/client/assets/ToolsView-lqFhr7Bk.js +0 -1
  99. package/dist/client/assets/el-input-DkJq57wP.js +0 -1
  100. package/dist/client/assets/el-loading-C3v6a9xV.js +0 -1
  101. package/dist/client/assets/el-radio-group-C9QUL5mm.js +0 -1
  102. package/dist/client/assets/el-skeleton-item-Bbmpc0Xz.js +0 -1
  103. package/dist/client/assets/el-tab-pane-YsYuBcem.js +0 -1
  104. package/dist/client/assets/index-5tzIwwtS.js +0 -1
  105. package/dist/client/assets/index-MqHvQjDP.js +0 -2
  106. package/dist/client/assets/omit-CB4hTeTH.js +0 -1
  107. package/dist/client/assets/typescript-Bp3YSIOJ.js +0 -1
@@ -17,8 +17,13 @@ describe('InstanceSelector', () => {
17
17
  headers: {},
18
18
  tags: {}
19
19
  };
20
+ // Mock statusChecker that simulates connected instances
21
+ const mockConnectedStatus = () => ({ connected: true });
22
+ const mockDisconnectedStatus = () => ({ connected: false });
20
23
  describe('random strategy', () => {
21
- it('should select random instance from enabled instances', () => {
24
+ it('should select random instance from connected instances', () => {
25
+ // Local mock that simulates idx=2 being disconnected
26
+ const localMockConnectedStatus = (_name, idx) => idx === 2 ? { connected: false } : { connected: true };
22
27
  const config = {
23
28
  template: {
24
29
  ...baseTemplate,
@@ -31,11 +36,11 @@ describe('InstanceSelector', () => {
31
36
  ],
32
37
  tagDefinitions: []
33
38
  };
34
- const selected = InstanceSelector.selectInstance('test-server', config);
39
+ const selected = InstanceSelector.selectInstance('test-server', config, undefined, localMockConnectedStatus);
35
40
  expect(selected).toBeDefined();
36
41
  expect(['1', '2']).toContain(selected.id);
37
42
  });
38
- it('should return undefined when no enabled instances', () => {
43
+ it('should return undefined when no connected instances', () => {
39
44
  const config = {
40
45
  template: {
41
46
  ...baseTemplate,
@@ -47,12 +52,12 @@ describe('InstanceSelector', () => {
47
52
  ],
48
53
  tagDefinitions: []
49
54
  };
50
- const selected = InstanceSelector.selectInstance('test-server', config);
55
+ const selected = InstanceSelector.selectInstance('test-server', config, undefined, mockDisconnectedStatus);
51
56
  expect(selected).toBeUndefined();
52
57
  });
53
58
  });
54
59
  describe('round-robin strategy', () => {
55
- it('should cycle through enabled instances', () => {
60
+ it('should cycle through connected instances', () => {
56
61
  const config = {
57
62
  template: {
58
63
  ...baseTemplate,
@@ -65,10 +70,10 @@ describe('InstanceSelector', () => {
65
70
  ],
66
71
  tagDefinitions: []
67
72
  };
68
- const selected1 = InstanceSelector.selectInstance('test-server-rr', config);
69
- const selected2 = InstanceSelector.selectInstance('test-server-rr', config);
70
- const selected3 = InstanceSelector.selectInstance('test-server-rr', config);
71
- const selected4 = InstanceSelector.selectInstance('test-server-rr', config);
73
+ const selected1 = InstanceSelector.selectInstance('test-server-rr', config, undefined, mockConnectedStatus);
74
+ const selected2 = InstanceSelector.selectInstance('test-server-rr', config, undefined, mockConnectedStatus);
75
+ const selected3 = InstanceSelector.selectInstance('test-server-rr', config, undefined, mockConnectedStatus);
76
+ const selected4 = InstanceSelector.selectInstance('test-server-rr', config, undefined, mockConnectedStatus);
72
77
  expect(selected1.id).toBe('1');
73
78
  expect(selected2.id).toBe('2');
74
79
  expect(selected3.id).toBe('3');
@@ -91,7 +96,7 @@ describe('InstanceSelector', () => {
91
96
  };
92
97
  const selected = InstanceSelector.selectInstance('test-server', config, {
93
98
  tags: { env: 'prod', region: 'us' }
94
- });
99
+ }, mockConnectedStatus);
95
100
  expect(selected).toBeDefined();
96
101
  expect(selected.id).toBe('2');
97
102
  });
@@ -110,7 +115,7 @@ describe('InstanceSelector', () => {
110
115
  expect(() => {
111
116
  InstanceSelector.selectInstance('test-server', config, {
112
117
  tags: { env: 'staging' }
113
- });
118
+ }, mockConnectedStatus);
114
119
  }).toThrow('No instance found matching tags');
115
120
  });
116
121
  it('should throw error when multiple instances match tags', () => {
@@ -128,7 +133,7 @@ describe('InstanceSelector', () => {
128
133
  expect(() => {
129
134
  InstanceSelector.selectInstance('test-server', config, {
130
135
  tags: { env: 'prod' }
131
- });
136
+ }, mockConnectedStatus);
132
137
  }).toThrow('Multiple instances match tags');
133
138
  });
134
139
  it('should return the single instance when no tags provided and only one instance exists', () => {
@@ -140,7 +145,7 @@ describe('InstanceSelector', () => {
140
145
  instances: [{ ...baseInstance, id: '1', index: 0 }],
141
146
  tagDefinitions: []
142
147
  };
143
- const selected = InstanceSelector.selectInstance('test-server', config);
148
+ const selected = InstanceSelector.selectInstance('test-server', config, undefined, mockConnectedStatus);
144
149
  expect(selected).toBeDefined();
145
150
  expect(selected.id).toBe('1');
146
151
  });
@@ -157,7 +162,7 @@ describe('InstanceSelector', () => {
157
162
  tagDefinitions: []
158
163
  };
159
164
  expect(() => {
160
- InstanceSelector.selectInstance('test-server', config);
165
+ InstanceSelector.selectInstance('test-server', config, undefined, mockConnectedStatus);
161
166
  }).toThrow('No tags provided for tag-match-unique strategy with 2 instances. Available: [0:{}, 1:{}]. Pass matching tags to select.');
162
167
  });
163
168
  });
@@ -171,7 +176,7 @@ describe('InstanceSelector', () => {
171
176
  instances: [{ ...baseInstance, id: '1', index: 0 }],
172
177
  tagDefinitions: []
173
178
  };
174
- const selected = InstanceSelector.selectInstance('test-server', config);
179
+ const selected = InstanceSelector.selectInstance('test-server', config, undefined, mockConnectedStatus);
175
180
  expect(selected).toBeDefined();
176
181
  expect(selected.id).toBe('1');
177
182
  });
@@ -187,7 +192,7 @@ describe('InstanceSelector', () => {
187
192
  tagDefinitions: []
188
193
  // No instanceSelectionStrategy field
189
194
  };
190
- const selected = InstanceSelector.selectInstance('test-server', config);
195
+ const selected = InstanceSelector.selectInstance('test-server', config, undefined, mockConnectedStatus);
191
196
  expect(selected).toBeDefined();
192
197
  expect(['1', '2']).toContain(selected.id);
193
198
  });
@@ -36,6 +36,7 @@ describe('HubToolsService', () => {
36
36
  instances: [
37
37
  {
38
38
  id: 'test-server-1-instance',
39
+ index: 0,
39
40
  enabled: true,
40
41
  args: [],
41
42
  env: {},
@@ -62,6 +63,7 @@ describe('HubToolsService', () => {
62
63
  instances: [
63
64
  {
64
65
  id: 'test-server-2-instance',
66
+ index: 0,
65
67
  enabled: true,
66
68
  args: [],
67
69
  env: {},
@@ -82,13 +84,11 @@ describe('HubToolsService', () => {
82
84
  const server = mockServers.find((s) => s.name === name);
83
85
  return server?.config;
84
86
  });
85
- vi.mocked(mcpConnectionManager.getStatusByName).mockImplementation(() => {
86
- return {
87
- connected: true,
88
- lastCheck: Date.now(),
89
- toolsCount: 0,
90
- resourcesCount: 0
91
- };
87
+ vi.mocked(mcpConnectionManager.getConnectedIndexes).mockImplementation(() => {
88
+ return [0];
89
+ });
90
+ vi.mocked(mcpConnectionManager.getStatus).mockImplementation(() => {
91
+ return { connected: true, lastCheck: Date.now(), toolsCount: 0, resourcesCount: 0 };
92
92
  });
93
93
  // Act
94
94
  const servers = await hubToolsService.listServers();
@@ -118,6 +118,7 @@ describe('HubToolsService', () => {
118
118
  instances: [
119
119
  {
120
120
  id: 'server1-instance',
121
+ index: 0,
121
122
  enabled: true,
122
123
  args: [],
123
124
  env: {},
@@ -138,13 +139,11 @@ describe('HubToolsService', () => {
138
139
  const server = mockServers.find((s) => s.name === name);
139
140
  return server?.config;
140
141
  });
141
- vi.mocked(mcpConnectionManager.getStatusByName).mockImplementation(() => {
142
- return {
143
- connected: true,
144
- lastCheck: Date.now(),
145
- toolsCount: 0,
146
- resourcesCount: 0
147
- };
142
+ vi.mocked(mcpConnectionManager.getConnectedIndexes).mockImplementation(() => {
143
+ return [0];
144
+ });
145
+ vi.mocked(mcpConnectionManager.getStatus).mockImplementation(() => {
146
+ return { connected: true, lastCheck: Date.now(), toolsCount: 0, resourcesCount: 0 };
148
147
  });
149
148
  // Act
150
149
  const servers = await hubToolsService.listServers();
@@ -173,6 +172,7 @@ describe('HubToolsService', () => {
173
172
  instances: [
174
173
  {
175
174
  id: 'filesystem-instance',
175
+ index: 0,
176
176
  enabled: true,
177
177
  args: [],
178
178
  env: {},
@@ -200,6 +200,7 @@ describe('HubToolsService', () => {
200
200
  instances: [
201
201
  {
202
202
  id: 'time-instance',
203
+ index: 0,
203
204
  enabled: true,
204
205
  args: [],
205
206
  env: {},
@@ -220,13 +221,11 @@ describe('HubToolsService', () => {
220
221
  const server = mockServers.find((s) => s.name === name);
221
222
  return server?.config;
222
223
  });
223
- vi.mocked(mcpConnectionManager.getStatusByName).mockImplementation(() => {
224
- return {
225
- connected: true,
226
- lastCheck: Date.now(),
227
- toolsCount: 0,
228
- resourcesCount: 0
229
- };
224
+ vi.mocked(mcpConnectionManager.getConnectedIndexes).mockImplementation(() => {
225
+ return [0];
226
+ });
227
+ vi.mocked(mcpConnectionManager.getStatus).mockImplementation(() => {
228
+ return { connected: true, lastCheck: Date.now(), toolsCount: 0, resourcesCount: 0 };
230
229
  });
231
230
  // Act
232
231
  const servers = await hubToolsService.listServers();
@@ -256,6 +255,7 @@ describe('HubToolsService', () => {
256
255
  instances: [
257
256
  {
258
257
  id: 'connected-server-instance',
258
+ index: 0,
259
259
  enabled: true,
260
260
  args: [],
261
261
  env: {},
@@ -303,21 +303,14 @@ describe('HubToolsService', () => {
303
303
  const server = mockServers.find((s) => s.name === name);
304
304
  return server?.config;
305
305
  });
306
- vi.mocked(mcpConnectionManager.getStatusByName).mockImplementation((name) => {
306
+ vi.mocked(mcpConnectionManager.getConnectedIndexes).mockImplementation((name) => {
307
307
  if (name === 'Connected Server') {
308
- return {
309
- connected: true,
310
- lastCheck: Date.now(),
311
- toolsCount: 0,
312
- resourcesCount: 0
313
- };
308
+ return [0];
314
309
  }
315
- return {
316
- connected: false,
317
- lastCheck: Date.now(),
318
- toolsCount: 0,
319
- resourcesCount: 0
320
- };
310
+ return [];
311
+ });
312
+ vi.mocked(mcpConnectionManager.getStatus).mockImplementation(() => {
313
+ return { connected: true, lastCheck: Date.now(), toolsCount: 0, resourcesCount: 0 };
321
314
  });
322
315
  // Act
323
316
  const servers = await hubToolsService.listServers();
@@ -380,6 +373,7 @@ describe('HubToolsService', () => {
380
373
  tagDefinitions: []
381
374
  });
382
375
  vi.mocked(mcpConnectionManager.getToolsByServerName).mockReturnValue(mockTools);
376
+ vi.mocked(mcpConnectionManager.getConnectedIndexes).mockReturnValue([0]);
383
377
  // Act
384
378
  const result = await hubToolsService.listToolsInServer({ serverName });
385
379
  // Assert
@@ -392,7 +386,7 @@ describe('HubToolsService', () => {
392
386
  it('should throw error if server not found', async () => {
393
387
  // Arrange
394
388
  const serverName = 'Non-existent Server';
395
- vi.mocked(mcpConnectionManager.getToolsByServerName).mockReturnValue([]);
389
+ vi.mocked(mcpConnectionManager.getConnectedIndexes).mockReturnValue([]);
396
390
  // Act & Assert
397
391
  await expect(hubToolsService.listToolsInServer({ serverName })).rejects.toThrow(`Server not found: ${serverName}`);
398
392
  });
@@ -440,6 +434,7 @@ describe('HubToolsService', () => {
440
434
  tagDefinitions: []
441
435
  });
442
436
  vi.mocked(mcpConnectionManager.getToolsByServerName).mockReturnValue(mockTools);
437
+ vi.mocked(mcpConnectionManager.getConnectedIndexes).mockReturnValue([0]);
443
438
  // Act
444
439
  const tool = await hubToolsService.getTool({ serverName, toolName });
445
440
  // Assert
@@ -480,6 +475,8 @@ describe('HubToolsService', () => {
480
475
  instances: [mockInstance],
481
476
  tagDefinitions: []
482
477
  });
478
+ vi.mocked(mcpConnectionManager.getToolsByServerName).mockReturnValue(mockTools);
479
+ vi.mocked(mcpConnectionManager.getConnectedIndexes).mockReturnValue([0]);
483
480
  vi.mocked(mcpConnectionManager.getTools).mockReturnValue(mockTools);
484
481
  // Act
485
482
  const tool = await hubToolsService.getTool({ serverName, toolName });
@@ -681,6 +678,7 @@ describe('HubToolsService', () => {
681
678
  vi.mocked(mcpConnectionManager.getResources).mockReturnValue([
682
679
  { uri: 'test://resource', name: 'Test Resource' }
683
680
  ]);
681
+ vi.mocked(mcpConnectionManager.getConnectedIndexes).mockReturnValue([0]);
684
682
  // Act
685
683
  const resources = await hubToolsService.listResources();
686
684
  // Assert - the resource list should include use-guide and at least the server resource
@@ -747,6 +745,7 @@ describe('HubToolsService', () => {
747
745
  instances: [
748
746
  {
749
747
  id: 'test-instance',
748
+ index: 0,
750
749
  enabled: true,
751
750
  args: [],
752
751
  env: {},
@@ -792,6 +791,7 @@ describe('HubToolsService', () => {
792
791
  const serverName = 'Test Server';
793
792
  const mockInstance = {
794
793
  id: '1',
794
+ index: 0,
795
795
  enabled: true,
796
796
  args: [],
797
797
  env: {},
@@ -817,6 +817,7 @@ describe('HubToolsService', () => {
817
817
  const serverName = 'Test Server';
818
818
  const mockInstance = {
819
819
  id: '1',
820
+ index: 0,
820
821
  enabled: true,
821
822
  args: [],
822
823
  env: {},
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=network-security.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"network-security.test.d.ts","sourceRoot":"","sources":["../../../../../tests/unit/utils/network-security.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,123 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { ip4ToInt, normalizeIp, parseCidr, isIpAllowed } from '../../../src/utils/network-security.js';
3
+ describe('network-security', () => {
4
+ describe('ip4ToInt', () => {
5
+ it('should convert standard IPv4 to 32-bit integer', () => {
6
+ expect(ip4ToInt('192.168.1.1')).toBe(0xc0a80101);
7
+ expect(ip4ToInt('0.0.0.0')).toBe(0);
8
+ expect(ip4ToInt('255.255.255.255')).toBe(0xffffffff >>> 0);
9
+ expect(ip4ToInt('127.0.0.1')).toBe(0x7f000001);
10
+ expect(ip4ToInt('10.0.0.0')).toBe(0x0a000000);
11
+ });
12
+ it('should return NaN for invalid format', () => {
13
+ expect(ip4ToInt('')).toBeNaN();
14
+ expect(ip4ToInt('abc')).toBeNaN();
15
+ expect(ip4ToInt('1.2.3.4.5')).toBeNaN();
16
+ expect(ip4ToInt('1.2.3')).toBeNaN();
17
+ });
18
+ it('should return NaN for out-of-range octets', () => {
19
+ expect(ip4ToInt('256.1.1.1')).toBeNaN();
20
+ expect(ip4ToInt('1.1.1.-1')).toBeNaN();
21
+ expect(ip4ToInt('1.1.1.999')).toBeNaN();
22
+ });
23
+ it('should return NaN for non-numeric octets', () => {
24
+ expect(ip4ToInt('1.2.3.abc')).toBeNaN();
25
+ expect(ip4ToInt('1.2.3.1.1')).toBeNaN();
26
+ });
27
+ });
28
+ describe('normalizeIp', () => {
29
+ it('should return IPv4 unchanged', () => {
30
+ expect(normalizeIp('192.168.1.1')).toBe('192.168.1.1');
31
+ expect(normalizeIp('127.0.0.1')).toBe('127.0.0.1');
32
+ });
33
+ it('should strip ::ffff: prefix from IPv4-mapped IPv6', () => {
34
+ expect(normalizeIp('::ffff:192.168.1.1')).toBe('192.168.1.1');
35
+ expect(normalizeIp('::ffff:127.0.0.1')).toBe('127.0.0.1');
36
+ expect(normalizeIp('::ffff:10.0.0.1')).toBe('10.0.0.1');
37
+ });
38
+ it('should map ::1 to 127.0.0.1', () => {
39
+ expect(normalizeIp('::1')).toBe('127.0.0.1');
40
+ });
41
+ it('should return bare IPv6 unchanged', () => {
42
+ expect(normalizeIp('fe80::1')).toBe('fe80::1');
43
+ expect(normalizeIp('::1:2:3:4')).toBe('::1:2:3:4');
44
+ });
45
+ it('should handle empty/falsy values', () => {
46
+ expect(normalizeIp('')).toBe('');
47
+ });
48
+ });
49
+ describe('parseCidr', () => {
50
+ it('should parse standard CIDR notation', () => {
51
+ const result = parseCidr('192.168.1.0/24');
52
+ expect(result).not.toBeNull();
53
+ expect(result.network).toBe(0xc0a80100);
54
+ expect(result.mask).toBe(0xffffff00);
55
+ });
56
+ it('should handle /32 prefix (single host)', () => {
57
+ const result = parseCidr('10.0.0.1/32');
58
+ expect(result).not.toBeNull();
59
+ expect(result.network).toBe(0x0a000001);
60
+ expect(result.mask).toBe(0xffffffff);
61
+ });
62
+ it('should handle /0 prefix (match all)', () => {
63
+ const result = parseCidr('0.0.0.0/0');
64
+ expect(result).not.toBeNull();
65
+ expect(result.mask).toBe(0);
66
+ });
67
+ it('should assume /32 when no prefix given', () => {
68
+ const result = parseCidr('192.168.1.1');
69
+ expect(result).not.toBeNull();
70
+ expect(result.network).toBe(0xc0a80101);
71
+ expect(result.mask).toBe(0xffffffff);
72
+ });
73
+ it('should return null for invalid prefix', () => {
74
+ expect(parseCidr('192.168.1.0/33')).toBeNull();
75
+ expect(parseCidr('192.168.1.0/-1')).toBeNull();
76
+ expect(parseCidr('192.168.1.0/abc')).toBeNull();
77
+ });
78
+ it('should return null for invalid IP in CIDR', () => {
79
+ expect(parseCidr('256.1.1.0/24')).toBeNull();
80
+ expect(parseCidr('abc/24')).toBeNull();
81
+ });
82
+ it('should return null for empty or null input', () => {
83
+ expect(parseCidr('')).toBeNull();
84
+ });
85
+ });
86
+ describe('isIpAllowed', () => {
87
+ const defaults = ['127.0.0.1', '192.168.0.0/16', '10.0.0.0/8'];
88
+ it('should allow IP matching a CIDR', () => {
89
+ expect(isIpAllowed('192.168.1.1', defaults)).toBe(true);
90
+ expect(isIpAllowed('10.0.0.1', defaults)).toBe(true);
91
+ expect(isIpAllowed('10.255.255.255', defaults)).toBe(true);
92
+ });
93
+ it('should reject IP not matching any CIDR', () => {
94
+ expect(isIpAllowed('1.2.3.4', defaults)).toBe(false);
95
+ expect(isIpAllowed('8.8.8.8', defaults)).toBe(false);
96
+ });
97
+ it('should allow all when list is empty', () => {
98
+ expect(isIpAllowed('1.2.3.4', [])).toBe(true);
99
+ expect(isIpAllowed('8.8.8.8', [])).toBe(true);
100
+ });
101
+ it('should reject invalid IPs (safety default)', () => {
102
+ expect(isIpAllowed('', defaults)).toBe(false);
103
+ expect(isIpAllowed('invalid', defaults)).toBe(false);
104
+ expect(isIpAllowed('256.1.1.1', defaults)).toBe(false);
105
+ });
106
+ it('should match IPv4-mapped IPv6 addresses', () => {
107
+ expect(isIpAllowed('::ffff:192.168.1.1', defaults)).toBe(true);
108
+ expect(isIpAllowed('::ffff:10.0.0.1', defaults)).toBe(true);
109
+ expect(isIpAllowed('::ffff:1.2.3.4', defaults)).toBe(false);
110
+ });
111
+ it('should match ::1 as localhost', () => {
112
+ expect(isIpAllowed('::1', defaults)).toBe(true);
113
+ });
114
+ it('should skip invalid CIDR entries silently', () => {
115
+ expect(isIpAllowed('192.168.1.1', ['invalid/24', '192.168.0.0/16'])).toBe(true);
116
+ expect(isIpAllowed('1.2.3.4', ['invalid/24'])).toBe(false);
117
+ });
118
+ it('should handle /32 exact match', () => {
119
+ expect(isIpAllowed('192.168.1.1', ['192.168.1.1/32'])).toBe(true);
120
+ expect(isIpAllowed('192.168.1.2', ['192.168.1.1/32'])).toBe(false);
121
+ });
122
+ });
123
+ });
@@ -118,7 +118,7 @@ export default defineConfig({
118
118
  server: {
119
119
  host: '127.0.0.1', // Explicitly use IPv4 address
120
120
  port: 5173, // Use common port number to avoid permission issues
121
- strictPort: true, // Allow automatic selection of available port
121
+ strictPort: false, // Auto-increment port if occupied
122
122
  proxy: {
123
123
  '/api': {
124
124
  target: `http://${backendHost}:${backendPort}`,