mastercontroller 1.2.12 → 1.2.14

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.
package/MasterTemplate.js CHANGED
@@ -1,6 +1,11 @@
1
- // version 0.0.3
1
+ // version 0.0.5
2
2
  // https://github.com/WebReflection/backtick-template
3
3
  // https://stackoverflow.com/questions/29182244/convert-a-string-to-a-template-string
4
+
5
+ // Security - Template injection prevention
6
+ const { escapeHTML } = require('./security/MasterSanitizer');
7
+ const { logger } = require('./error/MasterErrorLogger');
8
+
4
9
  var replace = ''.replace;
5
10
 
6
11
  var ca = /[&<>'"]/g;
@@ -47,8 +52,14 @@ class MasterTemplate{
47
52
  str = hasTransformer ? $str : fn,
48
53
  object = hasTransformer ? $object : $str,
49
54
  _ = this._,
50
- known = _.hasOwnProperty(str),
51
- parsed = known ? _[str] : (_[str] = this.parse(str)),
55
+ known = _.hasOwnProperty(str);
56
+
57
+ // Security: Validate template for dangerous patterns
58
+ if (!known) {
59
+ this.validateTemplate(str);
60
+ }
61
+
62
+ var parsed = known ? _[str] : (_[str] = this.parse(str)),
52
63
  chunks = parsed.chunks,
53
64
  values = parsed.values,
54
65
  strings
@@ -132,6 +143,88 @@ return {chunks: chunks, values: values};
132
143
  cape(m) {
133
144
  return unes[m];
134
145
  }
146
+
147
+ // ==================== Security Methods ====================
148
+
149
+ /**
150
+ * Validate template for dangerous patterns
151
+ * Prevents template injection attacks
152
+ */
153
+ validateTemplate(template) {
154
+ if (!template || typeof template !== 'string') {
155
+ return;
156
+ }
157
+
158
+ const isDevelopment = process.env.NODE_ENV !== 'production' && process.env.master === 'development';
159
+
160
+ // Dangerous patterns in templates
161
+ const dangerousPatterns = [
162
+ { pattern: /\$\{.*__proto__/gi, name: 'Prototype pollution' },
163
+ { pattern: /\$\{.*constructor.*\(/gi, name: 'Constructor access' },
164
+ { pattern: /\$\{.*\beval\s*\(/gi, name: 'eval() usage' },
165
+ { pattern: /\$\{.*Function\s*\(/gi, name: 'Function constructor' },
166
+ { pattern: /\$\{.*require\s*\(/gi, name: 'require() usage' },
167
+ { pattern: /\$\{.*import\s*\(/gi, name: 'import() usage' },
168
+ { pattern: /\$\{.*process\./gi, name: 'Process access' },
169
+ { pattern: /\$\{.*global\./gi, name: 'Global object access' },
170
+ { pattern: /\$\{.*\bfs\./gi, name: 'File system access' },
171
+ { pattern: /\$\{.*child_process/gi, name: 'Child process access' }
172
+ ];
173
+
174
+ for (const { pattern, name } of dangerousPatterns) {
175
+ if (pattern.test(template)) {
176
+ logger.error({
177
+ code: 'MC_SECURITY_TEMPLATE_INJECTION',
178
+ message: `Dangerous template pattern detected: ${name}`,
179
+ pattern: pattern.toString(),
180
+ template: template.substring(0, 200) // Log first 200 chars only
181
+ });
182
+
183
+ if (isDevelopment) {
184
+ throw new Error(`[MasterController Security] Template injection attempt detected: ${name}\nPattern: ${pattern}`);
185
+ }
186
+
187
+ // In production, sanitize by removing the dangerous expression
188
+ template = template.replace(pattern, '${/* REMOVED: Security risk */}');
189
+ }
190
+ }
191
+
192
+ return template;
193
+ }
194
+
195
+ /**
196
+ * Sanitize template variables before rendering
197
+ * Call this on user-provided data
198
+ */
199
+ sanitizeVariable(value) {
200
+ if (value === null || value === undefined) {
201
+ return '';
202
+ }
203
+
204
+ if (typeof value === 'string') {
205
+ return escapeHTML(value);
206
+ }
207
+
208
+ if (typeof value === 'object') {
209
+ // Prevent prototype pollution
210
+ if (value.__proto__ || value.constructor) {
211
+ logger.warn({
212
+ code: 'MC_SECURITY_OBJECT_POLLUTION',
213
+ message: 'Attempted to pass object with prototype/constructor to template'
214
+ });
215
+ return '[Object]';
216
+ }
217
+
218
+ // Safely stringify
219
+ try {
220
+ return JSON.stringify(value);
221
+ } catch (e) {
222
+ return '[Object]';
223
+ }
224
+ }
225
+
226
+ return String(value);
227
+ }
135
228
  }
136
229
 
137
230
  module.exports = MasterTemplate;
package/README.md CHANGED
@@ -49,50 +49,6 @@ Use `setupServer('https', credentials)` or configure via environment TLS; see do
49
49
  - `docs/server-setup-nginx-reverse-proxy.md`
50
50
  - `docs/environment-tls-reference.md`
51
51
 
52
- ### How File Uploads Work
53
-
54
- MasterController handles file uploads through the `formidable` library (v3.5.4+) integrated into the request parsing pipeline in `MasterRequest.js`.
55
-
56
- **Processing Flow:**
57
-
58
- 1. **Content-Type Detection** - When a request arrives, the framework parses the `Content-Type` header to determine how to handle the request body (`MasterRequest.js:34-36`)
59
-
60
- 2. **Multipart Form Data** - For `multipart/form-data` requests (file uploads), the framework uses formidable's `IncomingForm` to parse the request (`MasterRequest.js:43-78`)
61
-
62
- 3. **Event-Based Parsing** - Formidable emits events during parsing:
63
- - `field` event: Captures regular form fields and adds them to `parsedURL.formData.fields`
64
- - `file` event: Captures uploaded files and stores them in `parsedURL.formData.files` as arrays (supporting multiple file uploads per field)
65
- - `end` event: Signals completion and resolves the promise with parsed data
66
-
67
- 4. **File Metadata** - Each uploaded file object includes:
68
- - `name` or `originalFilename`: The original filename
69
- - `extension`: Extracted file extension (e.g., `.jpg`, `.pdf`)
70
- - `filepath`: Temporary location where formidable stored the file
71
- - Other formidable metadata (size, mimetype, etc.)
72
-
73
- 5. **Accessing Uploads in Controllers** - In your controller actions, access uploaded files via:
74
- ```js
75
- this.params.formData.files['fieldName'][0] // First file for 'fieldName'
76
- this.params.formData.fields['textField'] // Regular form fields
77
- ```
78
-
79
- 6. **Multiple Files** - Files are always stored as arrays in `parsedURL.formData.files[field]`, allowing multiple files to be uploaded with the same field name (`MasterRequest.js:59-65`)
80
-
81
- 7. **Cleanup** - Use `this.request.deleteFileBuffer(filePath)` to remove temporary files after processing (`MasterRequest.js:162-169`)
82
-
83
- **Configuration Options:**
84
-
85
- You can configure file upload behavior via `master.request.init()`:
86
- - `disableFormidableMultipartFormData`: Set to `true` to skip file upload parsing
87
- - `formidable`: Pass options directly to formidable (upload directory, max file size, etc.)
88
-
89
- **Supported Content Types:**
90
- - `multipart/form-data` - File uploads
91
- - `application/x-www-form-urlencoded` - Standard forms
92
- - `application/json` - JSON payloads
93
- - `text/plain` - Plain text (1MB limit)
94
- - `text/html` - HTML content
95
-
96
52
  ### Production tips
97
53
  - Prefer a reverse proxy for TLS and serve Node on a high port.
98
54
  - If keeping TLS in Node, harden TLS and manage cert rotation.
@@ -0,0 +1,353 @@
1
+ /**
2
+ * ErrorBoundary - Production error boundary system for Web Components
3
+ * Catches component errors without crashing entire application
4
+ * Version: 1.0.0
5
+ */
6
+
7
+ /**
8
+ * ErrorBoundary Web Component
9
+ * Usage:
10
+ * <error-boundary>
11
+ * <my-component></my-component>
12
+ * </error-boundary>
13
+ */
14
+ class ErrorBoundary extends HTMLElement {
15
+ constructor() {
16
+ super();
17
+ this._hasError = false;
18
+ this._errorInfo = null;
19
+ this._originalContent = null;
20
+ }
21
+
22
+ connectedCallback() {
23
+ // Store original content
24
+ this._originalContent = this.innerHTML;
25
+
26
+ // Catch errors from child components
27
+ this.addEventListener('error', this._handleError.bind(this), true);
28
+
29
+ // Also catch unhandled promise rejections in child components
30
+ window.addEventListener('unhandledrejection', this._handleRejection.bind(this));
31
+
32
+ // Wrap all child custom elements with error catching
33
+ this._wrapChildComponents();
34
+ }
35
+
36
+ disconnectedCallback() {
37
+ this.removeEventListener('error', this._handleError, true);
38
+ window.removeEventListener('unhandledrejection', this._handleRejection);
39
+ }
40
+
41
+ /**
42
+ * Wrap child component lifecycle methods with error handling
43
+ */
44
+ _wrapChildComponents() {
45
+ const customElements = this.querySelectorAll('*');
46
+
47
+ customElements.forEach(el => {
48
+ if (!el.tagName.includes('-')) return;
49
+
50
+ // Wrap connectedCallback
51
+ if (el.connectedCallback && !el._errorBoundaryWrapped) {
52
+ const originalConnected = el.connectedCallback.bind(el);
53
+ el.connectedCallback = (...args) => {
54
+ try {
55
+ return originalConnected(...args);
56
+ } catch (error) {
57
+ this._catchComponentError(error, el);
58
+ }
59
+ };
60
+ el._errorBoundaryWrapped = true;
61
+ }
62
+
63
+ // Wrap attributeChangedCallback
64
+ if (el.attributeChangedCallback && !el._errorBoundaryAttrWrapped) {
65
+ const originalAttrChanged = el.attributeChangedCallback.bind(el);
66
+ el.attributeChangedCallback = (...args) => {
67
+ try {
68
+ return originalAttrChanged(...args);
69
+ } catch (error) {
70
+ this._catchComponentError(error, el);
71
+ }
72
+ };
73
+ el._errorBoundaryAttrWrapped = true;
74
+ }
75
+ });
76
+ }
77
+
78
+ /**
79
+ * Handle error events
80
+ */
81
+ _handleError(event) {
82
+ // Only handle errors from child elements
83
+ if (!this.contains(event.target)) return;
84
+
85
+ event.preventDefault();
86
+ event.stopPropagation();
87
+
88
+ this._catchComponentError(event.error || new Error('Unknown error'), event.target);
89
+ }
90
+
91
+ /**
92
+ * Handle unhandled promise rejections
93
+ */
94
+ _handleRejection(event) {
95
+ // Check if rejection came from a component within this boundary
96
+ if (event.reason && event.reason.component) {
97
+ const component = this.querySelector(event.reason.component);
98
+ if (component) {
99
+ this._catchComponentError(event.reason, component);
100
+ }
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Catch and handle component errors
106
+ */
107
+ _catchComponentError(error, component) {
108
+ if (this._hasError) return; // Already in error state
109
+
110
+ this._hasError = true;
111
+ this._errorInfo = {
112
+ error,
113
+ component: component ? component.tagName.toLowerCase() : 'unknown',
114
+ timestamp: new Date().toISOString(),
115
+ userAgent: navigator.userAgent,
116
+ url: window.location.href
117
+ };
118
+
119
+ // Log error
120
+ this._logError();
121
+
122
+ // Show fallback UI
123
+ this._showFallbackUI();
124
+
125
+ // Call custom error handler if provided
126
+ if (typeof this.onError === 'function') {
127
+ try {
128
+ this.onError(this._errorInfo);
129
+ } catch (handlerError) {
130
+ console.error('[ErrorBoundary] onError handler failed:', handlerError);
131
+ }
132
+ }
133
+
134
+ // Dispatch custom event for external monitoring
135
+ this.dispatchEvent(new CustomEvent('error-boundary-catch', {
136
+ bubbles: true,
137
+ detail: this._errorInfo
138
+ }));
139
+ }
140
+
141
+ /**
142
+ * Log error to console and monitoring services
143
+ */
144
+ _logError() {
145
+ console.error('[ErrorBoundary] Caught error:', this._errorInfo);
146
+
147
+ // Send to monitoring service if configured
148
+ if (window.masterControllerErrorReporter) {
149
+ try {
150
+ window.masterControllerErrorReporter({
151
+ type: 'error-boundary',
152
+ ...this._errorInfo
153
+ });
154
+ } catch (reporterError) {
155
+ console.error('[ErrorBoundary] Error reporter failed:', reporterError);
156
+ }
157
+ }
158
+
159
+ // Send to Sentry if available
160
+ if (window.Sentry) {
161
+ window.Sentry.captureException(this._errorInfo.error, {
162
+ tags: {
163
+ component: this._errorInfo.component,
164
+ errorBoundary: true
165
+ },
166
+ extra: this._errorInfo
167
+ });
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Show fallback UI
173
+ */
174
+ _showFallbackUI() {
175
+ const fallbackTemplate = this.getAttribute('fallback-template');
176
+ const customMessage = this.getAttribute('error-message');
177
+
178
+ if (fallbackTemplate) {
179
+ // Use custom template
180
+ const template = document.querySelector(fallbackTemplate);
181
+ if (template) {
182
+ this.innerHTML = template.innerHTML;
183
+ return;
184
+ }
185
+ }
186
+
187
+ // Default fallback UI
188
+ const isDevelopment = this.hasAttribute('dev-mode');
189
+
190
+ this.innerHTML = `
191
+ <div class="error-boundary-fallback" style="
192
+ padding: 20px;
193
+ margin: 10px 0;
194
+ background: ${isDevelopment ? '#fee' : '#f9fafb'};
195
+ border: 2px solid ${isDevelopment ? '#f87171' : '#d1d5db'};
196
+ border-radius: 8px;
197
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
198
+ ">
199
+ <div style="display: flex; align-items: start; gap: 12px;">
200
+ <div style="font-size: 24px;">${isDevelopment ? '❌' : '⚠️'}</div>
201
+ <div style="flex: 1;">
202
+ <h3 style="margin: 0 0 8px 0; color: ${isDevelopment ? '#dc2626' : '#374151'}; font-size: 18px; font-weight: 600;">
203
+ ${customMessage || 'Something went wrong'}
204
+ </h3>
205
+ <p style="margin: 0 0 12px 0; color: #6b7280; font-size: 14px;">
206
+ ${isDevelopment
207
+ ? `Component "${this._errorInfo.component}" encountered an error.`
208
+ : 'We\'ve been notified and are working on it.'
209
+ }
210
+ </p>
211
+ ${isDevelopment ? `
212
+ <details style="margin-top: 12px;">
213
+ <summary style="cursor: pointer; color: #3b82f6; font-weight: 600; font-size: 14px;">
214
+ View Error Details
215
+ </summary>
216
+ <pre style="
217
+ margin-top: 12px;
218
+ padding: 12px;
219
+ background: #1f2937;
220
+ color: #f3f4f6;
221
+ border-radius: 4px;
222
+ font-size: 12px;
223
+ overflow-x: auto;
224
+ font-family: 'Courier New', monospace;
225
+ ">${this.escapeHtml(this._errorInfo.error.stack || this._errorInfo.error.message)}</pre>
226
+ </details>
227
+ ` : ''}
228
+ <button
229
+ onclick="this.closest('.error-boundary-fallback').parentElement.dispatchEvent(new CustomEvent('error-boundary-retry', { bubbles: true }))"
230
+ style="
231
+ margin-top: 12px;
232
+ padding: 8px 16px;
233
+ background: #3b82f6;
234
+ color: white;
235
+ border: none;
236
+ border-radius: 6px;
237
+ font-weight: 600;
238
+ font-size: 14px;
239
+ cursor: pointer;
240
+ "
241
+ onmouseover="this.style.background='#2563eb'"
242
+ onmouseout="this.style.background='#3b82f6'"
243
+ >
244
+ Try Again
245
+ </button>
246
+ </div>
247
+ </div>
248
+ </div>
249
+ `;
250
+
251
+ // Handle retry button
252
+ this.addEventListener('error-boundary-retry', this._handleRetry.bind(this), { once: true });
253
+ }
254
+
255
+ /**
256
+ * Handle retry
257
+ */
258
+ _handleRetry() {
259
+ this._hasError = false;
260
+ this._errorInfo = null;
261
+ this.innerHTML = this._originalContent;
262
+
263
+ // Re-wrap child components
264
+ this._wrapChildComponents();
265
+
266
+ // Dispatch retry event
267
+ this.dispatchEvent(new CustomEvent('error-boundary-retried', {
268
+ bubbles: true
269
+ }));
270
+ }
271
+
272
+ /**
273
+ * Escape HTML for safe rendering
274
+ */
275
+ escapeHtml(str) {
276
+ if (!str) return '';
277
+ return String(str)
278
+ .replace(/&/g, '&amp;')
279
+ .replace(/</g, '&lt;')
280
+ .replace(/>/g, '&gt;')
281
+ .replace(/"/g, '&quot;')
282
+ .replace(/'/g, '&#039;');
283
+ }
284
+
285
+ /**
286
+ * Public API: Reset error state
287
+ */
288
+ reset() {
289
+ this._handleRetry();
290
+ }
291
+
292
+ /**
293
+ * Public API: Get error info
294
+ */
295
+ getErrorInfo() {
296
+ return this._errorInfo;
297
+ }
298
+
299
+ /**
300
+ * Public API: Check if has error
301
+ */
302
+ hasError() {
303
+ return this._hasError;
304
+ }
305
+ }
306
+
307
+ // Register the error boundary component
308
+ if (!customElements.get('error-boundary')) {
309
+ customElements.define('error-boundary', ErrorBoundary);
310
+ }
311
+
312
+ // Global error handler setup
313
+ if (typeof window !== 'undefined') {
314
+ // Catch uncaught errors globally
315
+ window.addEventListener('error', (event) => {
316
+ console.error('[MasterController] Uncaught error:', event.error);
317
+
318
+ // Try to find nearest error boundary
319
+ if (event.target instanceof HTMLElement) {
320
+ let boundary = event.target.closest('error-boundary');
321
+ if (boundary) {
322
+ // Error will be handled by the boundary
323
+ return;
324
+ }
325
+ }
326
+
327
+ // No boundary found - log to monitoring service
328
+ if (window.masterControllerErrorReporter) {
329
+ window.masterControllerErrorReporter({
330
+ type: 'uncaught-error',
331
+ error: event.error,
332
+ message: event.message,
333
+ filename: event.filename,
334
+ lineno: event.lineno,
335
+ colno: event.colno
336
+ });
337
+ }
338
+ });
339
+
340
+ // Catch unhandled promise rejections
341
+ window.addEventListener('unhandledrejection', (event) => {
342
+ console.error('[MasterController] Unhandled rejection:', event.reason);
343
+
344
+ if (window.masterControllerErrorReporter) {
345
+ window.masterControllerErrorReporter({
346
+ type: 'unhandled-rejection',
347
+ reason: event.reason
348
+ });
349
+ }
350
+ });
351
+ }
352
+
353
+ export { ErrorBoundary };