@vinkius-core/mcp-fusion 0.1.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.
Files changed (211) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +391 -0
  3. package/dist/src/AbstractBase.d.ts +24 -0
  4. package/dist/src/AbstractBase.d.ts.map +1 -0
  5. package/dist/src/AbstractBase.js +63 -0
  6. package/dist/src/AbstractBase.js.map +1 -0
  7. package/dist/src/AbstractLeaf.d.ts +12 -0
  8. package/dist/src/AbstractLeaf.d.ts.map +1 -0
  9. package/dist/src/AbstractLeaf.js +32 -0
  10. package/dist/src/AbstractLeaf.js.map +1 -0
  11. package/dist/src/Annotations.d.ts +15 -0
  12. package/dist/src/Annotations.d.ts.map +1 -0
  13. package/dist/src/Annotations.js +29 -0
  14. package/dist/src/Annotations.js.map +1 -0
  15. package/dist/src/Group.d.ts +32 -0
  16. package/dist/src/Group.d.ts.map +1 -0
  17. package/dist/src/Group.js +131 -0
  18. package/dist/src/Group.js.map +1 -0
  19. package/dist/src/Icon.d.ts +19 -0
  20. package/dist/src/Icon.d.ts.map +1 -0
  21. package/dist/src/Icon.js +33 -0
  22. package/dist/src/Icon.js.map +1 -0
  23. package/dist/src/Prompt.d.ts +11 -0
  24. package/dist/src/Prompt.d.ts.map +1 -0
  25. package/dist/src/Prompt.js +28 -0
  26. package/dist/src/Prompt.js.map +1 -0
  27. package/dist/src/PromptArgument.d.ts +10 -0
  28. package/dist/src/PromptArgument.d.ts.map +1 -0
  29. package/dist/src/PromptArgument.js +20 -0
  30. package/dist/src/PromptArgument.js.map +1 -0
  31. package/dist/src/Resource.d.ts +19 -0
  32. package/dist/src/Resource.d.ts.map +1 -0
  33. package/dist/src/Resource.js +34 -0
  34. package/dist/src/Resource.js.map +1 -0
  35. package/dist/src/Role.d.ts +5 -0
  36. package/dist/src/Role.d.ts.map +1 -0
  37. package/dist/src/Role.js +6 -0
  38. package/dist/src/Role.js.map +1 -0
  39. package/dist/src/Tool.d.ts +16 -0
  40. package/dist/src/Tool.d.ts.map +1 -0
  41. package/dist/src/Tool.js +28 -0
  42. package/dist/src/Tool.js.map +1 -0
  43. package/dist/src/ToolAnnotations.d.ts +23 -0
  44. package/dist/src/ToolAnnotations.d.ts.map +1 -0
  45. package/dist/src/ToolAnnotations.js +44 -0
  46. package/dist/src/ToolAnnotations.js.map +1 -0
  47. package/dist/src/converters/GroupConverter.d.ts +14 -0
  48. package/dist/src/converters/GroupConverter.d.ts.map +1 -0
  49. package/dist/src/converters/GroupConverter.js +13 -0
  50. package/dist/src/converters/GroupConverter.js.map +1 -0
  51. package/dist/src/converters/PromptConverter.d.ts +14 -0
  52. package/dist/src/converters/PromptConverter.d.ts.map +1 -0
  53. package/dist/src/converters/PromptConverter.js +13 -0
  54. package/dist/src/converters/PromptConverter.js.map +1 -0
  55. package/dist/src/converters/ResourceConverter.d.ts +14 -0
  56. package/dist/src/converters/ResourceConverter.d.ts.map +1 -0
  57. package/dist/src/converters/ResourceConverter.js +13 -0
  58. package/dist/src/converters/ResourceConverter.js.map +1 -0
  59. package/dist/src/converters/ToolAnnotationsConverter.d.ts +16 -0
  60. package/dist/src/converters/ToolAnnotationsConverter.d.ts.map +1 -0
  61. package/dist/src/converters/ToolAnnotationsConverter.js +23 -0
  62. package/dist/src/converters/ToolAnnotationsConverter.js.map +1 -0
  63. package/dist/src/converters/ToolConverter.d.ts +14 -0
  64. package/dist/src/converters/ToolConverter.d.ts.map +1 -0
  65. package/dist/src/converters/ToolConverter.js +13 -0
  66. package/dist/src/converters/ToolConverter.js.map +1 -0
  67. package/dist/src/converters/index.d.ts +6 -0
  68. package/dist/src/converters/index.d.ts.map +1 -0
  69. package/dist/src/converters/index.js +6 -0
  70. package/dist/src/converters/index.js.map +1 -0
  71. package/dist/src/framework/GroupedToolBuilder.d.ts +137 -0
  72. package/dist/src/framework/GroupedToolBuilder.d.ts.map +1 -0
  73. package/dist/src/framework/GroupedToolBuilder.js +289 -0
  74. package/dist/src/framework/GroupedToolBuilder.js.map +1 -0
  75. package/dist/src/framework/ResponseHelper.d.ts +43 -0
  76. package/dist/src/framework/ResponseHelper.d.ts.map +1 -0
  77. package/dist/src/framework/ResponseHelper.js +49 -0
  78. package/dist/src/framework/ResponseHelper.js.map +1 -0
  79. package/dist/src/framework/ToolBuilder.d.ts +46 -0
  80. package/dist/src/framework/ToolBuilder.d.ts.map +1 -0
  81. package/dist/src/framework/ToolBuilder.js +2 -0
  82. package/dist/src/framework/ToolBuilder.js.map +1 -0
  83. package/dist/src/framework/ToolRegistry.d.ts +85 -0
  84. package/dist/src/framework/ToolRegistry.d.ts.map +1 -0
  85. package/dist/src/framework/ToolRegistry.js +153 -0
  86. package/dist/src/framework/ToolRegistry.js.map +1 -0
  87. package/dist/src/framework/index.d.ts +9 -0
  88. package/dist/src/framework/index.d.ts.map +1 -0
  89. package/dist/src/framework/index.js +8 -0
  90. package/dist/src/framework/index.js.map +1 -0
  91. package/dist/src/framework/strategies/AnnotationAggregator.d.ts +11 -0
  92. package/dist/src/framework/strategies/AnnotationAggregator.d.ts.map +1 -0
  93. package/dist/src/framework/strategies/AnnotationAggregator.js +25 -0
  94. package/dist/src/framework/strategies/AnnotationAggregator.js.map +1 -0
  95. package/dist/src/framework/strategies/DescriptionGenerator.d.ts +12 -0
  96. package/dist/src/framework/strategies/DescriptionGenerator.d.ts.map +1 -0
  97. package/dist/src/framework/strategies/DescriptionGenerator.js +70 -0
  98. package/dist/src/framework/strategies/DescriptionGenerator.js.map +1 -0
  99. package/dist/src/framework/strategies/MiddlewareCompiler.d.ts +13 -0
  100. package/dist/src/framework/strategies/MiddlewareCompiler.d.ts.map +1 -0
  101. package/dist/src/framework/strategies/MiddlewareCompiler.js +24 -0
  102. package/dist/src/framework/strategies/MiddlewareCompiler.js.map +1 -0
  103. package/dist/src/framework/strategies/SchemaGenerator.d.ts +15 -0
  104. package/dist/src/framework/strategies/SchemaGenerator.d.ts.map +1 -0
  105. package/dist/src/framework/strategies/SchemaGenerator.js +97 -0
  106. package/dist/src/framework/strategies/SchemaGenerator.js.map +1 -0
  107. package/dist/src/framework/strategies/SchemaUtils.d.ts +7 -0
  108. package/dist/src/framework/strategies/SchemaUtils.d.ts.map +1 -0
  109. package/dist/src/framework/strategies/SchemaUtils.js +17 -0
  110. package/dist/src/framework/strategies/SchemaUtils.js.map +1 -0
  111. package/dist/src/framework/strategies/ToonDescriptionGenerator.d.ts +3 -0
  112. package/dist/src/framework/strategies/ToonDescriptionGenerator.d.ts.map +1 -0
  113. package/dist/src/framework/strategies/ToonDescriptionGenerator.js +53 -0
  114. package/dist/src/framework/strategies/ToonDescriptionGenerator.js.map +1 -0
  115. package/dist/src/framework/strategies/Types.d.ts +34 -0
  116. package/dist/src/framework/strategies/Types.d.ts.map +1 -0
  117. package/dist/src/framework/strategies/Types.js +2 -0
  118. package/dist/src/framework/strategies/Types.js.map +1 -0
  119. package/dist/src/framework/strategies/index.d.ts +12 -0
  120. package/dist/src/framework/strategies/index.d.ts.map +1 -0
  121. package/dist/src/framework/strategies/index.js +11 -0
  122. package/dist/src/framework/strategies/index.js.map +1 -0
  123. package/dist/src/index.d.ts +15 -0
  124. package/dist/src/index.d.ts.map +1 -0
  125. package/dist/src/index.js +15 -0
  126. package/dist/src/index.js.map +1 -0
  127. package/dist/tests/AbstractBase.test.d.ts +2 -0
  128. package/dist/tests/AbstractBase.test.d.ts.map +1 -0
  129. package/dist/tests/AbstractBase.test.js +130 -0
  130. package/dist/tests/AbstractBase.test.js.map +1 -0
  131. package/dist/tests/AbstractLeaf.test.d.ts +2 -0
  132. package/dist/tests/AbstractLeaf.test.d.ts.map +1 -0
  133. package/dist/tests/AbstractLeaf.test.js +65 -0
  134. package/dist/tests/AbstractLeaf.test.js.map +1 -0
  135. package/dist/tests/Annotations.test.d.ts +2 -0
  136. package/dist/tests/Annotations.test.d.ts.map +1 -0
  137. package/dist/tests/Annotations.test.js +34 -0
  138. package/dist/tests/Annotations.test.js.map +1 -0
  139. package/dist/tests/BarrelExport.test.d.ts +2 -0
  140. package/dist/tests/BarrelExport.test.d.ts.map +1 -0
  141. package/dist/tests/BarrelExport.test.js +42 -0
  142. package/dist/tests/BarrelExport.test.js.map +1 -0
  143. package/dist/tests/Converters.test.d.ts +2 -0
  144. package/dist/tests/Converters.test.d.ts.map +1 -0
  145. package/dist/tests/Converters.test.js +193 -0
  146. package/dist/tests/Converters.test.js.map +1 -0
  147. package/dist/tests/Group.test.d.ts +2 -0
  148. package/dist/tests/Group.test.d.ts.map +1 -0
  149. package/dist/tests/Group.test.js +257 -0
  150. package/dist/tests/Group.test.js.map +1 -0
  151. package/dist/tests/Icon.test.d.ts +2 -0
  152. package/dist/tests/Icon.test.d.ts.map +1 -0
  153. package/dist/tests/Icon.test.js +44 -0
  154. package/dist/tests/Icon.test.js.map +1 -0
  155. package/dist/tests/Prompt.test.d.ts +2 -0
  156. package/dist/tests/Prompt.test.d.ts.map +1 -0
  157. package/dist/tests/Prompt.test.js +63 -0
  158. package/dist/tests/Prompt.test.js.map +1 -0
  159. package/dist/tests/PromptArgument.test.d.ts +2 -0
  160. package/dist/tests/PromptArgument.test.d.ts.map +1 -0
  161. package/dist/tests/PromptArgument.test.js +37 -0
  162. package/dist/tests/PromptArgument.test.js.map +1 -0
  163. package/dist/tests/Resource.test.d.ts +2 -0
  164. package/dist/tests/Resource.test.d.ts.map +1 -0
  165. package/dist/tests/Resource.test.js +61 -0
  166. package/dist/tests/Resource.test.js.map +1 -0
  167. package/dist/tests/Role.test.d.ts +2 -0
  168. package/dist/tests/Role.test.d.ts.map +1 -0
  169. package/dist/tests/Role.test.js +17 -0
  170. package/dist/tests/Role.test.js.map +1 -0
  171. package/dist/tests/Tool.test.d.ts +2 -0
  172. package/dist/tests/Tool.test.d.ts.map +1 -0
  173. package/dist/tests/Tool.test.js +62 -0
  174. package/dist/tests/Tool.test.js.map +1 -0
  175. package/dist/tests/ToolAnnotations.test.d.ts +2 -0
  176. package/dist/tests/ToolAnnotations.test.d.ts.map +1 -0
  177. package/dist/tests/ToolAnnotations.test.js +55 -0
  178. package/dist/tests/ToolAnnotations.test.js.map +1 -0
  179. package/dist/tests/framework/AdversarialQA.test.d.ts +2 -0
  180. package/dist/tests/framework/AdversarialQA.test.d.ts.map +1 -0
  181. package/dist/tests/framework/AdversarialQA.test.js +906 -0
  182. package/dist/tests/framework/AdversarialQA.test.js.map +1 -0
  183. package/dist/tests/framework/GroupedToolBuilder.test.d.ts +2 -0
  184. package/dist/tests/framework/GroupedToolBuilder.test.d.ts.map +1 -0
  185. package/dist/tests/framework/GroupedToolBuilder.test.js +712 -0
  186. package/dist/tests/framework/GroupedToolBuilder.test.js.map +1 -0
  187. package/dist/tests/framework/LargeScaleScenarios.test.d.ts +2 -0
  188. package/dist/tests/framework/LargeScaleScenarios.test.d.ts.map +1 -0
  189. package/dist/tests/framework/LargeScaleScenarios.test.js +1043 -0
  190. package/dist/tests/framework/LargeScaleScenarios.test.js.map +1 -0
  191. package/dist/tests/framework/McpServerAdapter.test.d.ts +2 -0
  192. package/dist/tests/framework/McpServerAdapter.test.d.ts.map +1 -0
  193. package/dist/tests/framework/McpServerAdapter.test.js +380 -0
  194. package/dist/tests/framework/McpServerAdapter.test.js.map +1 -0
  195. package/dist/tests/framework/ResponseHelper.test.d.ts +2 -0
  196. package/dist/tests/framework/ResponseHelper.test.d.ts.map +1 -0
  197. package/dist/tests/framework/ResponseHelper.test.js +202 -0
  198. package/dist/tests/framework/ResponseHelper.test.js.map +1 -0
  199. package/dist/tests/framework/SecurityDeep.test.d.ts +2 -0
  200. package/dist/tests/framework/SecurityDeep.test.d.ts.map +1 -0
  201. package/dist/tests/framework/SecurityDeep.test.js +825 -0
  202. package/dist/tests/framework/SecurityDeep.test.js.map +1 -0
  203. package/dist/tests/framework/ToolRegistry.test.d.ts +2 -0
  204. package/dist/tests/framework/ToolRegistry.test.d.ts.map +1 -0
  205. package/dist/tests/framework/ToolRegistry.test.js +152 -0
  206. package/dist/tests/framework/ToolRegistry.test.js.map +1 -0
  207. package/dist/tests/framework/ToonDescription.test.d.ts +2 -0
  208. package/dist/tests/framework/ToonDescription.test.d.ts.map +1 -0
  209. package/dist/tests/framework/ToonDescription.test.js +287 -0
  210. package/dist/tests/framework/ToonDescription.test.js.map +1 -0
  211. package/package.json +64 -0
