@dizzlkheinz/ynab-mcpb 0.18.3 → 0.19.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 (346) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/CLAUDE.md +87 -8
  3. package/bin/ynab-mcp-server.cjs +2 -2
  4. package/bin/ynab-mcp-server.js +3 -3
  5. package/biome.json +39 -0
  6. package/dist/bundle/index.cjs +67 -67
  7. package/dist/index.d.ts +1 -1
  8. package/dist/index.js +27 -27
  9. package/dist/server/YNABMCPServer.d.ts +3 -4
  10. package/dist/server/YNABMCPServer.js +111 -116
  11. package/dist/server/budgetResolver.d.ts +6 -5
  12. package/dist/server/budgetResolver.js +46 -36
  13. package/dist/server/cacheKeys.js +6 -6
  14. package/dist/server/cacheManager.js +14 -11
  15. package/dist/server/completions.d.ts +2 -2
  16. package/dist/server/completions.js +20 -15
  17. package/dist/server/config.d.ts +10 -5
  18. package/dist/server/config.js +24 -7
  19. package/dist/server/deltaCache.d.ts +2 -2
  20. package/dist/server/deltaCache.js +22 -16
  21. package/dist/server/deltaCache.merge.d.ts +2 -2
  22. package/dist/server/diagnostics.d.ts +4 -4
  23. package/dist/server/diagnostics.js +38 -32
  24. package/dist/server/errorHandler.d.ts +5 -12
  25. package/dist/server/errorHandler.js +219 -217
  26. package/dist/server/prompts.d.ts +2 -2
  27. package/dist/server/prompts.js +45 -45
  28. package/dist/server/rateLimiter.js +4 -4
  29. package/dist/server/requestLogger.d.ts +1 -1
  30. package/dist/server/requestLogger.js +40 -35
  31. package/dist/server/resources.d.ts +3 -3
  32. package/dist/server/resources.js +55 -52
  33. package/dist/server/responseFormatter.js +6 -6
  34. package/dist/server/securityMiddleware.d.ts +2 -2
  35. package/dist/server/securityMiddleware.js +22 -20
  36. package/dist/server/serverKnowledgeStore.js +1 -1
  37. package/dist/server/toolRegistry.d.ts +3 -3
  38. package/dist/server/toolRegistry.js +47 -40
  39. package/dist/tools/__tests__/deltaTestUtils.d.ts +3 -3
  40. package/dist/tools/__tests__/deltaTestUtils.js +2 -2
  41. package/dist/tools/accountTools.d.ts +9 -8
  42. package/dist/tools/accountTools.js +47 -47
  43. package/dist/tools/adapters.d.ts +13 -8
  44. package/dist/tools/adapters.js +21 -11
  45. package/dist/tools/budgetTools.d.ts +8 -7
  46. package/dist/tools/budgetTools.js +22 -22
  47. package/dist/tools/categoryTools.d.ts +9 -8
  48. package/dist/tools/categoryTools.js +68 -59
  49. package/dist/tools/compareTransactions/formatter.d.ts +3 -3
  50. package/dist/tools/compareTransactions/formatter.js +9 -9
  51. package/dist/tools/compareTransactions/index.d.ts +6 -6
  52. package/dist/tools/compareTransactions/index.js +58 -43
  53. package/dist/tools/compareTransactions/matcher.d.ts +1 -1
  54. package/dist/tools/compareTransactions/matcher.js +28 -15
  55. package/dist/tools/compareTransactions/parser.d.ts +2 -2
  56. package/dist/tools/compareTransactions/parser.js +144 -138
  57. package/dist/tools/compareTransactions/types.d.ts +4 -4
  58. package/dist/tools/compareTransactions.d.ts +1 -1
  59. package/dist/tools/compareTransactions.js +1 -1
  60. package/dist/tools/deltaFetcher.d.ts +2 -2
  61. package/dist/tools/deltaFetcher.js +16 -15
  62. package/dist/tools/deltaSupport.d.ts +4 -4
  63. package/dist/tools/deltaSupport.js +35 -41
  64. package/dist/tools/exportTransactions.d.ts +5 -4
  65. package/dist/tools/exportTransactions.js +61 -59
  66. package/dist/tools/monthTools.d.ts +7 -6
  67. package/dist/tools/monthTools.js +31 -29
  68. package/dist/tools/payeeTools.d.ts +7 -6
  69. package/dist/tools/payeeTools.js +28 -28
  70. package/dist/tools/reconcileAdapter.d.ts +2 -2
  71. package/dist/tools/reconcileAdapter.js +21 -11
  72. package/dist/tools/reconciliation/analyzer.d.ts +4 -4
  73. package/dist/tools/reconciliation/analyzer.js +136 -57
  74. package/dist/tools/reconciliation/csvParser.d.ts +3 -3
  75. package/dist/tools/reconciliation/csvParser.js +128 -104
  76. package/dist/tools/reconciliation/executor.d.ts +4 -4
  77. package/dist/tools/reconciliation/executor.js +148 -109
  78. package/dist/tools/reconciliation/index.d.ts +10 -10
  79. package/dist/tools/reconciliation/index.js +96 -83
  80. package/dist/tools/reconciliation/matcher.d.ts +3 -3
  81. package/dist/tools/reconciliation/matcher.js +17 -16
  82. package/dist/tools/reconciliation/payeeNormalizer.js +19 -8
  83. package/dist/tools/reconciliation/recommendationEngine.d.ts +1 -1
  84. package/dist/tools/reconciliation/recommendationEngine.js +40 -40
  85. package/dist/tools/reconciliation/reportFormatter.d.ts +2 -2
  86. package/dist/tools/reconciliation/reportFormatter.js +79 -54
  87. package/dist/tools/reconciliation/signDetector.d.ts +1 -1
  88. package/dist/tools/reconciliation/types.d.ts +19 -16
  89. package/dist/tools/reconciliation/ynabAdapter.d.ts +2 -2
  90. package/dist/tools/schemas/common.d.ts +1 -1
  91. package/dist/tools/schemas/common.js +1 -1
  92. package/dist/tools/schemas/outputs/accountOutputs.d.ts +1 -1
  93. package/dist/tools/schemas/outputs/accountOutputs.js +24 -18
  94. package/dist/tools/schemas/outputs/budgetOutputs.d.ts +1 -1
  95. package/dist/tools/schemas/outputs/budgetOutputs.js +14 -11
  96. package/dist/tools/schemas/outputs/categoryOutputs.d.ts +1 -1
  97. package/dist/tools/schemas/outputs/categoryOutputs.js +49 -29
  98. package/dist/tools/schemas/outputs/comparisonOutputs.d.ts +1 -1
  99. package/dist/tools/schemas/outputs/comparisonOutputs.js +12 -12
  100. package/dist/tools/schemas/outputs/index.d.ts +14 -14
  101. package/dist/tools/schemas/outputs/index.js +14 -14
  102. package/dist/tools/schemas/outputs/monthOutputs.d.ts +1 -1
  103. package/dist/tools/schemas/outputs/monthOutputs.js +56 -41
  104. package/dist/tools/schemas/outputs/payeeOutputs.d.ts +1 -1
  105. package/dist/tools/schemas/outputs/payeeOutputs.js +10 -10
  106. package/dist/tools/schemas/outputs/reconciliationOutputs.d.ts +2 -2
  107. package/dist/tools/schemas/outputs/reconciliationOutputs.js +45 -45
  108. package/dist/tools/schemas/outputs/transactionMutationOutputs.d.ts +1 -1
  109. package/dist/tools/schemas/outputs/transactionMutationOutputs.js +28 -22
  110. package/dist/tools/schemas/outputs/transactionOutputs.d.ts +1 -1
  111. package/dist/tools/schemas/outputs/transactionOutputs.js +43 -35
  112. package/dist/tools/schemas/outputs/utilityOutputs.d.ts +1 -1
  113. package/dist/tools/schemas/outputs/utilityOutputs.js +5 -3
  114. package/dist/tools/schemas/shared/commonOutputs.d.ts +1 -1
  115. package/dist/tools/schemas/shared/commonOutputs.js +15 -9
  116. package/dist/tools/transactionReadTools.d.ts +11 -0
  117. package/dist/tools/transactionReadTools.js +202 -0
  118. package/dist/tools/transactionSchemas.d.ts +309 -0
  119. package/dist/tools/transactionSchemas.js +235 -0
  120. package/dist/tools/transactionTools.d.ts +6 -302
  121. package/dist/tools/transactionTools.js +7 -2054
  122. package/dist/tools/transactionUtils.d.ts +31 -0
  123. package/dist/tools/transactionUtils.js +364 -0
  124. package/dist/tools/transactionWriteTools.d.ts +20 -0
  125. package/dist/tools/transactionWriteTools.js +1342 -0
  126. package/dist/tools/utilityTools.d.ts +5 -4
  127. package/dist/tools/utilityTools.js +11 -11
  128. package/dist/types/index.d.ts +7 -7
  129. package/dist/types/index.js +6 -6
  130. package/dist/types/reconciliation.d.ts +1 -1
  131. package/dist/types/toolRegistration.d.ts +14 -12
  132. package/dist/utils/amountUtils.js +1 -1
  133. package/dist/utils/dateUtils.js +4 -4
  134. package/dist/utils/errors.d.ts +3 -3
  135. package/dist/utils/errors.js +4 -4
  136. package/dist/utils/money.d.ts +2 -2
  137. package/dist/utils/money.js +8 -8
  138. package/dist/utils/validationError.d.ts +1 -1
  139. package/dist/utils/validationError.js +1 -1
  140. package/docs/assets/examples/reconciliation-with-recommendations.json +66 -66
  141. package/docs/assets/schemas/reconciliation-v2.json +360 -336
  142. package/docs/plans/2025-12-25-transaction-tools-refactor-design.md +211 -0
  143. package/docs/plans/2025-12-25-transaction-tools-refactor.md +905 -0
  144. package/esbuild.config.mjs +53 -50
  145. package/meta.json +12548 -12548
  146. package/package.json +98 -109
  147. package/scripts/analyze-bundle.mjs +33 -30
  148. package/scripts/create-pr-description.js +169 -120
  149. package/scripts/run-all-tests.js +205 -0
  150. package/scripts/run-domain-integration-tests.js +28 -18
  151. package/scripts/run-generate-mcpb.js +19 -17
  152. package/scripts/run-throttled-integration-tests.js +92 -83
  153. package/scripts/test-delta-params.mjs +149 -120
  154. package/scripts/test-recommendations.ts +36 -32
  155. package/scripts/tmpTransaction.ts +80 -43
  156. package/scripts/validate-env.js +98 -91
  157. package/scripts/verify-build.js +78 -76
  158. package/src/__tests__/comprehensive.integration.test.ts +1281 -1154
  159. package/src/__tests__/performance.test.ts +723 -671
  160. package/src/__tests__/setup.ts +442 -395
  161. package/src/__tests__/smoke.e2e.test.ts +41 -39
  162. package/src/__tests__/testRunner.ts +314 -295
  163. package/src/__tests__/testUtils.ts +456 -364
  164. package/src/__tests__/tools/reconciliation/csvParser.integration.test.ts +109 -107
  165. package/src/__tests__/tools/reconciliation/real-world.integration.test.ts +41 -41
  166. package/src/index.ts +68 -59
  167. package/src/server/CLAUDE.md +480 -0
  168. package/src/server/YNABMCPServer.ts +821 -794
  169. package/src/server/__tests__/YNABMCPServer.integration.test.ts +929 -893
  170. package/src/server/__tests__/YNABMCPServer.test.ts +903 -899
  171. package/src/server/__tests__/budgetResolver.test.ts +466 -423
  172. package/src/server/__tests__/cacheManager.test.ts +891 -874
  173. package/src/server/__tests__/completions.integration.test.ts +115 -106
  174. package/src/server/__tests__/completions.test.ts +334 -313
  175. package/src/server/__tests__/config.test.ts +98 -86
  176. package/src/server/__tests__/deltaCache.merge.test.ts +774 -703
  177. package/src/server/__tests__/deltaCache.swr.test.ts +198 -153
  178. package/src/server/__tests__/deltaCache.test.ts +946 -759
  179. package/src/server/__tests__/diagnostics.test.ts +825 -792
  180. package/src/server/__tests__/errorHandler.integration.test.ts +512 -462
  181. package/src/server/__tests__/errorHandler.test.ts +402 -397
  182. package/src/server/__tests__/prompts.test.ts +424 -347
  183. package/src/server/__tests__/rateLimiter.test.ts +313 -309
  184. package/src/server/__tests__/requestLogger.test.ts +443 -403
  185. package/src/server/__tests__/resources.template.test.ts +196 -185
  186. package/src/server/__tests__/resources.test.ts +294 -288
  187. package/src/server/__tests__/security.integration.test.ts +487 -421
  188. package/src/server/__tests__/securityMiddleware.test.ts +519 -444
  189. package/src/server/__tests__/server-startup.integration.test.ts +509 -490
  190. package/src/server/__tests__/serverKnowledgeStore.test.ts +174 -173
  191. package/src/server/__tests__/toolRegistration.test.ts +239 -210
  192. package/src/server/__tests__/toolRegistry.test.ts +907 -845
  193. package/src/server/budgetResolver.ts +221 -181
  194. package/src/server/cacheKeys.ts +6 -6
  195. package/src/server/cacheManager.ts +498 -484
  196. package/src/server/completions.ts +267 -243
  197. package/src/server/config.ts +35 -14
  198. package/src/server/deltaCache.merge.ts +146 -128
  199. package/src/server/deltaCache.ts +352 -309
  200. package/src/server/diagnostics.ts +257 -242
  201. package/src/server/errorHandler.ts +747 -744
  202. package/src/server/prompts.ts +181 -176
  203. package/src/server/rateLimiter.ts +131 -129
  204. package/src/server/requestLogger.ts +350 -322
  205. package/src/server/resources.ts +442 -374
  206. package/src/server/responseFormatter.ts +41 -37
  207. package/src/server/securityMiddleware.ts +223 -205
  208. package/src/server/serverKnowledgeStore.ts +67 -67
  209. package/src/server/toolRegistry.ts +508 -474
  210. package/src/tools/CLAUDE.md +604 -0
  211. package/src/tools/__tests__/accountTools.delta.integration.test.ts +128 -111
  212. package/src/tools/__tests__/accountTools.integration.test.ts +129 -111
  213. package/src/tools/__tests__/accountTools.test.ts +685 -638
  214. package/src/tools/__tests__/adapters.test.ts +142 -108
  215. package/src/tools/__tests__/budgetTools.delta.integration.test.ts +73 -73
  216. package/src/tools/__tests__/budgetTools.integration.test.ts +132 -124
  217. package/src/tools/__tests__/budgetTools.test.ts +442 -413
  218. package/src/tools/__tests__/categoryTools.delta.integration.test.ts +76 -68
  219. package/src/tools/__tests__/categoryTools.integration.test.ts +314 -288
  220. package/src/tools/__tests__/categoryTools.test.ts +656 -625
  221. package/src/tools/__tests__/compareTransactions/formatter.test.ts +535 -462
  222. package/src/tools/__tests__/compareTransactions/index.test.ts +378 -358
  223. package/src/tools/__tests__/compareTransactions/matcher.test.ts +497 -398
  224. package/src/tools/__tests__/compareTransactions/parser.test.ts +765 -747
  225. package/src/tools/__tests__/compareTransactions.test.ts +352 -332
  226. package/src/tools/__tests__/compareTransactions.window.test.ts +150 -146
  227. package/src/tools/__tests__/deltaFetcher.scheduled.integration.test.ts +69 -65
  228. package/src/tools/__tests__/deltaFetcher.test.ts +325 -265
  229. package/src/tools/__tests__/deltaSupport.test.ts +211 -184
  230. package/src/tools/__tests__/deltaTestUtils.ts +37 -33
  231. package/src/tools/__tests__/exportTransactions.test.ts +205 -200
  232. package/src/tools/__tests__/monthTools.delta.integration.test.ts +68 -68
  233. package/src/tools/__tests__/monthTools.integration.test.ts +178 -166
  234. package/src/tools/__tests__/monthTools.test.ts +561 -512
  235. package/src/tools/__tests__/payeeTools.delta.integration.test.ts +68 -68
  236. package/src/tools/__tests__/payeeTools.integration.test.ts +158 -142
  237. package/src/tools/__tests__/payeeTools.test.ts +486 -434
  238. package/src/tools/__tests__/transactionSchemas.test.ts +1204 -0
  239. package/src/tools/__tests__/transactionTools.integration.test.ts +875 -825
  240. package/src/tools/__tests__/transactionTools.test.ts +4923 -4366
  241. package/src/tools/__tests__/transactionUtils.test.ts +1016 -0
  242. package/src/tools/__tests__/utilityTools.integration.test.ts +32 -32
  243. package/src/tools/__tests__/utilityTools.test.ts +68 -58
  244. package/src/tools/accountTools.ts +293 -271
  245. package/src/tools/adapters.ts +120 -63
  246. package/src/tools/budgetTools.ts +121 -116
  247. package/src/tools/categoryTools.ts +379 -339
  248. package/src/tools/compareTransactions/formatter.ts +131 -119
  249. package/src/tools/compareTransactions/index.ts +249 -214
  250. package/src/tools/compareTransactions/matcher.ts +259 -209
  251. package/src/tools/compareTransactions/parser.ts +517 -487
  252. package/src/tools/compareTransactions/types.ts +38 -38
  253. package/src/tools/compareTransactions.ts +1 -1
  254. package/src/tools/deltaFetcher.ts +281 -260
  255. package/src/tools/deltaSupport.ts +264 -259
  256. package/src/tools/exportTransactions.ts +230 -218
  257. package/src/tools/monthTools.ts +180 -165
  258. package/src/tools/payeeTools.ts +152 -140
  259. package/src/tools/reconcileAdapter.ts +297 -246
  260. package/src/tools/reconciliation/CLAUDE.md +506 -0
  261. package/src/tools/reconciliation/__tests__/adapter.causes.test.ts +135 -112
  262. package/src/tools/reconciliation/__tests__/adapter.test.ts +249 -227
  263. package/src/tools/reconciliation/__tests__/analyzer.test.ts +408 -335
  264. package/src/tools/reconciliation/__tests__/csvParser.test.ts +71 -69
  265. package/src/tools/reconciliation/__tests__/executor.integration.test.ts +348 -323
  266. package/src/tools/reconciliation/__tests__/executor.progress.test.ts +503 -457
  267. package/src/tools/reconciliation/__tests__/executor.test.ts +898 -831
  268. package/src/tools/reconciliation/__tests__/matcher.test.ts +667 -663
  269. package/src/tools/reconciliation/__tests__/payeeNormalizer.test.ts +296 -276
  270. package/src/tools/reconciliation/__tests__/recommendationEngine.integration.test.ts +692 -624
  271. package/src/tools/reconciliation/__tests__/recommendationEngine.test.ts +1008 -986
  272. package/src/tools/reconciliation/__tests__/reconciliation.delta.integration.test.ts +187 -146
  273. package/src/tools/reconciliation/__tests__/reportFormatter.test.ts +583 -530
  274. package/src/tools/reconciliation/__tests__/scenarios/adapterCurrency.scenario.test.ts +75 -71
  275. package/src/tools/reconciliation/__tests__/scenarios/extremes.scenario.test.ts +70 -58
  276. package/src/tools/reconciliation/__tests__/scenarios/repeatAmount.scenario.test.ts +102 -88
  277. package/src/tools/reconciliation/__tests__/schemaUrl.test.ts +58 -43
  278. package/src/tools/reconciliation/__tests__/signDetector.test.ts +209 -206
  279. package/src/tools/reconciliation/__tests__/ynabAdapter.test.ts +66 -60
  280. package/src/tools/reconciliation/analyzer.ts +582 -406
  281. package/src/tools/reconciliation/csvParser.ts +656 -609
  282. package/src/tools/reconciliation/executor.ts +1290 -1128
  283. package/src/tools/reconciliation/index.ts +580 -528
  284. package/src/tools/reconciliation/matcher.ts +256 -240
  285. package/src/tools/reconciliation/payeeNormalizer.ts +92 -78
  286. package/src/tools/reconciliation/recommendationEngine.ts +357 -345
  287. package/src/tools/reconciliation/reportFormatter.ts +349 -276
  288. package/src/tools/reconciliation/signDetector.ts +89 -83
  289. package/src/tools/reconciliation/types.ts +164 -153
  290. package/src/tools/reconciliation/ynabAdapter.ts +17 -15
  291. package/src/tools/schemas/CLAUDE.md +546 -0
  292. package/src/tools/schemas/common.ts +1 -1
  293. package/src/tools/schemas/outputs/__tests__/accountOutputs.test.ts +410 -409
  294. package/src/tools/schemas/outputs/__tests__/budgetOutputs.test.ts +305 -299
  295. package/src/tools/schemas/outputs/__tests__/categoryOutputs.test.ts +431 -430
  296. package/src/tools/schemas/outputs/__tests__/comparisonOutputs.test.ts +510 -495
  297. package/src/tools/schemas/outputs/__tests__/dateValidation.test.ts +179 -153
  298. package/src/tools/schemas/outputs/__tests__/discrepancyDirection.test.ts +293 -254
  299. package/src/tools/schemas/outputs/__tests__/monthOutputs.test.ts +457 -457
  300. package/src/tools/schemas/outputs/__tests__/payeeOutputs.test.ts +362 -356
  301. package/src/tools/schemas/outputs/__tests__/reconciliationOutputs.test.ts +402 -399
  302. package/src/tools/schemas/outputs/__tests__/transactionMutationSchemas.test.ts +225 -211
  303. package/src/tools/schemas/outputs/__tests__/transactionOutputs.test.ts +457 -454
  304. package/src/tools/schemas/outputs/__tests__/utilityOutputs.test.ts +316 -315
  305. package/src/tools/schemas/outputs/accountOutputs.ts +40 -34
  306. package/src/tools/schemas/outputs/budgetOutputs.ts +24 -19
  307. package/src/tools/schemas/outputs/categoryOutputs.ts +76 -56
  308. package/src/tools/schemas/outputs/comparisonOutputs.ts +192 -169
  309. package/src/tools/schemas/outputs/index.ts +163 -163
  310. package/src/tools/schemas/outputs/monthOutputs.ts +95 -80
  311. package/src/tools/schemas/outputs/payeeOutputs.ts +18 -18
  312. package/src/tools/schemas/outputs/reconciliationOutputs.ts +386 -373
  313. package/src/tools/schemas/outputs/transactionMutationOutputs.ts +259 -231
  314. package/src/tools/schemas/outputs/transactionOutputs.ts +81 -71
  315. package/src/tools/schemas/outputs/utilityOutputs.ts +90 -84
  316. package/src/tools/schemas/shared/commonOutputs.ts +27 -19
  317. package/src/tools/toolCategories.ts +114 -114
  318. package/src/tools/transactionReadTools.ts +327 -0
  319. package/src/tools/transactionSchemas.ts +484 -0
  320. package/src/tools/transactionTools.ts +107 -2990
  321. package/src/tools/transactionUtils.ts +621 -0
  322. package/src/tools/transactionWriteTools.ts +2110 -0
  323. package/src/tools/utilityTools.ts +46 -41
  324. package/src/types/CLAUDE.md +477 -0
  325. package/src/types/__tests__/index.test.ts +51 -51
  326. package/src/types/index.ts +43 -39
  327. package/src/types/integration-tests.d.ts +26 -26
  328. package/src/types/reconciliation.ts +29 -29
  329. package/src/types/toolAnnotations.ts +30 -30
  330. package/src/types/toolRegistration.ts +43 -32
  331. package/src/utils/CLAUDE.md +508 -0
  332. package/src/utils/__tests__/dateUtils.test.ts +174 -168
  333. package/src/utils/__tests__/money.test.ts +193 -187
  334. package/src/utils/amountUtils.ts +5 -5
  335. package/src/utils/baseError.ts +5 -5
  336. package/src/utils/dateUtils.ts +29 -26
  337. package/src/utils/errors.ts +14 -14
  338. package/src/utils/money.ts +66 -52
  339. package/src/utils/validationError.ts +1 -1
  340. package/tsconfig.json +29 -29
  341. package/tsconfig.prod.json +16 -16
  342. package/vitest-reporters/split-json-reporter.ts +247 -204
  343. package/vitest.config.ts +99 -95
  344. package/.prettierignore +0 -10
  345. package/.prettierrc.json +0 -10
  346. package/eslint.config.js +0 -49
