mcp-wordpress 1.5.2 → 2.0.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 (190) hide show
  1. package/README.md +332 -61
  2. package/dist/cache/CacheInvalidation.d.ts.map +1 -1
  3. package/dist/cache/CacheInvalidation.js +4 -4
  4. package/dist/cache/CacheInvalidation.js.map +1 -1
  5. package/dist/client/MockWordPressClient.d.ts +55 -0
  6. package/dist/client/MockWordPressClient.d.ts.map +1 -0
  7. package/dist/client/MockWordPressClient.js +369 -0
  8. package/dist/client/MockWordPressClient.js.map +1 -0
  9. package/dist/client/api.d.ts +1 -0
  10. package/dist/client/api.d.ts.map +1 -1
  11. package/dist/client/api.js +26 -60
  12. package/dist/client/api.js.map +1 -1
  13. package/dist/client/managers/AuthenticationManager.d.ts.map +1 -1
  14. package/dist/client/managers/AuthenticationManager.js +4 -3
  15. package/dist/client/managers/AuthenticationManager.js.map +1 -1
  16. package/dist/config/ConfigurationSchema.d.ts +3 -3
  17. package/dist/config/ConfigurationSchema.d.ts.map +1 -1
  18. package/dist/config/ConfigurationSchema.js +7 -24
  19. package/dist/config/ConfigurationSchema.js.map +1 -1
  20. package/dist/config/ServerConfiguration.d.ts +8 -0
  21. package/dist/config/ServerConfiguration.d.ts.map +1 -1
  22. package/dist/config/ServerConfiguration.js +80 -31
  23. package/dist/config/ServerConfiguration.js.map +1 -1
  24. package/dist/docs/DocumentationGenerator.d.ts.map +1 -1
  25. package/dist/docs/DocumentationGenerator.js +5 -7
  26. package/dist/docs/DocumentationGenerator.js.map +1 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +33 -29
  29. package/dist/index.js.map +1 -1
  30. package/dist/security/InputValidator.d.ts.map +1 -1
  31. package/dist/security/InputValidator.js +3 -11
  32. package/dist/security/InputValidator.js.map +1 -1
  33. package/dist/server/ToolRegistry.d.ts +4 -0
  34. package/dist/server/ToolRegistry.d.ts.map +1 -1
  35. package/dist/server/ToolRegistry.js +71 -8
  36. package/dist/server/ToolRegistry.js.map +1 -1
  37. package/dist/tools/auth.d.ts.map +1 -1
  38. package/dist/tools/auth.js +8 -3
  39. package/dist/tools/auth.js.map +1 -1
  40. package/dist/tools/posts.d.ts.map +1 -1
  41. package/dist/tools/posts.js +287 -20
  42. package/dist/tools/posts.js.map +1 -1
  43. package/dist/tools/site.d.ts.map +1 -1
  44. package/dist/tools/site.js +47 -9
  45. package/dist/tools/site.js.map +1 -1
  46. package/dist/tools/users.d.ts.map +1 -1
  47. package/dist/tools/users.js +113 -10
  48. package/dist/tools/users.js.map +1 -1
  49. package/dist/utils/enhancedError.d.ts +61 -0
  50. package/dist/utils/enhancedError.d.ts.map +1 -0
  51. package/dist/utils/enhancedError.js +221 -0
  52. package/dist/utils/enhancedError.js.map +1 -0
  53. package/dist/utils/streaming.d.ts +104 -0
  54. package/dist/utils/streaming.d.ts.map +1 -0
  55. package/dist/utils/streaming.js +312 -0
  56. package/dist/utils/streaming.js.map +1 -0
  57. package/dist/utils/validation.d.ts +19 -3
  58. package/dist/utils/validation.d.ts.map +1 -1
  59. package/dist/utils/validation.js +174 -24
  60. package/dist/utils/validation.js.map +1 -1
  61. package/docs/ARCHITECTURE.md +850 -0
  62. package/docs/CACHING.md +20 -17
  63. package/docs/CONFIGURATION.md +660 -0
  64. package/docs/DOCKER.md +61 -60
  65. package/docs/EVALUATION.md +397 -0
  66. package/docs/INSTALLATION.md +423 -0
  67. package/docs/PERFORMANCE_MONITORING.md +17 -15
  68. package/docs/SECURITY.md +621 -0
  69. package/docs/SECURITY_TESTING.md +22 -26
  70. package/docs/TEST_SITE_SETUP.md +136 -0
  71. package/docs/TROUBLESHOOTING.md +578 -0
  72. package/docs/api/README.md +76 -91
  73. package/docs/api/categories/auth.md +0 -2
  74. package/docs/api/categories/cache.md +0 -2
  75. package/docs/api/categories/comment.md +0 -2
  76. package/docs/api/categories/media.md +0 -2
  77. package/docs/api/categories/page.md +0 -2
  78. package/docs/api/categories/performance.md +0 -2
  79. package/docs/api/categories/post.md +0 -2
  80. package/docs/api/categories/site.md +0 -2
  81. package/docs/api/categories/taxonomy.md +0 -2
  82. package/docs/api/categories/user.md +0 -2
  83. package/docs/api/summary.json +1 -1
  84. package/docs/api/tools/wp_approve_comment.md +11 -3
  85. package/docs/api/tools/wp_cache_clear.md +14 -5
  86. package/docs/api/tools/wp_cache_info.md +14 -5
  87. package/docs/api/tools/wp_cache_stats.md +14 -5
  88. package/docs/api/tools/wp_cache_warm.md +14 -5
  89. package/docs/api/tools/wp_create_application_password.md +11 -3
  90. package/docs/api/tools/wp_create_category.md +11 -3
  91. package/docs/api/tools/wp_create_comment.md +14 -5
  92. package/docs/api/tools/wp_create_page.md +13 -5
  93. package/docs/api/tools/wp_create_post.md +14 -7
  94. package/docs/api/tools/wp_create_tag.md +11 -3
  95. package/docs/api/tools/wp_create_user.md +13 -5
  96. package/docs/api/tools/wp_delete_application_password.md +11 -3
  97. package/docs/api/tools/wp_delete_category.md +11 -3
  98. package/docs/api/tools/wp_delete_comment.md +11 -3
  99. package/docs/api/tools/wp_delete_media.md +10 -3
  100. package/docs/api/tools/wp_delete_page.md +10 -3
  101. package/docs/api/tools/wp_delete_post.md +11 -5
  102. package/docs/api/tools/wp_delete_tag.md +11 -3
  103. package/docs/api/tools/wp_delete_user.md +10 -3
  104. package/docs/api/tools/wp_get_application_passwords.md +11 -3
  105. package/docs/api/tools/wp_get_auth_status.md +11 -3
  106. package/docs/api/tools/wp_get_category.md +11 -3
  107. package/docs/api/tools/wp_get_comment.md +11 -3
  108. package/docs/api/tools/wp_get_current_user.md +11 -3
  109. package/docs/api/tools/wp_get_media.md +11 -3
  110. package/docs/api/tools/wp_get_page.md +11 -3
  111. package/docs/api/tools/wp_get_page_revisions.md +11 -3
  112. package/docs/api/tools/wp_get_post.md +12 -5
  113. package/docs/api/tools/wp_get_post_revisions.md +11 -3
  114. package/docs/api/tools/wp_get_site_settings.md +10 -3
  115. package/docs/api/tools/wp_get_tag.md +11 -3
  116. package/docs/api/tools/wp_get_user.md +11 -3
  117. package/docs/api/tools/wp_list_categories.md +11 -3
  118. package/docs/api/tools/wp_list_comments.md +11 -3
  119. package/docs/api/tools/wp_list_media.md +14 -5
  120. package/docs/api/tools/wp_list_pages.md +14 -5
  121. package/docs/api/tools/wp_list_posts.md +15 -7
  122. package/docs/api/tools/wp_list_tags.md +11 -3
  123. package/docs/api/tools/wp_list_users.md +11 -3
  124. package/docs/api/tools/wp_performance_alerts.md +17 -7
  125. package/docs/api/tools/wp_performance_benchmark.md +17 -7
  126. package/docs/api/tools/wp_performance_export.md +17 -7
  127. package/docs/api/tools/wp_performance_history.md +17 -7
  128. package/docs/api/tools/wp_performance_optimize.md +17 -7
  129. package/docs/api/tools/wp_performance_stats.md +17 -7
  130. package/docs/api/tools/wp_search_site.md +11 -3
  131. package/docs/api/tools/wp_spam_comment.md +11 -3
  132. package/docs/api/tools/wp_switch_auth_method.md +14 -5
  133. package/docs/api/tools/wp_test_auth.md +11 -3
  134. package/docs/api/tools/wp_update_category.md +11 -3
  135. package/docs/api/tools/wp_update_comment.md +14 -5
  136. package/docs/api/tools/wp_update_media.md +14 -5
  137. package/docs/api/tools/wp_update_page.md +13 -5
  138. package/docs/api/tools/wp_update_post.md +14 -7
  139. package/docs/api/tools/wp_update_site_settings.md +14 -5
  140. package/docs/api/tools/wp_update_tag.md +11 -3
  141. package/docs/api/tools/wp_update_user.md +13 -5
  142. package/docs/api/tools/wp_upload_media.md +13 -5
  143. package/docs/api/types/WordPressPost.md +2 -0
  144. package/docs/code-improvements.md +40 -0
  145. package/docs/contract-testing.md +1 -1
  146. package/docs/developer/API_REFERENCE.md +19 -59
  147. package/docs/developer/ARCHITECTURE.md +8 -11
  148. package/docs/developer/BUILD_SYSTEM.md +2 -2
  149. package/docs/developer/CONTRIBUTING.md +3 -5
  150. package/docs/developer/GITHUB_ACTIONS_SETUP.md +2 -2
  151. package/docs/developer/MIGRATION_GUIDE.md +5 -6
  152. package/docs/developer/README.md +2 -1
  153. package/docs/developer/REFACTORING.md +9 -15
  154. package/docs/developer/RELEASE_PROCESS.md +4 -3
  155. package/docs/developer/TESTING.md +2 -2
  156. package/docs/examples/claude-desktop-config.md +8 -0
  157. package/docs/integrations/claude-desktop.md +426 -0
  158. package/docs/integrations/cline.md +537 -0
  159. package/docs/integrations/vs-code.md +515 -0
  160. package/docs/releases/COMMUNITY_ANNOUNCEMENT_v1.1.2.md +30 -23
  161. package/docs/releases/RELEASE_NOTES_v1.1.2.md +7 -6
  162. package/docs/testing-configurations.md +11 -0
  163. package/docs/user-guides/DOCKER_NPM_DTX_SETUP.md +3 -2
  164. package/docs/user-guides/DOCKER_SETUP.md +3 -2
  165. package/docs/user-guides/DTX_SETUP.md +6 -5
  166. package/docs/user-guides/DXT_INSTALLATION.md +4 -4
  167. package/docs/user-guides/NPM_SETUP.md +4 -2
  168. package/docs/user-guides/NPX_SETUP.md +4 -2
  169. package/docs/user-guides/SMITHERY_SETUP.md +402 -0
  170. package/docs/wordpress-rest-api-authentication-troubleshooting.md +45 -42
  171. package/package.json +12 -2
  172. package/src/cache/CacheInvalidation.ts +7 -18
  173. package/src/client/MockWordPressClient.ts +398 -0
  174. package/src/client/api.ts +77 -237
  175. package/src/client/managers/AuthenticationManager.ts +19 -56
  176. package/src/config/ConfigurationSchema.ts +14 -45
  177. package/src/config/ServerConfiguration.ts +98 -71
  178. package/src/docs/DocumentationGenerator.ts +39 -123
  179. package/src/dxt-entry.cjs +4 -1
  180. package/src/index.ts +35 -54
  181. package/src/security/InputValidator.ts +15 -57
  182. package/src/server/ToolRegistry.ts +88 -17
  183. package/src/tools/auth.ts +15 -22
  184. package/src/tools/posts.ts +347 -64
  185. package/src/tools/site.ts +69 -46
  186. package/src/tools/users.ts +142 -44
  187. package/src/utils/enhancedError.ts +248 -0
  188. package/src/utils/streaming.ts +428 -0
  189. package/src/utils/validation.ts +253 -92
  190. package/dist/mcp-wordpress-1.5.2.tgz +0 -0
