memory-journal-mcp 4.3.0 → 4.4.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 (109) hide show
  1. package/.dockerignore +131 -122
  2. package/.gitattributes +29 -0
  3. package/.github/workflows/docker-publish.yml +1 -1
  4. package/.github/workflows/lint-and-test.yml +1 -2
  5. package/.github/workflows/secrets-scanning.yml +0 -1
  6. package/.github/workflows/security-update.yml +6 -6
  7. package/.vscode/settings.json +17 -15
  8. package/CHANGELOG.md +1065 -11
  9. package/DOCKER_README.md +51 -33
  10. package/Dockerfile +14 -12
  11. package/README.md +68 -33
  12. package/SECURITY.md +225 -220
  13. package/dist/cli.js +7 -0
  14. package/dist/cli.js.map +1 -1
  15. package/dist/constants/ServerInstructions.d.ts +1 -1
  16. package/dist/constants/ServerInstructions.d.ts.map +1 -1
  17. package/dist/constants/ServerInstructions.js +70 -26
  18. package/dist/constants/ServerInstructions.js.map +1 -1
  19. package/dist/constants/icons.d.ts +2 -0
  20. package/dist/constants/icons.d.ts.map +1 -1
  21. package/dist/constants/icons.js +6 -0
  22. package/dist/constants/icons.js.map +1 -1
  23. package/dist/database/SqliteAdapter.d.ts +51 -10
  24. package/dist/database/SqliteAdapter.d.ts.map +1 -1
  25. package/dist/database/SqliteAdapter.js +143 -43
  26. package/dist/database/SqliteAdapter.js.map +1 -1
  27. package/dist/filtering/ToolFilter.d.ts +1 -1
  28. package/dist/filtering/ToolFilter.d.ts.map +1 -1
  29. package/dist/filtering/ToolFilter.js +7 -1
  30. package/dist/filtering/ToolFilter.js.map +1 -1
  31. package/dist/github/GitHubIntegration.d.ts +74 -2
  32. package/dist/github/GitHubIntegration.d.ts.map +1 -1
  33. package/dist/github/GitHubIntegration.js +508 -7
  34. package/dist/github/GitHubIntegration.js.map +1 -1
  35. package/dist/handlers/prompts/index.js +1 -0
  36. package/dist/handlers/prompts/index.js.map +1 -1
  37. package/dist/handlers/resources/index.d.ts.map +1 -1
  38. package/dist/handlers/resources/index.js +257 -13
  39. package/dist/handlers/resources/index.js.map +1 -1
  40. package/dist/handlers/tools/index.d.ts.map +1 -1
  41. package/dist/handlers/tools/index.js +595 -8
  42. package/dist/handlers/tools/index.js.map +1 -1
  43. package/dist/server/McpServer.d.ts +2 -0
  44. package/dist/server/McpServer.d.ts.map +1 -1
  45. package/dist/server/McpServer.js +69 -26
  46. package/dist/server/McpServer.js.map +1 -1
  47. package/dist/types/index.d.ts +97 -0
  48. package/dist/types/index.d.ts.map +1 -1
  49. package/dist/types/index.js.map +1 -1
  50. package/dist/utils/logger.d.ts +1 -0
  51. package/dist/utils/logger.d.ts.map +1 -1
  52. package/dist/utils/logger.js +8 -1
  53. package/dist/utils/logger.js.map +1 -1
  54. package/dist/utils/progress-utils.d.ts +18 -3
  55. package/dist/utils/progress-utils.d.ts.map +1 -1
  56. package/dist/utils/progress-utils.js.map +1 -1
  57. package/dist/utils/security-utils.d.ts +91 -0
  58. package/dist/utils/security-utils.d.ts.map +1 -0
  59. package/dist/utils/security-utils.js +184 -0
  60. package/dist/utils/security-utils.js.map +1 -0
  61. package/dist/vector/VectorSearchManager.d.ts +2 -1
  62. package/dist/vector/VectorSearchManager.d.ts.map +1 -1
  63. package/dist/vector/VectorSearchManager.js +100 -34
  64. package/dist/vector/VectorSearchManager.js.map +1 -1
  65. package/docker-compose.yml +46 -37
  66. package/mcp-config-example.json +0 -2
  67. package/package.json +21 -14
  68. package/releases/v4.3.1.md +69 -0
  69. package/releases/v4.4.0.md +120 -0
  70. package/server.json +3 -3
  71. package/src/cli.ts +11 -0
  72. package/src/constants/ServerInstructions.ts +70 -26
  73. package/src/constants/icons.ts +7 -0
  74. package/src/database/SqliteAdapter.ts +165 -44
  75. package/src/filtering/ToolFilter.ts +7 -1
  76. package/src/github/GitHubIntegration.ts +588 -8
  77. package/src/handlers/prompts/index.ts +1 -0
  78. package/src/handlers/resources/index.ts +318 -12
  79. package/src/handlers/tools/index.ts +686 -13
  80. package/src/server/McpServer.ts +79 -37
  81. package/src/types/index.ts +98 -0
  82. package/src/utils/logger.ts +10 -1
  83. package/src/utils/progress-utils.ts +17 -6
  84. package/src/utils/security-utils.ts +205 -0
  85. package/src/vector/VectorSearchManager.ts +110 -39
  86. package/tests/constants/icons.test.ts +102 -0
  87. package/tests/constants/server-instructions.test.ts +549 -0
  88. package/tests/database/sqlite-adapter.bench.ts +63 -0
  89. package/tests/database/sqlite-adapter.test.ts +555 -0
  90. package/tests/filtering/tool-filter.test.ts +266 -0
  91. package/tests/github/github-integration.test.ts +1024 -0
  92. package/tests/handlers/github-resource-handlers.test.ts +473 -0
  93. package/tests/handlers/github-tool-handlers.test.ts +556 -0
  94. package/tests/handlers/prompt-handlers.test.ts +91 -0
  95. package/tests/handlers/resource-handlers.test.ts +339 -0
  96. package/tests/handlers/tool-handlers.test.ts +497 -0
  97. package/tests/handlers/vector-tool-handlers.test.ts +238 -0
  98. package/tests/security/sql-injection.test.ts +347 -0
  99. package/tests/server/mcp-server.bench.ts +55 -0
  100. package/tests/server/mcp-server.test.ts +675 -0
  101. package/tests/utils/logger.test.ts +180 -0
  102. package/tests/utils/mcp-logger.test.ts +212 -0
  103. package/tests/utils/progress-utils.test.ts +156 -0
  104. package/tests/utils/security-utils.test.ts +82 -0
  105. package/tests/vector/vector-search-manager.test.ts +335 -0
  106. package/tests/vector/vector-search.bench.ts +53 -0
  107. package/vitest.config.ts +15 -0
  108. package/.github/workflows/DOCKER_DEPLOYMENT_SETUP.md +0 -387
  109. package/.github/workflows/dependabot-auto-merge.yml +0 -42
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Vector Tool Handler Tests
3
+ *
4
+ * Tests vector-dependent tools (semantic_search, rebuild_vector_index,
5
+ * add_to_vector_index, get_vector_index_stats) using a mock VectorSearchManager.
6
+ */
7
+
8
+ import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest'
9
+ import { callTool } from '../../src/handlers/tools/index.js'
10
+ import { SqliteAdapter } from '../../src/database/SqliteAdapter.js'
11
+ import type { VectorSearchManager } from '../../src/vector/VectorSearchManager.js'
12
+
13
+ /**
14
+ * Creates a mock VectorSearchManager.
15
+ */
16
+ function createMockVector(overrides: Partial<Record<string, unknown>> = {}): VectorSearchManager {
17
+ const defaults = {
18
+ isInitialized: vi.fn().mockReturnValue(true),
19
+ initialize: vi.fn().mockResolvedValue(undefined),
20
+ search: vi.fn().mockResolvedValue([]),
21
+ addEntry: vi.fn().mockResolvedValue(true),
22
+ removeEntry: vi.fn().mockResolvedValue(true),
23
+ rebuildIndex: vi.fn().mockResolvedValue(5),
24
+ getStats: vi.fn().mockResolvedValue({
25
+ itemCount: 10,
26
+ modelName: 'Xenova/all-MiniLM-L6-v2',
27
+ dimensions: 384,
28
+ }),
29
+ generateEmbedding: vi.fn().mockResolvedValue(new Array(384).fill(0)),
30
+ }
31
+ return { ...defaults, ...overrides } as unknown as VectorSearchManager
32
+ }
33
+
34
+ describe('Vector Tool Handlers', () => {
35
+ let db: SqliteAdapter
36
+ const testDbPath = './test-vector-tools.db'
37
+ let entryId: number
38
+
39
+ beforeAll(async () => {
40
+ db = new SqliteAdapter(testDbPath)
41
+ await db.initialize()
42
+ // Create an entry for testing
43
+ const entry = db.createEntry({
44
+ content: 'Test entry for vector tools',
45
+ entryType: 'personal_reflection',
46
+ tags: ['test'],
47
+ })
48
+ entryId = entry.id
49
+ })
50
+
51
+ afterAll(() => {
52
+ db.close()
53
+ try {
54
+ const fs = require('node:fs')
55
+ if (fs.existsSync(testDbPath)) fs.unlinkSync(testDbPath)
56
+ } catch {
57
+ // Ignore cleanup errors
58
+ }
59
+ })
60
+
61
+ // ========================================================================
62
+ // semantic_search
63
+ // ========================================================================
64
+
65
+ describe('semantic_search', () => {
66
+ it('should return results when vector manager has matches', async () => {
67
+ const vectorManager = createMockVector({
68
+ search: vi.fn().mockResolvedValue([{ entryId, score: 0.85 }]),
69
+ getStats: vi.fn().mockResolvedValue({ itemCount: 10 }),
70
+ })
71
+
72
+ const result = (await callTool(
73
+ 'semantic_search',
74
+ { query: 'test query' },
75
+ db,
76
+ vectorManager
77
+ )) as { query: string; entries: unknown[]; count: number }
78
+
79
+ expect(result.query).toBe('test query')
80
+ expect(result.count).toBe(1)
81
+ expect(result.entries).toHaveLength(1)
82
+ })
83
+
84
+ it('should return empty with hint when index is empty', async () => {
85
+ const vectorManager = createMockVector({
86
+ search: vi.fn().mockResolvedValue([]),
87
+ getStats: vi.fn().mockResolvedValue({ itemCount: 0 }),
88
+ })
89
+
90
+ const result = (await callTool(
91
+ 'semantic_search',
92
+ { query: 'anything' },
93
+ db,
94
+ vectorManager
95
+ )) as { entries: unknown[]; count: number; hint: string }
96
+
97
+ expect(result.count).toBe(0)
98
+ expect(result.hint).toContain('rebuild_vector_index')
99
+ })
100
+
101
+ it('should return hint when no matches above threshold', async () => {
102
+ const vectorManager = createMockVector({
103
+ search: vi.fn().mockResolvedValue([]),
104
+ getStats: vi.fn().mockResolvedValue({ itemCount: 10 }),
105
+ })
106
+
107
+ const result = (await callTool(
108
+ 'semantic_search',
109
+ { query: 'nothing matches' },
110
+ db,
111
+ vectorManager
112
+ )) as { count: number; hint: string }
113
+
114
+ expect(result.count).toBe(0)
115
+ expect(result.hint).toContain('similarity_threshold')
116
+ })
117
+
118
+ it('should suppress hint when hint_on_empty is false', async () => {
119
+ const vectorManager = createMockVector({
120
+ search: vi.fn().mockResolvedValue([]),
121
+ getStats: vi.fn().mockResolvedValue({ itemCount: 0 }),
122
+ })
123
+
124
+ const result = (await callTool(
125
+ 'semantic_search',
126
+ { query: 'query', hint_on_empty: false },
127
+ db,
128
+ vectorManager
129
+ )) as { count: number; hint?: string }
130
+
131
+ expect(result.count).toBe(0)
132
+ expect(result.hint).toBeUndefined()
133
+ })
134
+
135
+ it('should return error when no vector manager', async () => {
136
+ const result = (await callTool('semantic_search', { query: 'query' }, db)) as {
137
+ error: string
138
+ }
139
+
140
+ expect(result.error).toContain('not initialized')
141
+ })
142
+ })
143
+
144
+ // ========================================================================
145
+ // rebuild_vector_index
146
+ // ========================================================================
147
+
148
+ describe('rebuild_vector_index', () => {
149
+ it('should rebuild and return count', async () => {
150
+ const vectorManager = createMockVector()
151
+
152
+ const result = (await callTool('rebuild_vector_index', {}, db, vectorManager)) as {
153
+ success: boolean
154
+ entriesIndexed: number
155
+ }
156
+
157
+ expect(result.success).toBe(true)
158
+ expect(result.entriesIndexed).toBe(5)
159
+ })
160
+
161
+ it('should return error when no vector manager', async () => {
162
+ const result = (await callTool('rebuild_vector_index', {}, db)) as { error: string }
163
+
164
+ expect(result.error).toContain('not available')
165
+ })
166
+ })
167
+
168
+ // ========================================================================
169
+ // add_to_vector_index
170
+ // ========================================================================
171
+
172
+ describe('add_to_vector_index', () => {
173
+ it('should add entry to index', async () => {
174
+ const vectorManager = createMockVector()
175
+
176
+ const result = (await callTool(
177
+ 'add_to_vector_index',
178
+ { entry_id: entryId },
179
+ db,
180
+ vectorManager
181
+ )) as { success: boolean; entryId: number }
182
+
183
+ expect(result.success).toBe(true)
184
+ expect(result.entryId).toBe(entryId)
185
+ })
186
+
187
+ it('should return error for nonexistent entry', async () => {
188
+ const vectorManager = createMockVector()
189
+
190
+ const result = (await callTool(
191
+ 'add_to_vector_index',
192
+ { entry_id: 99999 },
193
+ db,
194
+ vectorManager
195
+ )) as { error: string }
196
+
197
+ expect(result.error).toContain('not found')
198
+ })
199
+
200
+ it('should return error when no vector manager', async () => {
201
+ const result = (await callTool('add_to_vector_index', { entry_id: 1 }, db)) as {
202
+ error: string
203
+ }
204
+
205
+ expect(result.error).toContain('not available')
206
+ })
207
+ })
208
+
209
+ // ========================================================================
210
+ // get_vector_index_stats
211
+ // ========================================================================
212
+
213
+ describe('get_vector_index_stats', () => {
214
+ it('should return stats', async () => {
215
+ const vectorManager = createMockVector()
216
+
217
+ const result = (await callTool('get_vector_index_stats', {}, db, vectorManager)) as {
218
+ available: boolean
219
+ itemCount: number
220
+ modelName: string
221
+ }
222
+
223
+ expect(result.available).toBe(true)
224
+ expect(result.itemCount).toBe(10)
225
+ expect(result.modelName).toBe('Xenova/all-MiniLM-L6-v2')
226
+ })
227
+
228
+ it('should return unavailable when no vector manager', async () => {
229
+ const result = (await callTool('get_vector_index_stats', {}, db)) as {
230
+ available: boolean
231
+ error: string
232
+ }
233
+
234
+ expect(result.available).toBe(false)
235
+ expect(result.error).toContain('not available')
236
+ })
237
+ })
238
+ })
@@ -0,0 +1,347 @@
1
+ /**
2
+ * SQL Injection Security Tests for Memory Journal MCP Server
3
+ *
4
+ * Tests the database layer's resilience against SQL injection attacks.
5
+ * Follows MCP Security Patterns for comprehensive coverage.
6
+ */
7
+
8
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest'
9
+ import { SqliteAdapter } from '../../src/database/SqliteAdapter.js'
10
+ import {
11
+ validateDateFormatPattern,
12
+ sanitizeSearchQuery,
13
+ containsSqlInjection,
14
+ assertNoSqlInjection,
15
+ assertNoPathTraversal,
16
+ InvalidDateFormatError,
17
+ SqlInjectionError,
18
+ PathTraversalError,
19
+ } from '../../src/utils/security-utils.js'
20
+
21
+ // ============================================================================
22
+ // Test Payloads
23
+ // ============================================================================
24
+
25
+ /**
26
+ * Standard SQL injection payloads for testing.
27
+ * These represent common attack vectors.
28
+ */
29
+ const INJECTION_PAYLOADS = [
30
+ // Stacked queries
31
+ "'; DROP TABLE memory_journal; --",
32
+ "test'; DELETE FROM tags; --",
33
+ "1; INSERT INTO memory_journal (content) VALUES ('hacked'); --",
34
+ "x'; UPDATE memory_journal SET content='pwned' WHERE 1=1; --",
35
+
36
+ // Boolean-based
37
+ "' OR '1'='1",
38
+ "' OR 1=1 --",
39
+ "admin' --",
40
+
41
+ // UNION-based
42
+ "' UNION SELECT * FROM sqlite_master --",
43
+ "' UNION ALL SELECT 1,2,3,4,5 --",
44
+
45
+ // Comment injection
46
+ "test' -- this is a comment",
47
+ 'value /* comment */ injection',
48
+
49
+ // SQLite-specific
50
+ "'; ATTACH DATABASE 'malicious.db' AS mal; --",
51
+ "'; load_extension('malicious.so'); --",
52
+ ]
53
+
54
+ /**
55
+ * Safe inputs that should be accepted
56
+ */
57
+ const SAFE_INPUTS = [
58
+ 'Normal search query',
59
+ "It's a valid apostrophe",
60
+ 'SELECT is just a word here',
61
+ 'test@email.com',
62
+ '100% success rate',
63
+ 'user_name with underscore',
64
+ ]
65
+
66
+ // ============================================================================
67
+ // Security Utility Tests
68
+ // ============================================================================
69
+
70
+ describe('Security Utilities', () => {
71
+ describe('validateDateFormatPattern', () => {
72
+ it('should return valid format for day', () => {
73
+ expect(validateDateFormatPattern('day')).toBe('%Y-%m-%d')
74
+ })
75
+
76
+ it('should return valid format for week', () => {
77
+ expect(validateDateFormatPattern('week')).toBe('%Y-W%W')
78
+ })
79
+
80
+ it('should return valid format for month', () => {
81
+ expect(validateDateFormatPattern('month')).toBe('%Y-%m')
82
+ })
83
+
84
+ it('should throw InvalidDateFormatError for invalid values', () => {
85
+ expect(() => validateDateFormatPattern('invalid')).toThrow(InvalidDateFormatError)
86
+ expect(() => validateDateFormatPattern('')).toThrow(InvalidDateFormatError)
87
+ expect(() => validateDateFormatPattern('%Y-%m-%d')).toThrow(InvalidDateFormatError)
88
+ })
89
+
90
+ it('should throw InvalidDateFormatError for injection attempts', () => {
91
+ expect(() => validateDateFormatPattern("'; DROP TABLE --")).toThrow(
92
+ InvalidDateFormatError
93
+ )
94
+ expect(() => validateDateFormatPattern('%Y-%m-%d) --')).toThrow(InvalidDateFormatError)
95
+ })
96
+ })
97
+
98
+ describe('sanitizeSearchQuery', () => {
99
+ it('should escape % characters', () => {
100
+ expect(sanitizeSearchQuery('100%')).toBe('100\\%')
101
+ expect(sanitizeSearchQuery('50% discount')).toBe('50\\% discount')
102
+ })
103
+
104
+ it('should escape _ characters', () => {
105
+ expect(sanitizeSearchQuery('user_name')).toBe('user\\_name')
106
+ expect(sanitizeSearchQuery('test_value_here')).toBe('test\\_value\\_here')
107
+ })
108
+
109
+ it('should escape backslashes', () => {
110
+ expect(sanitizeSearchQuery('path\\to\\file')).toBe('path\\\\to\\\\file')
111
+ })
112
+
113
+ it('should handle combined special characters', () => {
114
+ expect(sanitizeSearchQuery('50%_test\\')).toBe('50\\%\\_test\\\\')
115
+ })
116
+
117
+ it('should not modify safe strings', () => {
118
+ expect(sanitizeSearchQuery('normal text')).toBe('normal text')
119
+ expect(sanitizeSearchQuery("it's fine")).toBe("it's fine")
120
+ })
121
+ })
122
+
123
+ describe('containsSqlInjection', () => {
124
+ it('should detect stacked query injection', () => {
125
+ expect(containsSqlInjection('; DROP TABLE users')).toBe(true)
126
+ expect(containsSqlInjection("'; DELETE FROM logs --")).toBe(true)
127
+ })
128
+
129
+ it('should detect comment injection', () => {
130
+ expect(containsSqlInjection('value -- comment')).toBe(true)
131
+ expect(containsSqlInjection('test /* block */ comment')).toBe(true)
132
+ })
133
+
134
+ it('should detect UNION injection', () => {
135
+ expect(containsSqlInjection("' UNION SELECT * FROM users")).toBe(true)
136
+ expect(containsSqlInjection("' UNION ALL SELECT 1")).toBe(true)
137
+ })
138
+
139
+ it('should detect boolean bypass', () => {
140
+ expect(containsSqlInjection("' OR '1'='1")).toBe(true)
141
+ })
142
+
143
+ it('should detect SQLite-specific attacks', () => {
144
+ expect(containsSqlInjection("ATTACH DATABASE 'mal.db' AS x")).toBe(true)
145
+ expect(containsSqlInjection("load_extension('evil.so')")).toBe(true)
146
+ })
147
+
148
+ it('should not flag safe inputs', () => {
149
+ for (const input of SAFE_INPUTS) {
150
+ expect(containsSqlInjection(input)).toBe(false)
151
+ }
152
+ })
153
+ })
154
+
155
+ describe('assertNoSqlInjection', () => {
156
+ it('should throw SqlInjectionError for injection attempts', () => {
157
+ for (const payload of INJECTION_PAYLOADS) {
158
+ expect(() => assertNoSqlInjection(payload)).toThrow(SqlInjectionError)
159
+ }
160
+ })
161
+
162
+ it('should not throw for safe inputs', () => {
163
+ for (const input of SAFE_INPUTS) {
164
+ expect(() => assertNoSqlInjection(input)).not.toThrow()
165
+ }
166
+ })
167
+ })
168
+
169
+ describe('assertNoPathTraversal', () => {
170
+ it('should throw for path traversal attempts', () => {
171
+ expect(() => assertNoPathTraversal('../secret')).toThrow(PathTraversalError)
172
+ expect(() => assertNoPathTraversal('..\\secret')).toThrow(PathTraversalError)
173
+ expect(() => assertNoPathTraversal('/etc/passwd')).toThrow(PathTraversalError)
174
+ expect(() => assertNoPathTraversal('C:\\Windows')).toThrow(PathTraversalError)
175
+ })
176
+
177
+ it('should not throw for safe filenames', () => {
178
+ expect(() => assertNoPathTraversal('backup.db')).not.toThrow()
179
+ expect(() => assertNoPathTraversal('backup_2026-02-05.db')).not.toThrow()
180
+ })
181
+ })
182
+ })
183
+
184
+ // ============================================================================
185
+ // SqliteAdapter SQL Injection Tests
186
+ // ============================================================================
187
+
188
+ describe('SqliteAdapter SQL Injection Protection', () => {
189
+ let db: SqliteAdapter
190
+ const testDbPath = './test-security.db'
191
+
192
+ beforeAll(async () => {
193
+ // Use temp file database for testing (SqliteAdapter always saves to disk)
194
+ db = new SqliteAdapter(testDbPath)
195
+ await db.initialize()
196
+ })
197
+
198
+ afterAll(() => {
199
+ db.close()
200
+ // Clean up test database
201
+ try {
202
+ const fs = require('node:fs')
203
+ if (fs.existsSync(testDbPath)) {
204
+ fs.unlinkSync(testDbPath)
205
+ }
206
+ } catch {
207
+ // Ignore cleanup errors
208
+ }
209
+ })
210
+
211
+ describe('createEntry - Parameterized Queries', () => {
212
+ it('should safely handle content with SQL injection attempts', async () => {
213
+ for (const payload of INJECTION_PAYLOADS) {
214
+ // Should not throw - parameterized queries handle this
215
+ const entry = db.createEntry({ content: payload })
216
+ expect(entry.content).toBe(payload)
217
+
218
+ // Verify the payload was stored literally, not executed
219
+ const retrieved = db.getEntryById(entry.id)
220
+ expect(retrieved?.content).toBe(payload)
221
+ }
222
+ })
223
+
224
+ it('should safely handle tags with special characters', async () => {
225
+ const entry = db.createEntry({
226
+ content: 'Test entry',
227
+ tags: ["tag'; DROP TABLE tags; --", 'normal-tag', "tag' OR '1'='1"],
228
+ })
229
+
230
+ const tags = entry.tags
231
+ expect(tags).toContain("tag'; DROP TABLE tags; --")
232
+ expect(tags).toContain('normal-tag')
233
+ expect(tags).toContain("tag' OR '1'='1")
234
+ })
235
+ })
236
+
237
+ describe('searchEntries - LIKE Injection', () => {
238
+ it('should safely handle search queries with SQL injection attempts', async () => {
239
+ // Create a known entry
240
+ db.createEntry({ content: 'Known safe content' })
241
+
242
+ for (const payload of INJECTION_PAYLOADS) {
243
+ // Should not throw - parameterized queries handle this
244
+ const results = db.searchEntries(payload)
245
+ // Results should be based on LIKE matching, not SQL execution
246
+ expect(Array.isArray(results)).toBe(true)
247
+ }
248
+ })
249
+
250
+ it('should handle special LIKE characters in queries', async () => {
251
+ // Create entries with special characters
252
+ db.createEntry({ content: '50% discount on items' })
253
+ db.createEntry({ content: 'user_name property' })
254
+
255
+ // Search for literal special characters
256
+ const percentResults = db.searchEntries('50%')
257
+ const underscoreResults = db.searchEntries('user_name')
258
+
259
+ expect(Array.isArray(percentResults)).toBe(true)
260
+ expect(Array.isArray(underscoreResults)).toBe(true)
261
+ })
262
+ })
263
+
264
+ describe('getStatistics - Date Format Validation', () => {
265
+ it('should only accept valid groupBy values', async () => {
266
+ // Valid values should work
267
+ expect(() => db.getStatistics('day')).not.toThrow()
268
+ expect(() => db.getStatistics('week')).not.toThrow()
269
+ expect(() => db.getStatistics('month')).not.toThrow()
270
+ })
271
+
272
+ it('should reject invalid groupBy values', async () => {
273
+ // Type system prevents this at compile time, but runtime validation
274
+ // should also protect against invalid values
275
+ expect(() => db.getStatistics("'; DROP TABLE memory_journal; --" as 'day')).toThrow()
276
+ })
277
+ })
278
+
279
+ describe('updateEntry - Parameterized Updates', () => {
280
+ it('should safely handle updates with injection attempts', async () => {
281
+ const entry = db.createEntry({ content: 'Original content' })
282
+
283
+ const updated = db.updateEntry(entry.id, {
284
+ content: "'; DELETE FROM memory_journal; --",
285
+ })
286
+
287
+ expect(updated?.content).toBe("'; DELETE FROM memory_journal; --")
288
+ // Database should still be intact
289
+ const stats = db.getStatistics('day')
290
+ expect(stats.totalEntries).toBeGreaterThan(0)
291
+ })
292
+ })
293
+
294
+ describe('linkEntries - Relationship Injection', () => {
295
+ it('should safely handle descriptions with injection attempts', async () => {
296
+ const entry1 = db.createEntry({ content: 'Entry 1' })
297
+ const entry2 = db.createEntry({ content: 'Entry 2' })
298
+
299
+ const relationship = db.linkEntries(
300
+ entry1.id,
301
+ entry2.id,
302
+ 'references',
303
+ "'; DROP TABLE relationships; --"
304
+ )
305
+
306
+ expect(relationship.description).toBe("'; DROP TABLE relationships; --")
307
+
308
+ // Verify relationships table still exists
309
+ const rels = db.getRelationships(entry1.id)
310
+ expect(Array.isArray(rels)).toBe(true)
311
+ })
312
+ })
313
+
314
+ describe('restoreFromFile - Path Traversal Protection', () => {
315
+ it('should reject filenames with path traversal', async () => {
316
+ await expect(db.restoreFromFile('../../../etc/passwd')).rejects.toThrow(
317
+ 'Invalid backup filename: path separators not allowed'
318
+ )
319
+
320
+ await expect(db.restoreFromFile('..\\..\\windows\\system32')).rejects.toThrow(
321
+ 'Invalid backup filename: path separators not allowed'
322
+ )
323
+
324
+ await expect(db.restoreFromFile('/etc/passwd')).rejects.toThrow(
325
+ 'Invalid backup filename: path separators not allowed'
326
+ )
327
+ })
328
+ })
329
+
330
+ describe('Database Integrity After Attacks', () => {
331
+ it('should maintain database integrity after all injection attempts', async () => {
332
+ // Verify core tables still exist
333
+ const stats = db.getStatistics('day')
334
+ expect(stats.totalEntries).toBeGreaterThan(0)
335
+
336
+ // Verify we can still perform normal operations
337
+ const newEntry = db.createEntry({ content: 'Integrity check entry' })
338
+ expect(newEntry.id).toBeDefined()
339
+
340
+ const retrieved = db.getEntryById(newEntry.id)
341
+ expect(retrieved?.content).toBe('Integrity check entry')
342
+
343
+ const tags = db.listTags()
344
+ expect(Array.isArray(tags)).toBe(true)
345
+ })
346
+ })
347
+ })
@@ -0,0 +1,55 @@
1
+ import { bench, beforeAll, afterAll } from 'vitest'
2
+ import { getTools, callTool } from '../../src/handlers/tools/index.js'
3
+ import { SqliteAdapter } from '../../src/database/SqliteAdapter.js'
4
+ import * as fs from 'node:fs'
5
+ import * as path from 'node:path'
6
+
7
+ let db: SqliteAdapter
8
+ const testDbPath = path.join(process.cwd(), 'benchmark-server-tools.db')
9
+
10
+ beforeAll(async () => {
11
+ db = new SqliteAdapter(testDbPath)
12
+ await db.initialize()
13
+
14
+ // Setup initial data
15
+ for (let i = 0; i < 1000; i++) {
16
+ db.createEntry({
17
+ content: `Server benchmark tool entry ${i} ${Math.random().toString(36)}`,
18
+ entryType: 'decision',
19
+ })
20
+ }
21
+ })
22
+
23
+ afterAll(() => {
24
+ db.close()
25
+ try {
26
+ if (fs.existsSync(testDbPath)) {
27
+ fs.unlinkSync(testDbPath)
28
+ }
29
+ } catch {
30
+ // Ignore signup errors
31
+ }
32
+ })
33
+
34
+ bench('getTools', () => {
35
+ getTools(db, null)
36
+ })
37
+
38
+ bench('callTool create_entry', async () => {
39
+ await callTool(
40
+ 'create_entry',
41
+ {
42
+ content: 'Bench creation entry text over a tool call',
43
+ entry_type: 'personal_reflection',
44
+ },
45
+ db
46
+ )
47
+ })
48
+
49
+ bench('callTool get_recent_entries', async () => {
50
+ await callTool('get_recent_entries', { limit: 50 }, db)
51
+ })
52
+
53
+ bench('callTool search_entries', async () => {
54
+ await callTool('search_entries', { query: 'benchmark', limit: 20 }, db)
55
+ })