@@ -2,425 +2,491 @@
2
2
  * Integration tests for security measures
3
3
  */
4
4
 
5
- import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
6
- import { z } from 'zod/v4';
7
- import { SecurityMiddleware } from '../securityMiddleware.js';
8
- import { globalRateLimiter, RateLimiter } from '../rateLimiter.js';
9
- import { globalRequestLogger, RequestLogger } from '../requestLogger.js';
10
-
11
- describe('Security Integration', () => {
12
- const testAccessToken = 'integration-test-token-123';
13
- const testSchema = z.object({
14
- budget_id: z.string().min(1),
15
- amount: z.number().optional(),
16
- });
17
-
18
- beforeEach(() => {
19
- // Reset all security components
20
- SecurityMiddleware.reset();
21
-
22
- // Configure rate limiter for testing
23
- globalRateLimiter.config = {
24
- maxRequests: 3,
25
- windowMs: 1000,
26
- enableLogging: false,
27
- };
28
- });
29
-
30
- afterEach(() => {
31
- vi.restoreAllMocks();
32
- });
33
-
34
- describe('end-to-end security flow', () => {
35
- it(
36
- 'should handle a complete successful request flow',
37
- { meta: { tier: 'domain', domain: 'security' } },
38
- async () => {
39
- const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {
40
- // Mock implementation for testing
41
- });
42
-
43
- const context = {
44
- accessToken: testAccessToken,
45
- toolName: 'ynab:create_transaction',
46
- operation: 'creating transaction',
47
- parameters: { budget_id: 'test-budget-123', amount: 1000 },
48
- startTime: Date.now(),
49
- };
50
-
51
- const mockOperation = vi.fn().mockResolvedValue({
52
- content: [{ type: 'text', text: JSON.stringify({ success: true }) }],
53
- });
54
-
55
- // Execute the operation
56
- const result = await SecurityMiddleware.withSecurity(context, testSchema, mockOperation);
57
-
58
- // Verify operation was called with validated parameters
59
- expect(mockOperation).toHaveBeenCalledWith({
60
- budget_id: 'test-budget-123',
61
- amount: 1000,
62
- });
63
-
64
- // Verify successful result
65
- expect(result.content[0].text).toContain('success');
66
-
67
- // Verify logging occurred
68
- const logs = globalRequestLogger.getRecentLogs(1);
69
- expect(logs).toHaveLength(1);
70
- expect(logs[0].toolName).toBe('ynab:create_transaction');
71
- expect(logs[0].success).toBe(true);
72
- expect(logs[0].parameters.budget_id).toBe('test-budget-123');
73
- expect(logs[0].parameters.amount).toBe(1000);
74
-
75
- // Verify rate limiting is tracking
76
- expect(logs[0].rateLimitInfo.remaining).toBe(2); // Started with 3, used 1
77
-
78
- // Verify console logging (should show success)
79
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('[INFO]'));
80
- expect(consoleSpy).toHaveBeenCalledWith(
81
- expect.stringContaining('ynab:create_transaction:creating transaction | SUCCESS'),
82
- );
83
-
84
- consoleSpy.mockRestore();
85
- },
86
- );
87
-
88
- it(
89
- 'should handle validation failures with proper logging',
90
- { meta: { tier: 'domain', domain: 'security' } },
91
- async () => {
92
- const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {
93
- // Mock implementation for testing
94
- });
95
-
96
- const context = {
97
- accessToken: testAccessToken,
98
- toolName: 'ynab:create_transaction',
99
- operation: 'creating transaction',
100
- parameters: { budget_id: '', amount: 'invalid' }, // Invalid parameters
101
- startTime: Date.now(),
102
- };
103
-
104
- const mockOperation = vi.fn();
105
-
106
- const result = await SecurityMiddleware.withSecurity(context, testSchema, mockOperation);
107
-
108
- // Verify operation was not called
109
- expect(mockOperation).not.toHaveBeenCalled();
110
-
111
- // Verify error response
112
- const responseText = JSON.parse(result.content[0].text);
113
- expect(responseText.error.code).toBe('VALIDATION_ERROR');
114
- expect(responseText.error.message).toContain('Invalid parameters');
115
-
116
- // Verify error logging
117
- const logs = globalRequestLogger.getRecentLogs(1);
118
- expect(logs).toHaveLength(1);
119
- expect(logs[0].success).toBe(false);
120
- expect(logs[0].error).toContain('Validation failed');
121
-
122
- // Verify console error logging
123
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('[ERROR]'));
124
-
125
- consoleSpy.mockRestore();
126
- },
127
- );
128
-
129
- it(
130
- 'should handle rate limiting with proper responses and logging',
131
- { meta: { tier: 'domain', domain: 'security' } },
132
- async () => {
133
- const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {
134
- // Mock implementation for testing
135
- });
136
-
137
- const context = {
138
- accessToken: testAccessToken,
139
- toolName: 'ynab:list_budgets',
140
- operation: 'listing budgets',
141
- parameters: { budget_id: 'test-budget' },
142
- startTime: Date.now(),
143
- };
144
-
145
- const mockOperation = vi.fn().mockResolvedValue({
146
- content: [{ type: 'text', text: 'Success' }],
147
- });
148
-
149
- // Make requests up to the limit (3)
150
- await SecurityMiddleware.withSecurity(context, testSchema, mockOperation);
151
- await SecurityMiddleware.withSecurity(context, testSchema, mockOperation);
152
- await SecurityMiddleware.withSecurity(context, testSchema, mockOperation);
153
-
154
- // Fourth request should be rate limited
155
- const result = await SecurityMiddleware.withSecurity(context, testSchema, mockOperation);
156
-
157
- // Verify rate limit response
158
- const responseText = JSON.parse(result.content[0].text);
159
- expect(responseText.error.code).toBe('RATE_LIMIT_EXCEEDED');
160
- expect(responseText.error.message).toContain('Rate limit exceeded');
161
- expect(responseText.error.details.resetTime).toBeDefined();
162
- expect(responseText.error.details.remaining).toBe(0);
163
-
164
- // Verify all requests were logged
165
- const logs = globalRequestLogger.getRecentLogs(4);
166
- expect(logs).toHaveLength(4);
167
-
168
- // First 3 should be successful
169
- expect(logs[0].success).toBe(true);
170
- expect(logs[1].success).toBe(true);
171
- expect(logs[2].success).toBe(true);
172
-
173
- // Fourth should be failed due to rate limiting
174
- expect(logs[3].success).toBe(false);
175
- expect(logs[3].error).toContain('Rate limit exceeded');
176
-
177
- // Verify rate limit info progression
178
- expect(logs[0].rateLimitInfo.remaining).toBe(2);
179
- expect(logs[1].rateLimitInfo.remaining).toBe(1);
180
- expect(logs[2].rateLimitInfo.remaining).toBe(0);
181
- expect(logs[3].rateLimitInfo.isLimited).toBe(true);
182
-
183
- consoleSpy.mockRestore();
184
- },
185
- );
186
-
187
- it(
188
- 'should sanitize sensitive data in logs',
189
- { meta: { tier: 'domain', domain: 'security' } },
190
- async () => {
191
- const context = {
192
- accessToken: testAccessToken,
193
- toolName: 'ynab:create_transaction',
194
- operation: 'creating transaction',
195
- parameters: {
196
- budget_id: 'test-budget',
197
- memo: 'Payment with token=secret123456789',
198
- access_token: 'very-secret-token-abc123',
199
- api_key: 'secret-api-key-xyz789',
200
- },
201
- startTime: Date.now(),
202
- };
203
-
204
- const extendedSchema = z.object({
205
- budget_id: z.string(),
206
- memo: z.string().optional(),
207
- access_token: z.string().optional(),
208
- api_key: z.string().optional(),
209
- });
210
-
211
- const mockOperation = vi.fn().mockResolvedValue({
212
- content: [{ type: 'text', text: 'Success' }],
213
- });
214
-
215
- await SecurityMiddleware.withSecurity(context, extendedSchema, mockOperation);
216
-
217
- const logs = globalRequestLogger.getRecentLogs(1);
218
- const loggedParams = logs[0].parameters;
219
-
220
- // Verify sensitive data is sanitized
221
- expect(loggedParams.budget_id).toBe('test-budget'); // Non-sensitive data preserved
222
- expect(loggedParams.memo).toBe('Payment with token=***'); // Token pattern sanitized
223
- expect(loggedParams.access_token).toBe('***'); // Sensitive parameter name sanitized
224
- expect(loggedParams.api_key).toBe('***'); // Sensitive parameter name sanitized
225
- },
226
- );
227
-
228
- it(
229
- 'should handle multiple users with independent rate limits',
230
- { meta: { tier: 'domain', domain: 'security' } },
231
- async () => {
232
- const token1 = 'user-1-token';
233
- const token2 = 'user-2-token';
234
-
235
- const context1 = {
236
- accessToken: token1,
237
- toolName: 'ynab:test',
238
- operation: 'test',
239
- parameters: { budget_id: 'budget-1' },
240
- startTime: Date.now(),
241
- };
242
-
243
- const context2 = {
244
- accessToken: token2,
245
- toolName: 'ynab:test',
246
- operation: 'test',
247
- parameters: { budget_id: 'budget-2' },
248
- startTime: Date.now(),
249
- };
250
-
251
- const mockOperation = vi.fn().mockResolvedValue({
252
- content: [{ type: 'text', text: 'Success' }],
253
- });
254
-
255
- // Max out user 1's rate limit
256
- await SecurityMiddleware.withSecurity(context1, testSchema, mockOperation);
257
- await SecurityMiddleware.withSecurity(context1, testSchema, mockOperation);
258
- await SecurityMiddleware.withSecurity(context1, testSchema, mockOperation);
259
-
260
- // User 1 should be rate limited
261
- const user1Result = await SecurityMiddleware.withSecurity(
262
- context1,
263
- testSchema,
264
- mockOperation,
265
- );
266
- const user1Response = JSON.parse(user1Result.content[0].text);
267
- expect(user1Response.error.code).toBe('RATE_LIMIT_EXCEEDED');
268
-
269
- // User 2 should still be allowed
270
- const user2Result = await SecurityMiddleware.withSecurity(
271
- context2,
272
- testSchema,
273
- mockOperation,
274
- );
275
- expect(user2Result.content[0].text).toBe('Success');
276
-
277
- // Verify independent tracking in logs
278
- const logs = globalRequestLogger.getRecentLogs(5);
279
- const user1Logs = logs.filter((log) => log.parameters.budget_id === 'budget-1');
280
- const user2Logs = logs.filter((log) => log.parameters.budget_id === 'budget-2');
281
-
282
- expect(user1Logs).toHaveLength(4); // 3 successful + 1 rate limited
283
- expect(user2Logs).toHaveLength(1); // 1 successful
284
-
285
- // User 2 should have full rate limit remaining
286
- expect(user2Logs[0].rateLimitInfo.remaining).toBe(2);
287
- },
288
- );
289
-
290
- it(
291
- 'should provide comprehensive security statistics',
292
- { meta: { tier: 'domain', domain: 'security' } },
293
- async () => {
294
- const context = {
295
- accessToken: testAccessToken,
296
- toolName: 'ynab:test',
297
- operation: 'test',
298
- parameters: { budget_id: 'test-budget' },
299
- startTime: Date.now(),
300
- };
301
-
302
- const mockSuccessOperation = vi.fn().mockResolvedValue({
303
- content: [{ type: 'text', text: 'Success' }],
304
- });
305
-
306
- const mockFailOperation = vi.fn().mockRejectedValue(new Error('Test error'));
307
-
308
- // Make some successful requests
309
- await SecurityMiddleware.withSecurity(context, testSchema, mockSuccessOperation);
310
- await SecurityMiddleware.withSecurity(context, testSchema, mockSuccessOperation);
311
-
312
- // Make a failed request
313
- try {
314
- await SecurityMiddleware.withSecurity(context, testSchema, mockFailOperation);
315
- } catch {
316
- // Expected
317
- }
318
-
319
- const stats = SecurityMiddleware.getSecurityStats();
320
-
321
- expect(stats.rateLimitStats).toBeDefined();
322
- expect(stats.requestStats).toBeDefined();
323
- expect(stats.requestStats.totalRequests).toBe(3);
324
- expect(stats.requestStats.successfulRequests).toBe(2);
325
- expect(stats.requestStats.failedRequests).toBe(1);
326
- expect(stats.requestStats.averageDuration).toBeGreaterThanOrEqual(0);
327
- expect(stats.requestStats.toolUsage['ynab:test']).toBe(3);
328
- },
329
- );
330
- });
331
-
332
- describe('performance under load', () => {
333
- it(
334
- 'should handle rapid requests efficiently',
335
- { meta: { tier: 'domain', domain: 'security' } },
336
- async () => {
337
- const context = {
338
- accessToken: testAccessToken,
339
- toolName: 'ynab:performance_test',
340
- operation: 'performance test',
341
- parameters: { budget_id: 'test-budget' },
342
- startTime: Date.now(),
343
- };
344
-
345
- const mockOperation = vi.fn().mockResolvedValue({
346
- content: [{ type: 'text', text: 'Success' }],
347
- });
348
-
349
- const startTime = Date.now();
350
-
351
- // Make multiple rapid requests (within rate limit)
352
- const promises = [];
353
- for (let i = 0; i < 3; i++) {
354
- promises.push(SecurityMiddleware.withSecurity(context, testSchema, mockOperation));
355
- }
356
-
357
- await Promise.all(promises);
358
-
359
- const endTime = Date.now();
360
- const totalTime = endTime - startTime;
361
-
362
- // Should complete reasonably quickly (less than 1 second for 3 requests)
363
- expect(totalTime).toBeLessThan(1000);
364
-
365
- // Verify all requests were processed
366
- expect(mockOperation).toHaveBeenCalledTimes(3);
367
-
368
- const logs = globalRequestLogger.getRecentLogs(3);
369
- expect(logs).toHaveLength(3);
370
- expect(logs.every((log) => log.success)).toBe(true);
371
- },
372
- );
373
- });
374
-
375
- describe('cleanup and maintenance', () => {
376
- it(
377
- 'should clean up expired rate limit entries',
378
- { meta: { tier: 'domain', domain: 'security' } },
379
- async () => {
380
- // Use a very short window for testing
381
- const testLimiter = new RateLimiter({
382
- maxRequests: 5,
383
- windowMs: 50, // 50ms
384
- enableLogging: false,
385
- });
386
-
387
- const testToken = 'cleanup-test-token';
388
-
389
- // Make some requests
390
- testLimiter.recordRequest(testToken);
391
- testLimiter.recordRequest(testToken);
392
-
393
- expect(testLimiter.getStatus(testToken).remaining).toBe(3);
394
-
395
- // Wait for window to expire
396
- await new Promise((resolve) => setTimeout(resolve, 60));
397
-
398
- // Cleanup
399
- testLimiter.cleanup();
400
-
401
- // Should have full capacity again
402
- expect(testLimiter.getStatus(testToken).remaining).toBe(5);
403
- },
404
- );
405
-
406
- it(
407
- 'should maintain log size limits',
408
- { meta: { tier: 'domain', domain: 'security' } },
409
- async () => {
410
- const testLogger = new RequestLogger({ maxLogEntries: 2 });
411
-
412
- // Add more logs than the limit
413
- testLogger.logSuccess('tool1', 'op1', {});
414
- testLogger.logSuccess('tool2', 'op2', {});
415
- testLogger.logSuccess('tool3', 'op3', {});
416
-
417
- const logs = testLogger.getRecentLogs();
418
- expect(logs).toHaveLength(2);
419
-
420
- // Should keep the most recent entries
421
- expect(logs[0].toolName).toBe('tool2');
422
- expect(logs[1].toolName).toBe('tool3');
423
- },
424
- );
425
- });
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
+ import { z } from "zod/v4";
7
+ import { RateLimiter, globalRateLimiter } from "../rateLimiter.js";
8
+ import { RequestLogger, globalRequestLogger } from "../requestLogger.js";
9
+ import { SecurityMiddleware } from "../securityMiddleware.js";
10
+
11
+ describe("Security Integration", () => {
12
+ const testAccessToken = "integration-test-token-123";
13
+ const testSchema = z.object({
14
+ budget_id: z.string().min(1),
15
+ amount: z.number().optional(),
16
+ });
17
+
18
+ beforeEach(() => {
19
+ // Reset all security components
20
+ SecurityMiddleware.reset();
21
+
22
+ // Configure rate limiter for testing
23
+ globalRateLimiter.config = {
24
+ maxRequests: 3,
25
+ windowMs: 1000,
26
+ enableLogging: false,
27
+ };
28
+ });
29
+
30
+ afterEach(() => {
31
+ vi.restoreAllMocks();
32
+ });
33
+
34
+ describe("end-to-end security flow", () => {
35
+ it(
36
+ "should handle a complete successful request flow",
37
+ { meta: { tier: "domain", domain: "security" } },
38
+ async () => {
39
+ const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {
40
+ // Mock implementation for testing
41
+ });
42
+
43
+ const context = {
44
+ accessToken: testAccessToken,
45
+ toolName: "ynab:create_transaction",
46
+ operation: "creating transaction",
47
+ parameters: { budget_id: "test-budget-123", amount: 1000 },
48
+ startTime: Date.now(),
49
+ };
50
+
51
+ const mockOperation = vi.fn().mockResolvedValue({
52
+ content: [{ type: "text", text: JSON.stringify({ success: true }) }],
53
+ });
54
+
55
+ // Execute the operation
56
+ const result = await SecurityMiddleware.withSecurity(
57
+ context,
58
+ testSchema,
59
+ mockOperation,
60
+ );
61
+
62
+ // Verify operation was called with validated parameters
63
+ expect(mockOperation).toHaveBeenCalledWith({
64
+ budget_id: "test-budget-123",
65
+ amount: 1000,
66
+ });
67
+
68
+ // Verify successful result
69
+ expect(result.content[0].text).toContain("success");
70
+
71
+ // Verify logging occurred
72
+ const logs = globalRequestLogger.getRecentLogs(1);
73
+ expect(logs).toHaveLength(1);
74
+ expect(logs[0].toolName).toBe("ynab:create_transaction");
75
+ expect(logs[0].success).toBe(true);
76
+ expect(logs[0].parameters.budget_id).toBe("test-budget-123");
77
+ expect(logs[0].parameters.amount).toBe(1000);
78
+
79
+ // Verify rate limiting is tracking
80
+ expect(logs[0].rateLimitInfo.remaining).toBe(2); // Started with 3, used 1
81
+
82
+ // Verify console logging (should show success)
83
+ expect(consoleSpy).toHaveBeenCalledWith(
84
+ expect.stringContaining("[INFO]"),
85
+ );
86
+ expect(consoleSpy).toHaveBeenCalledWith(
87
+ expect.stringContaining(
88
+ "ynab:create_transaction:creating transaction | SUCCESS",
89
+ ),
90
+ );
91
+
92
+ consoleSpy.mockRestore();
93
+ },
94
+ );
95
+
96
+ it(
97
+ "should handle validation failures with proper logging",
98
+ { meta: { tier: "domain", domain: "security" } },
99
+ async () => {
100
+ const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {
101
+ // Mock implementation for testing
102
+ });
103
+
104
+ const context = {
105
+ accessToken: testAccessToken,
106
+ toolName: "ynab:create_transaction",
107
+ operation: "creating transaction",
108
+ parameters: { budget_id: "", amount: "invalid" }, // Invalid parameters
109
+ startTime: Date.now(),
110
+ };
111
+
112
+ const mockOperation = vi.fn();
113
+
114
+ const result = await SecurityMiddleware.withSecurity(
115
+ context,
116
+ testSchema,
117
+ mockOperation,
118
+ );
119
+
120
+ // Verify operation was not called
121
+ expect(mockOperation).not.toHaveBeenCalled();
122
+
123
+ // Verify error response
124
+ const responseText = JSON.parse(result.content[0].text);
125
+ expect(responseText.error.code).toBe("VALIDATION_ERROR");
126
+ expect(responseText.error.message).toContain("Invalid parameters");
127
+
128
+ // Verify error logging
129
+ const logs = globalRequestLogger.getRecentLogs(1);
130
+ expect(logs).toHaveLength(1);
131
+ expect(logs[0].success).toBe(false);
132
+ expect(logs[0].error).toContain("Validation failed");
133
+
134
+ // Verify console error logging
135
+ expect(consoleSpy).toHaveBeenCalledWith(
136
+ expect.stringContaining("[ERROR]"),
137
+ );
138
+
139
+ consoleSpy.mockRestore();
140
+ },
141
+ );
142
+
143
+ it(
144
+ "should handle rate limiting with proper responses and logging",
145
+ { meta: { tier: "domain", domain: "security" } },
146
+ async () => {
147
+ const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {
148
+ // Mock implementation for testing
149
+ });
150
+
151
+ const context = {
152
+ accessToken: testAccessToken,
153
+ toolName: "ynab:list_budgets",
154
+ operation: "listing budgets",
155
+ parameters: { budget_id: "test-budget" },
156
+ startTime: Date.now(),
157
+ };
158
+
159
+ const mockOperation = vi.fn().mockResolvedValue({
160
+ content: [{ type: "text", text: "Success" }],
161
+ });
162
+
163
+ // Make requests up to the limit (3)
164
+ await SecurityMiddleware.withSecurity(
165
+ context,
166
+ testSchema,
167
+ mockOperation,
168
+ );
169
+ await SecurityMiddleware.withSecurity(
170
+ context,
171
+ testSchema,
172
+ mockOperation,
173
+ );
174
+ await SecurityMiddleware.withSecurity(
175
+ context,
176
+ testSchema,
177
+ mockOperation,
178
+ );
179
+
180
+ // Fourth request should be rate limited
181
+ const result = await SecurityMiddleware.withSecurity(
182
+ context,
183
+ testSchema,
184
+ mockOperation,
185
+ );
186
+
187
+ // Verify rate limit response
188
+ const responseText = JSON.parse(result.content[0].text);
189
+ expect(responseText.error.code).toBe("RATE_LIMIT_EXCEEDED");
190
+ expect(responseText.error.message).toContain("Rate limit exceeded");
191
+ expect(responseText.error.details.resetTime).toBeDefined();
192
+ expect(responseText.error.details.remaining).toBe(0);
193
+
194
+ // Verify all requests were logged
195
+ const logs = globalRequestLogger.getRecentLogs(4);
196
+ expect(logs).toHaveLength(4);
197
+
198
+ // First 3 should be successful
199
+ expect(logs[0].success).toBe(true);
200
+ expect(logs[1].success).toBe(true);
201
+ expect(logs[2].success).toBe(true);
202
+
203
+ // Fourth should be failed due to rate limiting
204
+ expect(logs[3].success).toBe(false);
205
+ expect(logs[3].error).toContain("Rate limit exceeded");
206
+
207
+ // Verify rate limit info progression
208
+ expect(logs[0].rateLimitInfo.remaining).toBe(2);
209
+ expect(logs[1].rateLimitInfo.remaining).toBe(1);
210
+ expect(logs[2].rateLimitInfo.remaining).toBe(0);
211
+ expect(logs[3].rateLimitInfo.isLimited).toBe(true);
212
+
213
+ consoleSpy.mockRestore();
214
+ },
215
+ );
216
+
217
+ it(
218
+ "should sanitize sensitive data in logs",
219
+ { meta: { tier: "domain", domain: "security" } },
220
+ async () => {
221
+ const context = {
222
+ accessToken: testAccessToken,
223
+ toolName: "ynab:create_transaction",
224
+ operation: "creating transaction",
225
+ parameters: {
226
+ budget_id: "test-budget",
227
+ memo: "Payment with token=secret123456789",
228
+ access_token: "very-secret-token-abc123",
229
+ api_key: "secret-api-key-xyz789",
230
+ },
231
+ startTime: Date.now(),
232
+ };
233
+
234
+ const extendedSchema = z.object({
235
+ budget_id: z.string(),
236
+ memo: z.string().optional(),
237
+ access_token: z.string().optional(),
238
+ api_key: z.string().optional(),
239
+ });
240
+
241
+ const mockOperation = vi.fn().mockResolvedValue({
242
+ content: [{ type: "text", text: "Success" }],
243
+ });
244
+
245
+ await SecurityMiddleware.withSecurity(
246
+ context,
247
+ extendedSchema,
248
+ mockOperation,
249
+ );
250
+
251
+ const logs = globalRequestLogger.getRecentLogs(1);
252
+ const loggedParams = logs[0].parameters;
253
+
254
+ // Verify sensitive data is sanitized
255
+ expect(loggedParams.budget_id).toBe("test-budget"); // Non-sensitive data preserved
256
+ expect(loggedParams.memo).toBe("Payment with token=***"); // Token pattern sanitized
257
+ expect(loggedParams.access_token).toBe("***"); // Sensitive parameter name sanitized
258
+ expect(loggedParams.api_key).toBe("***"); // Sensitive parameter name sanitized
259
+ },
260
+ );
261
+
262
+ it(
263
+ "should handle multiple users with independent rate limits",
264
+ { meta: { tier: "domain", domain: "security" } },
265
+ async () => {
266
+ const token1 = "user-1-token";
267
+ const token2 = "user-2-token";
268
+
269
+ const context1 = {
270
+ accessToken: token1,
271
+ toolName: "ynab:test",
272
+ operation: "test",
273
+ parameters: { budget_id: "budget-1" },
274
+ startTime: Date.now(),
275
+ };
276
+
277
+ const context2 = {
278
+ accessToken: token2,
279
+ toolName: "ynab:test",
280
+ operation: "test",
281
+ parameters: { budget_id: "budget-2" },
282
+ startTime: Date.now(),
283
+ };
284
+
285
+ const mockOperation = vi.fn().mockResolvedValue({
286
+ content: [{ type: "text", text: "Success" }],
287
+ });
288
+
289
+ // Max out user 1's rate limit
290
+ await SecurityMiddleware.withSecurity(
291
+ context1,
292
+ testSchema,
293
+ mockOperation,
294
+ );
295
+ await SecurityMiddleware.withSecurity(
296
+ context1,
297
+ testSchema,
298
+ mockOperation,
299
+ );
300
+ await SecurityMiddleware.withSecurity(
301
+ context1,
302
+ testSchema,
303
+ mockOperation,
304
+ );
305
+
306
+ // User 1 should be rate limited
307
+ const user1Result = await SecurityMiddleware.withSecurity(
308
+ context1,
309
+ testSchema,
310
+ mockOperation,
311
+ );
312
+ const user1Response = JSON.parse(user1Result.content[0].text);
313
+ expect(user1Response.error.code).toBe("RATE_LIMIT_EXCEEDED");
314
+
315
+ // User 2 should still be allowed
316
+ const user2Result = await SecurityMiddleware.withSecurity(
317
+ context2,
318
+ testSchema,
319
+ mockOperation,
320
+ );
321
+ expect(user2Result.content[0].text).toBe("Success");
322
+
323
+ // Verify independent tracking in logs
324
+ const logs = globalRequestLogger.getRecentLogs(5);
325
+ const user1Logs = logs.filter(
326
+ (log) => log.parameters.budget_id === "budget-1",
327
+ );
328
+ const user2Logs = logs.filter(
329
+ (log) => log.parameters.budget_id === "budget-2",
330
+ );
331
+
332
+ expect(user1Logs).toHaveLength(4); // 3 successful + 1 rate limited
333
+ expect(user2Logs).toHaveLength(1); // 1 successful
334
+
335
+ // User 2 should have full rate limit remaining
336
+ expect(user2Logs[0].rateLimitInfo.remaining).toBe(2);
337
+ },
338
+ );
339
+
340
+ it(
341
+ "should provide comprehensive security statistics",
342
+ { meta: { tier: "domain", domain: "security" } },
343
+ async () => {
344
+ const context = {
345
+ accessToken: testAccessToken,
346
+ toolName: "ynab:test",
347
+ operation: "test",
348
+ parameters: { budget_id: "test-budget" },
349
+ startTime: Date.now(),
350
+ };
351
+
352
+ const mockSuccessOperation = vi.fn().mockResolvedValue({
353
+ content: [{ type: "text", text: "Success" }],
354
+ });
355
+
356
+ const mockFailOperation = vi
357
+ .fn()
358
+ .mockRejectedValue(new Error("Test error"));
359
+
360
+ // Make some successful requests
361
+ await SecurityMiddleware.withSecurity(
362
+ context,
363
+ testSchema,
364
+ mockSuccessOperation,
365
+ );
366
+ await SecurityMiddleware.withSecurity(
367
+ context,
368
+ testSchema,
369
+ mockSuccessOperation,
370
+ );
371
+
372
+ // Make a failed request
373
+ try {
374
+ await SecurityMiddleware.withSecurity(
375
+ context,
376
+ testSchema,
377
+ mockFailOperation,
378
+ );
379
+ } catch {
380
+ // Expected
381
+ }
382
+
383
+ const stats = SecurityMiddleware.getSecurityStats();
384
+
385
+ expect(stats.rateLimitStats).toBeDefined();
386
+ expect(stats.requestStats).toBeDefined();
387
+ expect(stats.requestStats.totalRequests).toBe(3);
388
+ expect(stats.requestStats.successfulRequests).toBe(2);
389
+ expect(stats.requestStats.failedRequests).toBe(1);
390
+ expect(stats.requestStats.averageDuration).toBeGreaterThanOrEqual(0);
391
+ expect(stats.requestStats.toolUsage["ynab:test"]).toBe(3);
392
+ },
393
+ );
394
+ });
395
+
396
+ describe("performance under load", () => {
397
+ it(
398
+ "should handle rapid requests efficiently",
399
+ { meta: { tier: "domain", domain: "security" } },
400
+ async () => {
401
+ const context = {
402
+ accessToken: testAccessToken,
403
+ toolName: "ynab:performance_test",
404
+ operation: "performance test",
405
+ parameters: { budget_id: "test-budget" },
406
+ startTime: Date.now(),
407
+ };
408
+
409
+ const mockOperation = vi.fn().mockResolvedValue({
410
+ content: [{ type: "text", text: "Success" }],
411
+ });
412
+
413
+ const startTime = Date.now();
414
+
415
+ // Make multiple rapid requests (within rate limit)
416
+ const promises = [];
417
+ for (let i = 0; i < 3; i++) {
418
+ promises.push(
419
+ SecurityMiddleware.withSecurity(context, testSchema, mockOperation),
420
+ );
421
+ }
422
+
423
+ await Promise.all(promises);
424
+
425
+ const endTime = Date.now();
426
+ const totalTime = endTime - startTime;
427
+
428
+ // Should complete reasonably quickly (less than 1 second for 3 requests)
429
+ expect(totalTime).toBeLessThan(1000);
430
+
431
+ // Verify all requests were processed
432
+ expect(mockOperation).toHaveBeenCalledTimes(3);
433
+
434
+ const logs = globalRequestLogger.getRecentLogs(3);
435
+ expect(logs).toHaveLength(3);
436
+ expect(logs.every((log) => log.success)).toBe(true);
437
+ },
438
+ );
439
+ });
440
+
441
+ describe("cleanup and maintenance", () => {
442
+ it(
443
+ "should clean up expired rate limit entries",
444
+ { meta: { tier: "domain", domain: "security" } },
445
+ async () => {
446
+ // Use a very short window for testing
447
+ const testLimiter = new RateLimiter({
448
+ maxRequests: 5,
449
+ windowMs: 50, // 50ms
450
+ enableLogging: false,
451
+ });
452
+
453
+ const testToken = "cleanup-test-token";
454
+
455
+ // Make some requests
456
+ testLimiter.recordRequest(testToken);
457
+ testLimiter.recordRequest(testToken);
458
+
459
+ expect(testLimiter.getStatus(testToken).remaining).toBe(3);
460
+
461
+ // Wait for window to expire
462
+ await new Promise((resolve) => setTimeout(resolve, 60));
463
+
464
+ // Cleanup
465
+ testLimiter.cleanup();
466
+
467
+ // Should have full capacity again
468
+ expect(testLimiter.getStatus(testToken).remaining).toBe(5);
469
+ },
470
+ );
471
+
472
+ it(
473
+ "should maintain log size limits",
474
+ { meta: { tier: "domain", domain: "security" } },
475
+ async () => {
476
+ const testLogger = new RequestLogger({ maxLogEntries: 2 });
477
+
478
+ // Add more logs than the limit
479
+ testLogger.logSuccess("tool1", "op1", {});
480
+ testLogger.logSuccess("tool2", "op2", {});
481
+ testLogger.logSuccess("tool3", "op3", {});
482
+
483
+ const logs = testLogger.getRecentLogs();
484
+ expect(logs).toHaveLength(2);
485
+
486
+ // Should keep the most recent entries
487
+ expect(logs[0].toolName).toBe("tool2");
488
+ expect(logs[1].toolName).toBe("tool3");
489
+ },
490
+ );
491
+ });
426
492
  });