@@ -6,35 +6,61 @@ import { WordPressAPIError } from "../types/client.js";
6
6
  */
7
7
 
8
8
  /**
9
- * Validates and sanitizes numeric IDs
9
+ * Validates and sanitizes numeric IDs with comprehensive edge case handling
10
10
  */
11
11
  export function validateId(id: any, fieldName: string = "id"): number {
12
- const numId = parseInt(String(id), 10);
13
- if (isNaN(numId) || numId <= 0) {
12
+ // Handle null/undefined
13
+ if (id === null || id === undefined) {
14
+ throw new WordPressAPIError(`${fieldName} is required`, 400, "MISSING_PARAMETER");
15
+ }
16
+
17
+ // Convert to string first to handle various input types
18
+ const strId = String(id).trim();
19
+
20
+ // Check for empty string after trim
21
+ if (strId === "") {
22
+ throw new WordPressAPIError(`${fieldName} cannot be empty`, 400, "INVALID_PARAMETER");
23
+ }
24
+
25
+ // Handle decimal inputs
26
+ if (strId.includes(".")) {
27
+ throw new WordPressAPIError(`${fieldName} must be a whole number, not a decimal`, 400, "INVALID_PARAMETER");
28
+ }
29
+
30
+ const numId = parseInt(strId, 10);
31
+
32
+ // Check for NaN
33
+ if (isNaN(numId)) {
34
+ throw new WordPressAPIError(`Invalid ${fieldName}: "${id}" is not a valid number`, 400, "INVALID_PARAMETER");
35
+ }
36
+
37
+ // Check for negative or zero
38
+ if (numId <= 0) {
14
39
  throw new WordPressAPIError(
15
- `Invalid ${fieldName}: must be a positive number`,
40
+ `Invalid ${fieldName}: must be a positive number (got ${numId})`,
16
41
  400,
17
42
  "INVALID_PARAMETER",
18
43
  );
19
44
  }
45
+
46
+ // Check for max int32 limit (WordPress database limit)
47
+ if (numId > 2147483647) {
48
+ throw new WordPressAPIError(
49
+ `Invalid ${fieldName}: exceeds maximum allowed value (2147483647)`,
50
+ 400,
51
+ "INVALID_PARAMETER",
52
+ );
53
+ }
54
+
20
55
  return numId;
21
56
  }
22
57
 
23
58
  /**
24
59
  * Validates string length within bounds
25
60
  */
26
- export function validateString(
27
- value: any,
28
- fieldName: string,
29
- minLength: number = 1,
30
- maxLength: number = 1000,
31
- ): string {
61
+ export function validateString(value: any, fieldName: string, minLength: number = 1, maxLength: number = 1000): string {
32
62
  if (typeof value !== "string") {
33
- throw new WordPressAPIError(
34
- `Invalid ${fieldName}: must be a string`,
35
- 400,
36
- "INVALID_PARAMETER",
37
- );
63
+ throw new WordPressAPIError(`Invalid ${fieldName}: must be a string`, 400, "INVALID_PARAMETER");
38
64
  }
39
65
 
40
66
  const trimmed = value.trim();
@@ -52,21 +78,14 @@ export function validateString(
52
78
  /**
53
79
  * Validates and sanitizes file paths to prevent directory traversal
54
80
  */
55
- export function validateFilePath(
56
- userPath: string,
57
- allowedBasePath: string,
58
- ): string {
81
+ export function validateFilePath(userPath: string, allowedBasePath: string): string {
59
82
  // Normalize the path to remove ../ and other dangerous patterns
60
83
  const normalizedPath = path.normalize(userPath);
61
84
  const resolvedPath = path.resolve(allowedBasePath, normalizedPath);
62
85
 
63
86
  // Ensure the resolved path is within the allowed directory
64
87
  if (!resolvedPath.startsWith(path.resolve(allowedBasePath))) {
65
- throw new WordPressAPIError(
66
- "Invalid file path: access denied",
67
- 403,
68
- "PATH_TRAVERSAL_ATTEMPT",
69
- );
88
+ throw new WordPressAPIError("Invalid file path: access denied", 403, "PATH_TRAVERSAL_ATTEMPT");
70
89
  }
71
90
 
72
91
  return resolvedPath;
@@ -76,69 +95,96 @@ export function validateFilePath(
76
95
  * Validates WordPress post status values
77
96
  */
78
97
  export function validatePostStatus(status: string): string {
79
- const validStatuses = [
80
- "publish",
81
- "draft",
82
- "pending",
83
- "private",
84
- "future",
85
- "auto-draft",
86
- "trash",
87
- ];
98
+ const validStatuses = ["publish", "draft", "pending", "private", "future", "auto-draft", "trash"];
88
99
  if (!validStatuses.includes(status)) {
89
- throw new WordPressAPIError(
90
- `Invalid status: must be one of ${validStatuses.join(", ")}`,
91
- 400,
92
- "INVALID_PARAMETER",
93
- );
100
+ throw new WordPressAPIError(`Invalid status: must be one of ${validStatuses.join(", ")}`, 400, "INVALID_PARAMETER");
94
101
  }
95
102
  return status;
96
103
  }
97
104
 
98
105
  /**
99
- * Validates and sanitizes URLs
106
+ * Validates and sanitizes URLs with enhanced edge case handling
100
107
  */
101
108
  export function validateUrl(url: string, fieldName: string = "url"): string {
102
- try {
103
- const urlObj = new URL(url);
104
- // Only allow http and https protocols
105
- if (!["http:", "https:"].includes(urlObj.protocol)) {
106
- throw new Error("Invalid protocol");
107
- }
108
- return urlObj.toString();
109
- } catch {
109
+ // Check for empty or whitespace-only URLs
110
+ const trimmedUrl = url.trim();
111
+ if (!trimmedUrl) {
112
+ throw new WordPressAPIError(`${fieldName} cannot be empty`, 400, "INVALID_PARAMETER");
113
+ }
114
+
115
+ // Remove trailing slashes for consistency
116
+ const cleanUrl = trimmedUrl.replace(/\/+$/, "");
117
+
118
+ // Check for common URL mistakes
119
+ if (!cleanUrl.match(/^https?:\/\//i)) {
110
120
  throw new WordPressAPIError(
111
- `Invalid ${fieldName}: must be a valid URL`,
121
+ `Invalid ${fieldName}: must start with http:// or https:// (got "${cleanUrl}")`,
112
122
  400,
113
123
  "INVALID_PARAMETER",
114
124
  );
115
125
  }
126
+
127
+ try {
128
+ const urlObj = new URL(cleanUrl);
129
+
130
+ // Only allow http and https protocols
131
+ if (!["http:", "https:"].includes(urlObj.protocol)) {
132
+ throw new WordPressAPIError(
133
+ `Invalid ${fieldName}: only HTTP and HTTPS protocols are allowed`,
134
+ 400,
135
+ "INVALID_PARAMETER",
136
+ );
137
+ }
138
+
139
+ // Validate hostname
140
+ if (!urlObj.hostname || urlObj.hostname.length < 3) {
141
+ throw new WordPressAPIError(`Invalid ${fieldName}: hostname is missing or too short`, 400, "INVALID_PARAMETER");
142
+ }
143
+
144
+ // Check for localhost in production
145
+ if (process.env.NODE_ENV === "production" && (urlObj.hostname === "localhost" || urlObj.hostname === "127.0.0.1")) {
146
+ throw new WordPressAPIError(
147
+ `Invalid ${fieldName}: localhost URLs are not allowed in production`,
148
+ 400,
149
+ "INVALID_PARAMETER",
150
+ );
151
+ }
152
+
153
+ // Validate port if present
154
+ if (urlObj.port) {
155
+ const port = parseInt(urlObj.port);
156
+ if (port < 1 || port > 65535) {
157
+ throw new WordPressAPIError(
158
+ `Invalid ${fieldName}: port number must be between 1 and 65535`,
159
+ 400,
160
+ "INVALID_PARAMETER",
161
+ );
162
+ }
163
+ }
164
+
165
+ return cleanUrl;
166
+ } catch (error) {
167
+ if (error instanceof WordPressAPIError) {
168
+ throw error;
169
+ }
170
+ throw new WordPressAPIError(`Invalid ${fieldName}: malformed URL "${cleanUrl}"`, 400, "INVALID_PARAMETER");
171
+ }
116
172
  }
117
173
 
118
174
  /**
119
175
  * Validates file size
120
176
  */
121
- export function validateFileSize(
122
- sizeInBytes: number,
123
- maxSizeInMB: number = 10,
124
- ): void {
177
+ export function validateFileSize(sizeInBytes: number, maxSizeInMB: number = 10): void {
125
178
  const maxSizeInBytes = maxSizeInMB * 1024 * 1024;
126
179
  if (sizeInBytes > maxSizeInBytes) {
127
- throw new WordPressAPIError(
128
- `File size exceeds maximum allowed size of ${maxSizeInMB}MB`,
129
- 413,
130
- "FILE_TOO_LARGE",
131
- );
180
+ throw new WordPressAPIError(`File size exceeds maximum allowed size of ${maxSizeInMB}MB`, 413, "FILE_TOO_LARGE");
132
181
  }
133
182
  }
134
183
 
135
184
  /**
136
185
  * Validates MIME types for file uploads
137
186
  */
138
- export function validateMimeType(
139
- mimeType: string,
140
- allowedTypes: string[],
141
- ): void {
187
+ export function validateMimeType(mimeType: string, allowedTypes: string[]): void {
142
188
  if (!allowedTypes.includes(mimeType)) {
143
189
  throw new WordPressAPIError(
144
190
  `Invalid file type: ${mimeType}. Allowed types: ${allowedTypes.join(", ")}`,
@@ -155,10 +201,7 @@ export function validateMimeType(
155
201
  */
156
202
  export function sanitizeHtml(html: string): string {
157
203
  // Remove script tags and their content
158
- let sanitized = html.replace(
159
- /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
160
- "",
161
- );
204
+ let sanitized = html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "");
162
205
 
163
206
  // Remove event handlers
164
207
  sanitized = sanitized.replace(/\s*on\w+\s*=\s*["'][^"']*["']/gi, "");
@@ -175,18 +218,9 @@ export function sanitizeHtml(html: string): string {
175
218
  /**
176
219
  * Validates array input
177
220
  */
178
- export function validateArray<T>(
179
- value: any,
180
- fieldName: string,
181
- minItems: number = 0,
182
- maxItems: number = 100,
183
- ): T[] {
221
+ export function validateArray<T>(value: any, fieldName: string, minItems: number = 0, maxItems: number = 100): T[] {
184
222
  if (!Array.isArray(value)) {
185
- throw new WordPressAPIError(
186
- `Invalid ${fieldName}: must be an array`,
187
- 400,
188
- "INVALID_PARAMETER",
189
- );
223
+ throw new WordPressAPIError(`Invalid ${fieldName}: must be an array`, 400, "INVALID_PARAMETER");
190
224
  }
191
225
 
192
226
  if (value.length < minItems || value.length > maxItems) {
@@ -206,22 +240,24 @@ export function validateArray<T>(
206
240
  export function validateEmail(email: string): string {
207
241
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
208
242
  if (!emailRegex.test(email)) {
209
- throw new WordPressAPIError(
210
- "Invalid email address format",
211
- 400,
212
- "INVALID_PARAMETER",
213
- );
243
+ throw new WordPressAPIError("Invalid email address format", 400, "INVALID_PARAMETER");
214
244
  }
215
245
  return email.toLowerCase();
216
246
  }
217
247
 
218
248
  /**
219
- * Validates username format
249
+ * Validates username format with enhanced security checks
220
250
  */
221
251
  export function validateUsername(username: string): string {
252
+ // Trim and check for empty
253
+ const trimmed = username.trim();
254
+ if (!trimmed) {
255
+ throw new WordPressAPIError("Username cannot be empty", 400, "INVALID_PARAMETER");
256
+ }
257
+
222
258
  // WordPress username rules: alphanumeric, space, underscore, hyphen, period, @ symbol
223
259
  const usernameRegex = /^[a-zA-Z0-9 _.\-@]+$/;
224
- if (!usernameRegex.test(username)) {
260
+ if (!usernameRegex.test(trimmed)) {
225
261
  throw new WordPressAPIError(
226
262
  "Invalid username: can only contain letters, numbers, spaces, and _.-@ symbols",
227
263
  400,
@@ -229,15 +265,27 @@ export function validateUsername(username: string): string {
229
265
  );
230
266
  }
231
267
 
232
- if (username.length < 3 || username.length > 60) {
268
+ // Length validation
269
+ if (trimmed.length < 3 || trimmed.length > 60) {
233
270
  throw new WordPressAPIError(
234
- "Invalid username: must be between 3 and 60 characters",
271
+ `Invalid username: must be between 3 and 60 characters (got ${trimmed.length})`,
235
272
  400,
236
273
  "INVALID_PARAMETER",
237
274
  );
238
275
  }
239
276
 
240
- return username;
277
+ // Check for consecutive spaces
278
+ if (/\s{2,}/.test(trimmed)) {
279
+ throw new WordPressAPIError("Invalid username: cannot contain consecutive spaces", 400, "INVALID_PARAMETER");
280
+ }
281
+
282
+ // Security: Prevent common problematic usernames
283
+ const blacklist = ["admin", "root", "wordpress", "wp-admin", "administrator"];
284
+ if (blacklist.includes(trimmed.toLowerCase())) {
285
+ throw new WordPressAPIError(`Username "${trimmed}" is reserved and cannot be used`, 400, "RESERVED_USERNAME");
286
+ }
287
+
288
+ return trimmed;
241
289
  }
242
290
 
243
291
  /**
@@ -245,8 +293,7 @@ export function validateUsername(username: string): string {
245
293
  * For production, use Redis or similar
246
294
  */
247
295
  class RateLimiter {
248
- private attempts: Map<string, { count: number; resetTime: number }> =
249
- new Map();
296
+ private attempts: Map<string, { count: number; resetTime: number }> = new Map();
250
297
 
251
298
  constructor(
252
299
  private maxAttempts: number = 5,
@@ -298,13 +345,127 @@ export function validateSearchQuery(query: string): string {
298
345
  }
299
346
 
300
347
  // Remove SQL-like patterns (basic protection)
301
- sanitized = sanitized.replace(
302
- /(\b(union|select|insert|update|delete|drop|create)\b)/gi,
303
- "",
304
- );
348
+ sanitized = sanitized.replace(/(\b(union|select|insert|update|delete|drop|create)\b)/gi, "");
305
349
 
306
350
  // Remove special characters that might be used for injection
307
351
  sanitized = sanitized.replace(/[<>'"`;\\]/g, "");
308
352
 
309
353
  return sanitized;
310
354
  }
355
+
356
+ /**
357
+ * Validates pagination parameters as a set
358
+ */
359
+ export function validatePaginationParams(params: { page?: any; per_page?: any; offset?: any }): {
360
+ page?: number;
361
+ per_page?: number;
362
+ offset?: number;
363
+ } {
364
+ const validated: { page?: number; per_page?: number; offset?: number } = {};
365
+
366
+ // Validate page
367
+ if (params.page !== undefined) {
368
+ const page = parseInt(String(params.page), 10);
369
+ if (isNaN(page) || page < 1) {
370
+ throw new WordPressAPIError("Page must be a positive integer", 400, "INVALID_PARAMETER");
371
+ }
372
+ if (page > 10000) {
373
+ throw new WordPressAPIError("Page number too high (max 10000)", 400, "INVALID_PARAMETER");
374
+ }
375
+ validated.page = page;
376
+ }
377
+
378
+ // Validate per_page
379
+ if (params.per_page !== undefined) {
380
+ const perPage = parseInt(String(params.per_page), 10);
381
+ if (isNaN(perPage) || perPage < 1) {
382
+ throw new WordPressAPIError("Per page must be a positive integer", 400, "INVALID_PARAMETER");
383
+ }
384
+ if (perPage > 100) {
385
+ throw new WordPressAPIError(`Per page exceeds maximum allowed (100), got ${perPage}`, 400, "INVALID_PARAMETER");
386
+ }
387
+ validated.per_page = perPage;
388
+ }
389
+
390
+ // Validate offset
391
+ if (params.offset !== undefined) {
392
+ const offset = parseInt(String(params.offset), 10);
393
+ if (isNaN(offset) || offset < 0) {
394
+ throw new WordPressAPIError("Offset must be a non-negative integer", 400, "INVALID_PARAMETER");
395
+ }
396
+ if (offset > 1000000) {
397
+ throw new WordPressAPIError("Offset too large (max 1000000)", 400, "INVALID_PARAMETER");
398
+ }
399
+ validated.offset = offset;
400
+ }
401
+
402
+ // Check for conflicting parameters
403
+ if (validated.page && validated.offset) {
404
+ throw new WordPressAPIError(
405
+ "Cannot use both 'page' and 'offset' parameters together",
406
+ 400,
407
+ "CONFLICTING_PARAMETERS",
408
+ );
409
+ }
410
+
411
+ return validated;
412
+ }
413
+
414
+ /**
415
+ * Validates complex post creation parameters
416
+ */
417
+ export function validatePostParams(params: any): any {
418
+ const validated: any = {};
419
+
420
+ // Title validation
421
+ if (!params.title || typeof params.title !== "string") {
422
+ throw new WordPressAPIError("Post title is required and must be a string", 400, "INVALID_PARAMETER");
423
+ }
424
+ validated.title = validateString(params.title, "title", 1, 200);
425
+
426
+ // Content validation
427
+ if (params.content !== undefined) {
428
+ validated.content = sanitizeHtml(String(params.content));
429
+ }
430
+
431
+ // Status validation with context
432
+ if (params.status) {
433
+ validated.status = validatePostStatus(params.status);
434
+
435
+ // Future posts need a date
436
+ if (validated.status === "future" && !params.date) {
437
+ throw new WordPressAPIError("Future posts require a 'date' parameter", 400, "MISSING_PARAMETER");
438
+ }
439
+ }
440
+
441
+ // Categories and tags validation
442
+ if (params.categories) {
443
+ validated.categories = validateArray(params.categories, "categories", 0, 50);
444
+ validated.categories = validated.categories.map((id: any) => validateId(id, "category ID"));
445
+ }
446
+
447
+ if (params.tags) {
448
+ validated.tags = validateArray(params.tags, "tags", 0, 100);
449
+ validated.tags = validated.tags.map((id: any) => validateId(id, "tag ID"));
450
+ }
451
+
452
+ // Date validation for scheduled posts
453
+ if (params.date) {
454
+ try {
455
+ const date = new Date(params.date);
456
+ if (isNaN(date.getTime())) {
457
+ throw new Error("Invalid date");
458
+ }
459
+ // WordPress expects ISO 8601 format
460
+ validated.date = date.toISOString();
461
+ } catch {
462
+ throw new WordPressAPIError(
463
+ "Invalid date format. Use ISO 8601 format (YYYY-MM-DDTHH:mm:ss)",
464
+ 400,
465
+ "INVALID_PARAMETER",
466
+ );
467
+ }
468
+ }
469
+
470
+ return validated;
471
+ }
Binary file