@@ -0,0 +1,825 @@
1
+ /**
2
+ * SecurityDeep.test.ts
3
+ *
4
+ * Deep security testing for the MCP Tool Consolidation Framework.
5
+ * These tests probe attack vectors that are critical in production
6
+ * AI infrastructure where LLM outputs are untrusted input.
7
+ *
8
+ * Attack Vectors:
9
+ * 1. ReDoS — Catastrophic regex backtracking via Zod patterns
10
+ * 2. JSON Bomb / Memory Exhaustion — Deeply nested objects, huge payloads
11
+ * 3. Error Message Information Leakage — No internal paths or stack traces
12
+ * 4. Handler Isolation — One handler's failure must not corrupt another
13
+ * 5. Context Pollution — Shared mutable context between calls
14
+ * 6. Registry Enumeration — Error messages reveal tool inventory
15
+ * 7. Type Confusion — JS coercion attacks via valueOf/toString
16
+ * 8. Schema Poisoning — Action schema polluting shared inputSchema
17
+ * 9. Middleware Bypass Attempts — Manipulating args to skip validation
18
+ * 10. Frozen Definition Tampering — Mutating cached tool definition
19
+ * 11. Timing-Safe Action Lookup — No enumeration via timing
20
+ * 12. Zod Coercion Exploitation — Exploiting type coercion edge cases
21
+ * 13. Recursive/Circular Reference — Objects with circular refs
22
+ * 14. Symbol & Non-String Key Injection — Non-string property keys
23
+ * 15. Denial of Service via Handler — Handlers that never resolve
24
+ */
25
+ import { describe, it, expect } from 'vitest';
26
+ import { z } from 'zod';
27
+ import { GroupedToolBuilder } from '../../src/framework/GroupedToolBuilder.js';
28
+ import { ToolRegistry } from '../../src/framework/ToolRegistry.js';
29
+ import { success, error } from '../../src/framework/ResponseHelper.js';
30
+ // ============================================================================
31
+ // 1. ReDoS — Catastrophic Regex Backtracking
32
+ // ============================================================================
33
+ describe('Security: ReDoS via Zod Patterns', () => {
34
+ it('should handle evil regex input without hanging (exponential backtracking)', async () => {
35
+ const builder = new GroupedToolBuilder('redos_test')
36
+ .action({
37
+ name: 'search',
38
+ schema: z.object({
39
+ // Common vulnerable pattern: nested quantifiers
40
+ query: z.string().regex(/^([a-zA-Z0-9]+)*$/),
41
+ }),
42
+ handler: async (_ctx, args) => success(`found: ${args.query}`),
43
+ });
44
+ builder.buildToolDefinition();
45
+ // Evil input designed to cause catastrophic backtracking
46
+ // Pattern: valid chars followed by a non-matching char
47
+ const evilInput = 'a'.repeat(25) + '!';
48
+ const start = Date.now();
49
+ const result = await builder.execute(undefined, {
50
+ action: 'search',
51
+ query: evilInput,
52
+ });
53
+ const elapsed = Date.now() - start;
54
+ // Should fail validation (not match regex) — not hang
55
+ expect(result.isError).toBe(true);
56
+ // Should complete in reasonable time (< 5s), not exponential
57
+ expect(elapsed).toBeLessThan(5000);
58
+ });
59
+ it('should safely validate very long strings against patterns', async () => {
60
+ const builder = new GroupedToolBuilder('long_pattern')
61
+ .action({
62
+ name: 'validate',
63
+ schema: z.object({
64
+ input: z.string().max(10000).regex(/^[a-z]+$/),
65
+ }),
66
+ handler: async () => success('ok'),
67
+ });
68
+ builder.buildToolDefinition();
69
+ // Valid long string
70
+ const result = await builder.execute(undefined, {
71
+ action: 'validate',
72
+ input: 'a'.repeat(10000),
73
+ });
74
+ expect(result.isError).toBeUndefined();
75
+ // Just over max
76
+ const result2 = await builder.execute(undefined, {
77
+ action: 'validate',
78
+ input: 'a'.repeat(10001),
79
+ });
80
+ expect(result2.isError).toBe(true);
81
+ });
82
+ });
83
+ // ============================================================================
84
+ // 2. JSON Bomb / Memory Exhaustion
85
+ // ============================================================================
86
+ describe('Security: JSON Bomb & Memory Exhaustion', () => {
87
+ it('should handle deeply nested object input without stack overflow', async () => {
88
+ const builder = new GroupedToolBuilder('nested_bomb')
89
+ .action({
90
+ name: 'process',
91
+ schema: z.object({ data: z.any() }),
92
+ handler: async () => success('processed'),
93
+ });
94
+ builder.buildToolDefinition();
95
+ // Create deeply nested object (100 levels)
96
+ let nested = { value: 'leaf' };
97
+ for (let i = 0; i < 100; i++) {
98
+ nested = { child: nested };
99
+ }
100
+ const result = await builder.execute(undefined, {
101
+ action: 'process',
102
+ data: nested,
103
+ });
104
+ // Should not crash — z.any() accepts anything
105
+ expect(result.isError).toBeUndefined();
106
+ });
107
+ it('should handle massive string payload gracefully', async () => {
108
+ const builder = new GroupedToolBuilder('big_string')
109
+ .action({
110
+ name: 'ingest',
111
+ schema: z.object({
112
+ content: z.string().max(1000000),
113
+ }),
114
+ handler: async (_ctx, args) => success(`ingested ${args.content.length} chars`),
115
+ });
116
+ builder.buildToolDefinition();
117
+ // 1MB string — at limit
118
+ const result = await builder.execute(undefined, {
119
+ action: 'ingest',
120
+ content: 'x'.repeat(1000000),
121
+ });
122
+ expect(result.isError).toBeUndefined();
123
+ // Over limit
124
+ const result2 = await builder.execute(undefined, {
125
+ action: 'ingest',
126
+ content: 'x'.repeat(1000001),
127
+ });
128
+ expect(result2.isError).toBe(true);
129
+ });
130
+ it('should handle array with many elements', async () => {
131
+ const builder = new GroupedToolBuilder('big_array')
132
+ .action({
133
+ name: 'batch',
134
+ schema: z.object({
135
+ items: z.array(z.string()).max(10000),
136
+ }),
137
+ handler: async (_ctx, args) => success(`batched ${args.items.length}`),
138
+ });
139
+ builder.buildToolDefinition();
140
+ const result = await builder.execute(undefined, {
141
+ action: 'batch',
142
+ items: Array.from({ length: 10000 }, (_, i) => `item-${i}`),
143
+ });
144
+ expect(result.isError).toBeUndefined();
145
+ });
146
+ });
147
+ // ============================================================================
148
+ // 3. Error Message Information Leakage
149
+ // ============================================================================
150
+ describe('Security: Error Message Information Leakage', () => {
151
+ it('handler exceptions should not expose stack traces', async () => {
152
+ const builder = new GroupedToolBuilder('leak_test')
153
+ .action({
154
+ name: 'explode',
155
+ handler: async () => {
156
+ const err = new Error('DB connection failed');
157
+ err.stack = 'Error: DB connection failed\n at /app/src/db/pool.ts:42:12';
158
+ throw err;
159
+ },
160
+ });
161
+ builder.buildToolDefinition();
162
+ const result = await builder.execute(undefined, { action: 'explode' });
163
+ expect(result.isError).toBe(true);
164
+ // Should contain message but NOT stack trace
165
+ expect(result.content[0].text).toContain('DB connection failed');
166
+ expect(result.content[0].text).not.toContain('/app/src/');
167
+ expect(result.content[0].text).not.toContain('.ts:');
168
+ expect(result.content[0].text).not.toContain('at ');
169
+ });
170
+ it('validation errors should not expose Zod internals', async () => {
171
+ const builder = new GroupedToolBuilder('zod_leak')
172
+ .action({
173
+ name: 'create',
174
+ schema: z.object({
175
+ email: z.string().email(),
176
+ password: z.string().min(8),
177
+ }),
178
+ handler: async () => success('created'),
179
+ });
180
+ builder.buildToolDefinition();
181
+ const result = await builder.execute(undefined, {
182
+ action: 'create',
183
+ email: 'not-email',
184
+ password: '123',
185
+ });
186
+ expect(result.isError).toBe(true);
187
+ // Should describe the field errors but not Zod class names
188
+ expect(result.content[0].text).not.toContain('ZodError');
189
+ expect(result.content[0].text).not.toContain('ZodIssue');
190
+ expect(result.content[0].text).toContain('Validation failed');
191
+ });
192
+ it('unknown action error should list available actions (intentional for LLM)', async () => {
193
+ const builder = new GroupedToolBuilder('enum_test')
194
+ .action({ name: 'list', handler: async () => success('ok') })
195
+ .action({ name: 'create', handler: async () => success('ok') });
196
+ builder.buildToolDefinition();
197
+ const result = await builder.execute(undefined, { action: 'hack' });
198
+ expect(result.isError).toBe(true);
199
+ // For MCP, listing available actions is intentional (helps LLM self-correct)
200
+ expect(result.content[0].text).toContain('list');
201
+ expect(result.content[0].text).toContain('create');
202
+ });
203
+ it('unknown tool in registry should list available tools (intentional for LLM)', async () => {
204
+ const registry = new ToolRegistry();
205
+ registry.register(new GroupedToolBuilder('users')
206
+ .action({ name: 'list', handler: async () => success('ok') }));
207
+ registry.register(new GroupedToolBuilder('billing')
208
+ .action({ name: 'charge', handler: async () => success('ok') }));
209
+ const result = await registry.routeCall(undefined, 'admin', {});
210
+ expect(result.isError).toBe(true);
211
+ expect(result.content[0].text).toContain('users');
212
+ expect(result.content[0].text).toContain('billing');
213
+ });
214
+ });
215
+ // ============================================================================
216
+ // 4. Handler Isolation — Failure Containment
217
+ // ============================================================================
218
+ describe('Security: Handler Isolation', () => {
219
+ it('one handler throwing should not affect another handler', async () => {
220
+ let stateA = 'clean';
221
+ const builder = new GroupedToolBuilder('isolation')
222
+ .action({
223
+ name: 'safe',
224
+ handler: async () => {
225
+ stateA = 'executed';
226
+ return success('safe ok');
227
+ },
228
+ })
229
+ .action({
230
+ name: 'bomb',
231
+ handler: async () => {
232
+ throw new Error('KABOOM');
233
+ },
234
+ });
235
+ builder.buildToolDefinition();
236
+ // Bomb first
237
+ const r1 = await builder.execute(undefined, { action: 'bomb' });
238
+ expect(r1.isError).toBe(true);
239
+ // Safe should still work perfectly
240
+ const r2 = await builder.execute(undefined, { action: 'safe' });
241
+ expect(r2.isError).toBeUndefined();
242
+ expect(stateA).toBe('executed');
243
+ });
244
+ it('handler throwing non-Error objects should be contained', async () => {
245
+ const builder = new GroupedToolBuilder('non_error')
246
+ .action({
247
+ name: 'throw_string',
248
+ handler: async () => { throw 'raw string error'; },
249
+ })
250
+ .action({
251
+ name: 'throw_number',
252
+ handler: async () => { throw 42; },
253
+ })
254
+ .action({
255
+ name: 'throw_null',
256
+ handler: async () => { throw null; },
257
+ })
258
+ .action({
259
+ name: 'throw_undefined',
260
+ handler: async () => { throw undefined; },
261
+ })
262
+ .action({
263
+ name: 'throw_object',
264
+ handler: async () => { throw { code: 'ERR', msg: 'fail' }; },
265
+ });
266
+ builder.buildToolDefinition();
267
+ for (const action of ['throw_string', 'throw_number', 'throw_null', 'throw_undefined', 'throw_object']) {
268
+ const r = await builder.execute(undefined, { action });
269
+ expect(r.isError).toBe(true);
270
+ expect(r.content[0].type).toBe('text');
271
+ expect(typeof r.content[0].text).toBe('string');
272
+ }
273
+ });
274
+ it('synchronous throw inside async handler should be caught', async () => {
275
+ const builder = new GroupedToolBuilder('sync_throw')
276
+ .action({
277
+ name: 'sync_bomb',
278
+ handler: async () => {
279
+ // Synchronous throw inside async function
280
+ if (true)
281
+ throw new RangeError('out of range');
282
+ return success('unreachable');
283
+ },
284
+ });
285
+ builder.buildToolDefinition();
286
+ const r = await builder.execute(undefined, { action: 'sync_bomb' });
287
+ expect(r.isError).toBe(true);
288
+ expect(r.content[0].text).toContain('out of range');
289
+ });
290
+ });
291
+ // ============================================================================
292
+ // 5. Context Pollution Between Calls
293
+ // ============================================================================
294
+ describe('Security: Context Pollution', () => {
295
+ it('mutable context should not leak state between independent calls', async () => {
296
+ const builder = new GroupedToolBuilder('ctx_pollution')
297
+ .action({
298
+ name: 'write',
299
+ schema: z.object({ key: z.string(), value: z.string() }),
300
+ handler: async (ctx, args) => {
301
+ ctx.data.set(args.key, args.value);
302
+ return success(`wrote ${args.key}`);
303
+ },
304
+ })
305
+ .action({
306
+ name: 'read',
307
+ schema: z.object({ key: z.string() }),
308
+ handler: async (ctx, args) => {
309
+ const val = ctx.data.get(args.key);
310
+ return success(val !== null && val !== void 0 ? val : 'NOT_FOUND');
311
+ },
312
+ });
313
+ builder.buildToolDefinition();
314
+ // Tenant A writes
315
+ const ctxA = { tenantId: 'A', data: new Map() };
316
+ await builder.execute(ctxA, { action: 'write', key: 'secret', value: 'A-data' });
317
+ // Tenant B should NOT see Tenant A's data (separate context objects)
318
+ const ctxB = { tenantId: 'B', data: new Map() };
319
+ const result = await builder.execute(ctxB, { action: 'read', key: 'secret' });
320
+ expect(result.content[0].text).toBe('NOT_FOUND');
321
+ // Tenant A should still have its data
322
+ const resultA = await builder.execute(ctxA, { action: 'read', key: 'secret' });
323
+ expect(resultA.content[0].text).toBe('A-data');
324
+ });
325
+ it('middleware should not be able to permanently pollute shared builder state', async () => {
326
+ let middlewareCallCount = 0;
327
+ const builder = new GroupedToolBuilder('mw_pollution')
328
+ .use(async (_ctx, _args, next) => {
329
+ middlewareCallCount++;
330
+ return next();
331
+ })
332
+ .action({
333
+ name: 'check',
334
+ handler: async () => success('ok'),
335
+ });
336
+ builder.buildToolDefinition();
337
+ await builder.execute(undefined, { action: 'check' });
338
+ expect(middlewareCallCount).toBe(1);
339
+ await builder.execute(undefined, { action: 'check' });
340
+ expect(middlewareCallCount).toBe(2);
341
+ // Each call increments individually — no accumulated side effects on builder
342
+ });
343
+ });
344
+ // ============================================================================
345
+ // 6. Registry Enumeration Attack
346
+ // ============================================================================
347
+ describe('Security: Registry Enumeration', () => {
348
+ it('routeCall error reveals registered tool names (by design for LLM)', async () => {
349
+ const registry = new ToolRegistry();
350
+ const secretTools = ['admin_panel', 'internal_debug', 'user_management'];
351
+ for (const name of secretTools) {
352
+ registry.register(new GroupedToolBuilder(name)
353
+ .action({ name: 'run', handler: async () => success('ok') }));
354
+ }
355
+ const result = await registry.routeCall(undefined, 'probe', {});
356
+ expect(result.isError).toBe(true);
357
+ // In MCP context, this is BY DESIGN — the LLM needs to know available tools
358
+ // But this test documents it explicitly so the team knows
359
+ for (const name of secretTools) {
360
+ expect(result.content[0].text).toContain(name);
361
+ }
362
+ });
363
+ it('tag filtering can hide tools from LLM context', () => {
364
+ const registry = new ToolRegistry();
365
+ registry.register(new GroupedToolBuilder('public_api')
366
+ .tags('public')
367
+ .action({ name: 'list', handler: async () => success('ok') }));
368
+ registry.register(new GroupedToolBuilder('admin_internal')
369
+ .tags('admin', 'internal')
370
+ .action({ name: 'debug', handler: async () => success('ok') }));
371
+ // Public API should only see public tools
372
+ const publicTools = registry.getTools({ tags: ['public'] });
373
+ expect(publicTools).toHaveLength(1);
374
+ expect(publicTools[0].name).toBe('public_api');
375
+ // Exclude internal tools
376
+ const filtered = registry.getTools({ exclude: ['internal'] });
377
+ expect(filtered).toHaveLength(1);
378
+ expect(filtered[0].name).toBe('public_api');
379
+ });
380
+ });
381
+ // ============================================================================
382
+ // 7. Type Confusion Attacks
383
+ // ============================================================================
384
+ describe('Security: Type Confusion', () => {
385
+ it('object with valueOf returning string should be rejected by Zod string validation', async () => {
386
+ const builder = new GroupedToolBuilder('type_confusion')
387
+ .action({
388
+ name: 'process',
389
+ schema: z.object({ name: z.string() }),
390
+ handler: async (_ctx, args) => success(`hello ${args.name}`),
391
+ });
392
+ builder.buildToolDefinition();
393
+ // Object masquerading as string
394
+ const evilObj = {
395
+ valueOf: () => 'injected',
396
+ toString: () => 'injected',
397
+ };
398
+ const result = await builder.execute(undefined, {
399
+ action: 'process',
400
+ name: evilObj,
401
+ });
402
+ // Zod's strict type checking should reject non-string
403
+ expect(result.isError).toBe(true);
404
+ });
405
+ it('number where string expected should fail validation', async () => {
406
+ const builder = new GroupedToolBuilder('num_as_str')
407
+ .action({
408
+ name: 'greet',
409
+ schema: z.object({ name: z.string() }),
410
+ handler: async (_ctx, args) => success(`hi ${args.name}`),
411
+ });
412
+ builder.buildToolDefinition();
413
+ const result = await builder.execute(undefined, {
414
+ action: 'greet',
415
+ name: 12345,
416
+ });
417
+ expect(result.isError).toBe(true);
418
+ });
419
+ it('string where number expected should fail validation', async () => {
420
+ const builder = new GroupedToolBuilder('str_as_num')
421
+ .action({
422
+ name: 'compute',
423
+ schema: z.object({ value: z.number() }),
424
+ handler: async (_ctx, args) => success(`result: ${args.value}`),
425
+ });
426
+ builder.buildToolDefinition();
427
+ const result = await builder.execute(undefined, {
428
+ action: 'compute',
429
+ value: '42', // string "42", not number 42
430
+ });
431
+ expect(result.isError).toBe(true);
432
+ });
433
+ it('boolean where string expected should fail validation', async () => {
434
+ const builder = new GroupedToolBuilder('bool_as_str')
435
+ .action({
436
+ name: 'process',
437
+ schema: z.object({ flag: z.string() }),
438
+ handler: async (_ctx, args) => success(`flag: ${args.flag}`),
439
+ });
440
+ builder.buildToolDefinition();
441
+ const result = await builder.execute(undefined, {
442
+ action: 'process',
443
+ flag: true,
444
+ });
445
+ expect(result.isError).toBe(true);
446
+ });
447
+ it('array where object expected should fail validation', async () => {
448
+ const builder = new GroupedToolBuilder('array_as_obj')
449
+ .action({
450
+ name: 'process',
451
+ schema: z.object({
452
+ config: z.object({ key: z.string() }),
453
+ }),
454
+ handler: async () => success('ok'),
455
+ });
456
+ builder.buildToolDefinition();
457
+ const result = await builder.execute(undefined, {
458
+ action: 'process',
459
+ config: ['key', 'value'], // array, not object
460
+ });
461
+ expect(result.isError).toBe(true);
462
+ });
463
+ });
464
+ // ============================================================================
465
+ // 8. Schema Poisoning — Cross-Action Contamination
466
+ // ============================================================================
467
+ describe('Security: Schema Poisoning', () => {
468
+ it('first action schema should not be overwritten by second action with same field', () => {
469
+ const builder = new GroupedToolBuilder('schema_poison')
470
+ .action({
471
+ name: 'create',
472
+ schema: z.object({
473
+ name: z.string().describe('Full name of the user'),
474
+ }),
475
+ handler: async () => success('ok'),
476
+ })
477
+ .action({
478
+ name: 'search',
479
+ schema: z.object({
480
+ name: z.string().describe('HACKED: This overrides the first'),
481
+ }),
482
+ handler: async () => success('ok'),
483
+ });
484
+ const def = builder.buildToolDefinition();
485
+ const nameField = def.inputSchema.properties.name;
486
+ // First declaration wins — description should be from 'create'
487
+ expect(nameField.description).toContain('Full name');
488
+ });
489
+ it('common schema fields should be separate from action schema fields in validation', async () => {
490
+ const builder = new GroupedToolBuilder('validate_isolation')
491
+ .commonSchema(z.object({
492
+ org: z.string().min(1),
493
+ }))
494
+ .action({
495
+ name: 'strict',
496
+ schema: z.object({
497
+ value: z.number().int().positive(),
498
+ }),
499
+ handler: async () => success('strict ok'),
500
+ })
501
+ .action({
502
+ name: 'loose',
503
+ schema: z.object({
504
+ value: z.string().optional(),
505
+ }),
506
+ handler: async () => success('loose ok'),
507
+ });
508
+ builder.buildToolDefinition();
509
+ // 'strict' requires positive integer for value
510
+ const r1 = await builder.execute(undefined, {
511
+ action: 'strict', org: 'acme', value: -5,
512
+ });
513
+ expect(r1.isError).toBe(true);
514
+ // 'loose' allows optional string for value — different validation context
515
+ const r2 = await builder.execute(undefined, {
516
+ action: 'loose', org: 'acme',
517
+ });
518
+ expect(r2.isError).toBeUndefined();
519
+ });
520
+ });
521
+ // ============================================================================
522
+ // 9. Middleware Bypass Attempts
523
+ // ============================================================================
524
+ describe('Security: Middleware Bypass', () => {
525
+ it('cannot bypass middleware by manipulating discriminator after validation', async () => {
526
+ const middlewareLog = [];
527
+ const builder = new GroupedToolBuilder('mw_bypass')
528
+ .use(async (_ctx, args, next) => {
529
+ middlewareLog.push(`mw:${args.action}`);
530
+ return next();
531
+ })
532
+ .action({
533
+ name: 'public',
534
+ handler: async () => success('public result'),
535
+ })
536
+ .action({
537
+ name: 'admin',
538
+ handler: async () => success('admin result'),
539
+ });
540
+ builder.buildToolDefinition();
541
+ // Normal call
542
+ await builder.execute(undefined, { action: 'public' });
543
+ expect(middlewareLog).toContain('mw:public');
544
+ // Try calling admin — middleware still runs
545
+ middlewareLog.length = 0;
546
+ await builder.execute(undefined, { action: 'admin' });
547
+ expect(middlewareLog).toContain('mw:admin');
548
+ });
549
+ it('middleware receives discriminator value for authorization checks', async () => {
550
+ const builder = new GroupedToolBuilder('auth_mw')
551
+ .use(async (ctx, args, next) => {
552
+ if (args.action === 'admin_delete' && ctx.role !== 'admin') {
553
+ return error('FORBIDDEN: admin only');
554
+ }
555
+ return next();
556
+ })
557
+ .action({
558
+ name: 'list',
559
+ handler: async () => success('list ok'),
560
+ })
561
+ .action({
562
+ name: 'admin_delete',
563
+ handler: async () => success('deleted'),
564
+ });
565
+ builder.buildToolDefinition();
566
+ // Non-admin trying admin action
567
+ const r1 = await builder.execute({ role: 'user' }, { action: 'admin_delete' });
568
+ expect(r1.isError).toBe(true);
569
+ expect(r1.content[0].text).toContain('FORBIDDEN');
570
+ // Admin can access
571
+ const r2 = await builder.execute({ role: 'admin' }, { action: 'admin_delete' });
572
+ expect(r2.isError).toBeUndefined();
573
+ // Non-admin on public action is fine
574
+ const r3 = await builder.execute({ role: 'user' }, { action: 'list' });
575
+ expect(r3.isError).toBeUndefined();
576
+ });
577
+ });
578
+ // ============================================================================
579
+ // 10. Frozen Definition Tampering
580
+ // ============================================================================
581
+ describe('Security: Frozen Definition Tampering', () => {
582
+ it('mutating returned tool definition should not affect future calls', () => {
583
+ const builder = new GroupedToolBuilder('tamper')
584
+ .description('Original description')
585
+ .action({ name: 'ping', handler: async () => success('pong') });
586
+ const def1 = builder.buildToolDefinition();
587
+ // Attempt to tamper with the returned definition
588
+ def1.name = 'HACKED';
589
+ def1.description = 'HACKED';
590
+ def1.inputSchema = { type: 'object', properties: {} };
591
+ // Since buildToolDefinition returns cached ref, the cached version IS mutated.
592
+ // This tests documents the behavior: the reference IS shared.
593
+ const def2 = builder.buildToolDefinition();
594
+ // Same reference — mutation is visible (this is a documentation test)
595
+ expect(def2).toBe(def1);
596
+ });
597
+ it('builder should still route correctly even if definition is externally mutated', async () => {
598
+ const builder = new GroupedToolBuilder('tamper_route')
599
+ .action({ name: 'ping', handler: async () => success('pong') });
600
+ const def = builder.buildToolDefinition();
601
+ // Tamper with the definition
602
+ def.name = 'HACKED';
603
+ // Routing uses internal state, not the cached definition
604
+ const result = await builder.execute(undefined, { action: 'ping' });
605
+ expect(result.isError).toBeUndefined();
606
+ expect(result.content[0].text).toBe('pong');
607
+ });
608
+ });
609
+ // ============================================================================
610
+ // 11. Zod Coercion Edge Cases
611
+ // ============================================================================
612
+ describe('Security: Zod Coercion Edge Cases', () => {
613
+ it('NaN should be rejected for number fields', async () => {
614
+ const builder = new GroupedToolBuilder('nan_test')
615
+ .action({
616
+ name: 'compute',
617
+ schema: z.object({ value: z.number() }),
618
+ handler: async () => success('ok'),
619
+ });
620
+ builder.buildToolDefinition();
621
+ const result = await builder.execute(undefined, {
622
+ action: 'compute',
623
+ value: NaN,
624
+ });
625
+ // NaN is technically a number type, but Zod should handle it
626
+ // This documents the behavior
627
+ expect(typeof NaN).toBe('number');
628
+ });
629
+ it('Infinity should be handled for number fields', async () => {
630
+ const builder = new GroupedToolBuilder('inf_test')
631
+ .action({
632
+ name: 'compute',
633
+ schema: z.object({ value: z.number().finite() }),
634
+ handler: async () => success('ok'),
635
+ });
636
+ builder.buildToolDefinition();
637
+ const r1 = await builder.execute(undefined, {
638
+ action: 'compute',
639
+ value: Infinity,
640
+ });
641
+ expect(r1.isError).toBe(true);
642
+ const r2 = await builder.execute(undefined, {
643
+ action: 'compute',
644
+ value: -Infinity,
645
+ });
646
+ expect(r2.isError).toBe(true);
647
+ });
648
+ it('Date object where string expected should fail', async () => {
649
+ const builder = new GroupedToolBuilder('date_confusion')
650
+ .action({
651
+ name: 'process',
652
+ schema: z.object({ timestamp: z.string() }),
653
+ handler: async () => success('ok'),
654
+ });
655
+ builder.buildToolDefinition();
656
+ const result = await builder.execute(undefined, {
657
+ action: 'process',
658
+ timestamp: new Date(),
659
+ });
660
+ expect(result.isError).toBe(true);
661
+ });
662
+ });
663
+ // ============================================================================
664
+ // 12. Circular Reference Handling
665
+ // ============================================================================
666
+ describe('Security: Circular References', () => {
667
+ it('circular reference in args should not cause infinite loop', async () => {
668
+ const builder = new GroupedToolBuilder('circular')
669
+ .action({
670
+ name: 'process',
671
+ // No schema — no validation, just pass through
672
+ handler: async (_ctx, args) => {
673
+ try {
674
+ // Handler tries to serialize — would fail with circular ref
675
+ return success('processed');
676
+ }
677
+ catch (_a) {
678
+ return error('serialization failed');
679
+ }
680
+ },
681
+ });
682
+ builder.buildToolDefinition();
683
+ // Create circular reference
684
+ const args = { action: 'process', data: {} };
685
+ args.data.self = args.data; // circular!
686
+ // Should not hang — handler doesn't try to serialize
687
+ const result = await builder.execute(undefined, args);
688
+ expect(result.content[0]).toHaveProperty('type', 'text');
689
+ });
690
+ });
691
+ // ============================================================================
692
+ // 13. Symbol & Non-String Key Injection
693
+ // ============================================================================
694
+ describe('Security: Symbol & Non-String Key Injection', () => {
695
+ it('Symbol keys in args should not affect routing', async () => {
696
+ const builder = new GroupedToolBuilder('symbol_test')
697
+ .action({
698
+ name: 'run',
699
+ handler: async () => success('ok'),
700
+ });
701
+ builder.buildToolDefinition();
702
+ const sym = Symbol('malicious');
703
+ const args = { action: 'run' };
704
+ args[sym] = 'hidden payload';
705
+ const result = await builder.execute(undefined, args);
706
+ expect(result.isError).toBeUndefined();
707
+ });
708
+ it('numeric keys in args should not confuse routing', async () => {
709
+ const builder = new GroupedToolBuilder('numeric_key')
710
+ .action({
711
+ name: 'run',
712
+ handler: async () => success('ok'),
713
+ });
714
+ builder.buildToolDefinition();
715
+ const result = await builder.execute(undefined, {
716
+ action: 'run',
717
+ 0: 'zero',
718
+ 1: 'one',
719
+ length: 2,
720
+ });
721
+ expect(result.isError).toBeUndefined();
722
+ });
723
+ });
724
+ // ============================================================================
725
+ // 14. Concurrent Execution Safety
726
+ // ============================================================================
727
+ describe('Security: Concurrent Execution Safety', () => {
728
+ it('parallel calls with different contexts should not cross-contaminate', async () => {
729
+ const builder = new GroupedToolBuilder('concurrent_ctx')
730
+ .action({
731
+ name: 'whoami',
732
+ handler: async (ctx) => {
733
+ // Simulate async work
734
+ await new Promise(r => setTimeout(r, Math.random() * 20));
735
+ return success(`user:${ctx.userId}`);
736
+ },
737
+ });
738
+ builder.buildToolDefinition();
739
+ // Fire 20 concurrent calls with different contexts
740
+ const promises = Array.from({ length: 20 }, (_, i) => builder.execute({ userId: `user-${i}` }, { action: 'whoami' }));
741
+ const results = await Promise.all(promises);
742
+ // Each result must correspond to its own context
743
+ for (let i = 0; i < 20; i++) {
744
+ expect(results[i].content[0].text).toBe(`user:user-${i}`);
745
+ }
746
+ });
747
+ it('parallel registry routing should not mix up tool handlers', async () => {
748
+ const registry = new ToolRegistry();
749
+ for (let i = 0; i < 10; i++) {
750
+ registry.register(new GroupedToolBuilder(`tool_${i}`)
751
+ .action({
752
+ name: 'identify',
753
+ handler: async () => {
754
+ await new Promise(r => setTimeout(r, Math.random() * 10));
755
+ return success(`tool_${i}`);
756
+ },
757
+ }));
758
+ }
759
+ const promises = Array.from({ length: 10 }, (_, i) => registry.routeCall(undefined, `tool_${i}`, { action: 'identify' }));
760
+ const results = await Promise.all(promises);
761
+ for (let i = 0; i < 10; i++) {
762
+ expect(results[i].content[0].text).toBe(`tool_${i}`);
763
+ }
764
+ });
765
+ });
766
+ // ============================================================================
767
+ // 15. Payload Injection via JSON Special Values
768
+ // ============================================================================
769
+ describe('Security: JSON Special Values', () => {
770
+ it('should handle -0 correctly', async () => {
771
+ const builder = new GroupedToolBuilder('neg_zero')
772
+ .action({
773
+ name: 'compute',
774
+ schema: z.object({ value: z.number() }),
775
+ handler: async (_ctx, args) => success(`value: ${Object.is(args.value, -0) ? '-0' : args.value}`),
776
+ });
777
+ builder.buildToolDefinition();
778
+ const result = await builder.execute(undefined, {
779
+ action: 'compute',
780
+ value: -0,
781
+ });
782
+ expect(result.isError).toBeUndefined();
783
+ });
784
+ it('should handle unicode null bytes in strings', async () => {
785
+ const builder = new GroupedToolBuilder('null_byte')
786
+ .action({
787
+ name: 'process',
788
+ schema: z.object({ data: z.string() }),
789
+ handler: async (_ctx, args) => success(`len: ${args.data.length}`),
790
+ });
791
+ builder.buildToolDefinition();
792
+ // String with null bytes embedded
793
+ const result = await builder.execute(undefined, {
794
+ action: 'process',
795
+ data: 'hello\x00world\x00evil',
796
+ });
797
+ expect(result.isError).toBeUndefined();
798
+ });
799
+ it('should handle unicode surrogate pairs correctly', async () => {
800
+ const builder = new GroupedToolBuilder('surrogate')
801
+ .action({
802
+ name: 'process',
803
+ schema: z.object({ data: z.string() }),
804
+ handler: async (_ctx, args) => success(`got: ${args.data}`),
805
+ });
806
+ builder.buildToolDefinition();
807
+ // Emoji + surrogate pairs
808
+ const result = await builder.execute(undefined, {
809
+ action: 'process',
810
+ data: '💀🔥 test \uD83D\uDE00 end',
811
+ });
812
+ expect(result.isError).toBeUndefined();
813
+ });
814
+ it('should handle extremely long action name in input (not in definition)', async () => {
815
+ const builder = new GroupedToolBuilder('long_action_input')
816
+ .action({ name: 'valid', handler: async () => success('ok') });
817
+ builder.buildToolDefinition();
818
+ const result = await builder.execute(undefined, {
819
+ action: 'x'.repeat(100000), // 100KB action name
820
+ });
821
+ expect(result.isError).toBe(true);
822
+ expect(result.content[0].text).toContain('Unknown');
823
+ });
824
+ });
825
+ //# sourceMappingURL=SecurityDeep.test.js.map