pulse-js-framework 1.11.3 → 1.11.4
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/cli/analyze.js +21 -8
- package/cli/build.js +83 -56
- package/cli/dev.js +108 -94
- package/cli/docs-test.js +52 -33
- package/cli/index.js +81 -51
- package/cli/mobile.js +92 -40
- package/cli/release.js +64 -46
- package/cli/scaffold.js +14 -13
- package/compiler/lexer.js +55 -54
- package/compiler/parser/core.js +1 -0
- package/compiler/parser/state.js +6 -12
- package/compiler/parser/style.js +17 -20
- package/compiler/parser/view.js +1 -3
- package/compiler/preprocessor.js +124 -262
- package/compiler/sourcemap.js +10 -4
- package/compiler/transformer/expressions.js +122 -106
- package/compiler/transformer/index.js +2 -4
- package/compiler/transformer/style.js +74 -7
- package/compiler/transformer/view.js +86 -36
- package/loader/esbuild-plugin-server-components.js +209 -0
- package/loader/esbuild-plugin.js +41 -93
- package/loader/parcel-plugin.js +37 -97
- package/loader/rollup-plugin-server-components.js +30 -169
- package/loader/rollup-plugin.js +27 -78
- package/loader/shared.js +362 -0
- package/loader/swc-plugin.js +65 -82
- package/loader/vite-plugin-server-components.js +30 -171
- package/loader/vite-plugin.js +25 -10
- package/loader/webpack-loader-server-components.js +21 -134
- package/loader/webpack-loader.js +25 -80
- package/package.json +52 -12
- package/runtime/dom-selector.js +2 -1
- package/runtime/form.js +4 -3
- package/runtime/http.js +6 -1
- package/runtime/logger.js +44 -24
- package/runtime/router/utils.js +14 -7
- package/runtime/security.js +13 -1
- package/runtime/server-components/actions-server.js +23 -19
- package/runtime/server-components/error-sanitizer.js +18 -18
- package/runtime/server-components/security.js +41 -24
- package/runtime/ssr-preload.js +5 -3
- package/runtime/testing.js +759 -0
- package/runtime/utils.js +3 -2
- package/server/utils.js +15 -9
- package/sw/index.js +2 -0
- package/types/loaders.d.ts +1043 -0
- package/compiler/parser/_extract.js +0 -393
- package/loader/README.md +0 -509
package/loader/webpack-loader.js
CHANGED
|
@@ -30,34 +30,12 @@
|
|
|
30
30
|
|
|
31
31
|
import { compile } from '../compiler/index.js';
|
|
32
32
|
import {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
getStylusVersion,
|
|
40
|
-
detectPreprocessor
|
|
41
|
-
} from '../compiler/preprocessor.js';
|
|
42
|
-
import { dirname } from 'path';
|
|
43
|
-
|
|
44
|
-
// Cache for preprocessor availability checks
|
|
45
|
-
let preprocessorCache = null;
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Check available preprocessors once
|
|
49
|
-
*/
|
|
50
|
-
function checkPreprocessors() {
|
|
51
|
-
if (preprocessorCache) return preprocessorCache;
|
|
52
|
-
|
|
53
|
-
preprocessorCache = {
|
|
54
|
-
sass: isSassAvailable(),
|
|
55
|
-
less: isLessAvailable(),
|
|
56
|
-
stylus: isStylusAvailable()
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
return preprocessorCache;
|
|
60
|
-
}
|
|
33
|
+
logPreprocessorAvailability,
|
|
34
|
+
extractCssFromOutput,
|
|
35
|
+
removeInlineStyles,
|
|
36
|
+
processStyles,
|
|
37
|
+
getPreprocessorOptions
|
|
38
|
+
} from './shared.js';
|
|
61
39
|
|
|
62
40
|
/**
|
|
63
41
|
* Webpack loader for .pulse files
|
|
@@ -98,42 +76,24 @@ export default function pulseLoader(source) {
|
|
|
98
76
|
let outputMap = result.map;
|
|
99
77
|
|
|
100
78
|
// Extract CSS from compiled output
|
|
101
|
-
const
|
|
79
|
+
const { css: extractedCss, found: cssFound } = extractCssFromOutput(outputCode);
|
|
102
80
|
|
|
103
|
-
if (
|
|
104
|
-
let css =
|
|
105
|
-
|
|
106
|
-
// Check available preprocessors
|
|
107
|
-
const available = checkPreprocessors();
|
|
108
|
-
const preprocessor = detectPreprocessor(css);
|
|
81
|
+
if (cssFound) {
|
|
82
|
+
let css = extractedCss;
|
|
109
83
|
|
|
110
84
|
// Preprocess if preprocessor detected and available
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
preprocessor // Force detected preprocessor
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
css = preprocessed.css;
|
|
127
|
-
|
|
128
|
-
// Log preprocessor usage in verbose mode
|
|
129
|
-
if (preprocessorOptions.verbose) {
|
|
130
|
-
console.log(`[Pulse] Compiled ${preprocessor.toUpperCase()} in ${this.resourcePath}`);
|
|
131
|
-
}
|
|
132
|
-
} catch (preprocessorError) {
|
|
133
|
-
// Emit warning but continue with original CSS
|
|
134
|
-
this.emitWarning(
|
|
135
|
-
new Error(`${preprocessor.toUpperCase()} compilation warning: ${preprocessorError.message}`)
|
|
136
|
-
);
|
|
85
|
+
const styleResult = processStyles(css, this.resourcePath, { sassOptions, lessOptions, stylusOptions });
|
|
86
|
+
css = styleResult.css;
|
|
87
|
+
|
|
88
|
+
if (styleResult.warning) {
|
|
89
|
+
this.emitWarning(new Error(styleResult.warning));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Log preprocessor usage in verbose mode
|
|
93
|
+
if (styleResult.preprocessor !== 'none') {
|
|
94
|
+
const preprocessorOptions = getPreprocessorOptions(styleResult.preprocessor, { sassOptions, lessOptions, stylusOptions });
|
|
95
|
+
if (preprocessorOptions?.verbose) {
|
|
96
|
+
console.log(`[Pulse] Compiled ${styleResult.preprocessor.toUpperCase()} in ${this.resourcePath}`);
|
|
137
97
|
}
|
|
138
98
|
}
|
|
139
99
|
|
|
@@ -148,8 +108,8 @@ export default function pulseLoader(source) {
|
|
|
148
108
|
|
|
149
109
|
// Replace inline CSS injection with import statement
|
|
150
110
|
// css-loader will process the CSS and style-loader will inject it
|
|
151
|
-
outputCode =
|
|
152
|
-
|
|
111
|
+
outputCode = removeInlineStyles(
|
|
112
|
+
outputCode,
|
|
153
113
|
`// Styles extracted - handled by css-loader\nimport "./${this.resourcePath.split('/').pop().replace(/\.pulse$/, '.pulse.css')}";`
|
|
154
114
|
);
|
|
155
115
|
}
|
|
@@ -200,26 +160,11 @@ if (module.hot) {
|
|
|
200
160
|
* Used to log preprocessor availability
|
|
201
161
|
*/
|
|
202
162
|
export function pitch() {
|
|
203
|
-
const available = checkPreprocessors();
|
|
204
163
|
const options = this.getOptions() || {};
|
|
205
164
|
|
|
206
165
|
// Log preprocessor availability once on first run
|
|
207
|
-
if (!pulseLoader._logged && options.verbose !== false) {
|
|
208
|
-
|
|
209
|
-
if (available.sass) {
|
|
210
|
-
preprocessors.push(`SASS ${getSassVersion() || 'unknown'}`);
|
|
211
|
-
}
|
|
212
|
-
if (available.less) {
|
|
213
|
-
preprocessors.push(`LESS ${getLessVersion() || 'unknown'}`);
|
|
214
|
-
}
|
|
215
|
-
if (available.stylus) {
|
|
216
|
-
preprocessors.push(`Stylus ${getStylusVersion() || 'unknown'}`);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if (preprocessors.length > 0) {
|
|
220
|
-
console.log(`[Pulse Webpack] Preprocessor support: ${preprocessors.join(', ')}`);
|
|
221
|
-
}
|
|
222
|
-
|
|
166
|
+
if (!pulseLoader._logged && options.verbose !== false && !options.quiet) {
|
|
167
|
+
logPreprocessorAvailability('Pulse Webpack');
|
|
223
168
|
pulseLoader._logged = true;
|
|
224
169
|
}
|
|
225
170
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pulse-js-framework",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.4",
|
|
4
4
|
"description": "A declarative DOM framework with CSS selector-based structure and reactive pulsations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -100,17 +100,45 @@
|
|
|
100
100
|
},
|
|
101
101
|
"./core/errors": "./runtime/errors.js",
|
|
102
102
|
"./vite": {
|
|
103
|
-
"types": "./types/
|
|
103
|
+
"types": "./types/loaders.d.ts",
|
|
104
104
|
"default": "./loader/vite-plugin.js"
|
|
105
105
|
},
|
|
106
|
-
"./vite/server-components":
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
"./
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
106
|
+
"./vite/server-components": {
|
|
107
|
+
"types": "./types/loaders.d.ts",
|
|
108
|
+
"default": "./loader/vite-plugin-server-components.js"
|
|
109
|
+
},
|
|
110
|
+
"./webpack": {
|
|
111
|
+
"types": "./types/loaders.d.ts",
|
|
112
|
+
"default": "./loader/webpack-loader.js"
|
|
113
|
+
},
|
|
114
|
+
"./webpack/server-components": {
|
|
115
|
+
"types": "./types/loaders.d.ts",
|
|
116
|
+
"default": "./loader/webpack-loader-server-components.js"
|
|
117
|
+
},
|
|
118
|
+
"./rollup": {
|
|
119
|
+
"types": "./types/loaders.d.ts",
|
|
120
|
+
"default": "./loader/rollup-plugin.js"
|
|
121
|
+
},
|
|
122
|
+
"./rollup/server-components": {
|
|
123
|
+
"types": "./types/loaders.d.ts",
|
|
124
|
+
"default": "./loader/rollup-plugin-server-components.js"
|
|
125
|
+
},
|
|
126
|
+
"./esbuild": {
|
|
127
|
+
"types": "./types/loaders.d.ts",
|
|
128
|
+
"default": "./loader/esbuild-plugin.js"
|
|
129
|
+
},
|
|
130
|
+
"./esbuild/server-components": {
|
|
131
|
+
"types": "./types/loaders.d.ts",
|
|
132
|
+
"default": "./loader/esbuild-plugin-server-components.js"
|
|
133
|
+
},
|
|
134
|
+
"./parcel": {
|
|
135
|
+
"types": "./types/loaders.d.ts",
|
|
136
|
+
"default": "./loader/parcel-plugin.js"
|
|
137
|
+
},
|
|
138
|
+
"./swc": {
|
|
139
|
+
"types": "./types/loaders.d.ts",
|
|
140
|
+
"default": "./loader/swc-plugin.js"
|
|
141
|
+
},
|
|
114
142
|
"./mobile": "./mobile/bridge/pulse-native.js",
|
|
115
143
|
"./runtime/ssr": "./runtime/ssr.js",
|
|
116
144
|
"./runtime/ssr-stream": "./runtime/ssr-stream.js",
|
|
@@ -142,6 +170,9 @@
|
|
|
142
170
|
"types": "./types/sw.d.ts",
|
|
143
171
|
"default": "./sw/index.js"
|
|
144
172
|
},
|
|
173
|
+
"./testing": {
|
|
174
|
+
"default": "./runtime/testing.js"
|
|
175
|
+
},
|
|
145
176
|
"./package.json": "./package.json"
|
|
146
177
|
},
|
|
147
178
|
"files": [
|
|
@@ -187,6 +218,7 @@
|
|
|
187
218
|
"test:css-parsing": "node test/css-parsing.test.js",
|
|
188
219
|
"test:dev-server": "node --test test/dev-server.test.js",
|
|
189
220
|
"test:devtools": "node test/devtools.test.js",
|
|
221
|
+
"test:directives": "node --test test/directives.test.js",
|
|
190
222
|
"test:docs": "node test/docs.test.js",
|
|
191
223
|
"test:docs-nav": "node test/docs-navigation.test.js",
|
|
192
224
|
"test:docs-navigation": "node test/docs-navigation.test.js",
|
|
@@ -208,6 +240,7 @@
|
|
|
208
240
|
"test:error-sanitizer": "node --test test/error-sanitizer.test.js",
|
|
209
241
|
"test:errors": "node test/errors.test.js",
|
|
210
242
|
"test:esbuild-plugin": "node test/esbuild-plugin.test.js",
|
|
243
|
+
"test:expressions-coverage-boost": "node --test test/expressions-coverage-boost.test.js",
|
|
211
244
|
"test:form": "node test/form.test.js",
|
|
212
245
|
"test:form-coverage": "node test/form-coverage.test.js",
|
|
213
246
|
"test:form-edge-cases": "node test/form-edge-cases.test.js",
|
|
@@ -222,11 +255,14 @@
|
|
|
222
255
|
"test:http": "node test/http.test.js",
|
|
223
256
|
"test:http-edge-cases": "node test/http-edge-cases.test.js",
|
|
224
257
|
"test:i18n": "node --test test/i18n.test.js",
|
|
258
|
+
"test:imports-coverage-boost": "node --test test/imports-coverage-boost.test.js",
|
|
225
259
|
"test:integration": "node test/integration.test.js",
|
|
226
260
|
"test:integration-advanced": "node test/integration-advanced.test.js",
|
|
227
261
|
"test:interceptor-manager": "node test/interceptor-manager.test.js",
|
|
262
|
+
"test:lexer-coverage-boost": "node --test test/lexer-coverage-boost.test.js",
|
|
228
263
|
"test:lint": "node test/lint.test.js",
|
|
229
264
|
"test:lite": "node test/lite.test.js",
|
|
265
|
+
"test:loader-shared": "node test/loader-shared.test.js",
|
|
230
266
|
"test:logger": "node test/logger.test.js",
|
|
231
267
|
"test:logger-coverage-boost": "node --test test/logger-coverage-boost.test.js",
|
|
232
268
|
"test:logger-prod": "node test/logger-prod.test.js",
|
|
@@ -236,12 +272,13 @@
|
|
|
236
272
|
"test:native": "node test/native.test.js",
|
|
237
273
|
"test:native-coverage-boost": "node --test test/native-coverage-boost.test.js",
|
|
238
274
|
"test:parcel-plugin": "node test/parcel-plugin.test.js",
|
|
239
|
-
"test:path-sanitizer": "node --test test/path-sanitizer.test.js",
|
|
240
275
|
"test:parser-coverage": "node test/parser-coverage.test.js",
|
|
276
|
+
"test:path-sanitizer": "node --test test/path-sanitizer.test.js",
|
|
241
277
|
"test:persistence": "node --test test/persistence.test.js",
|
|
242
278
|
"test:persistence-coverage-boost": "node --test test/persistence-coverage-boost.test.js",
|
|
243
279
|
"test:portal": "node --test test/portal.test.js",
|
|
244
280
|
"test:preprocessor": "node test/preprocessor.test.js",
|
|
281
|
+
"test:preprocessor-coverage-boost": "node --test test/preprocessor-coverage-boost.test.js",
|
|
245
282
|
"test:pulse": "node test/pulse.test.js",
|
|
246
283
|
"test:rollup-plugin": "node test/rollup-plugin.test.js",
|
|
247
284
|
"test:router": "node test/router.test.js",
|
|
@@ -251,7 +288,6 @@
|
|
|
251
288
|
"test:security": "node test/security.test.js",
|
|
252
289
|
"test:security-coverage-boost": "node --test test/security-coverage-boost.test.js",
|
|
253
290
|
"test:security-regression": "node test/security-regression.test.js",
|
|
254
|
-
"test:server-utils": "node --test test/server-utils.test.js",
|
|
255
291
|
"test:server-actions": "node --test test/server-actions.test.js",
|
|
256
292
|
"test:server-actions-client": "node --test test/server-actions-client.test.js",
|
|
257
293
|
"test:server-actions-server-extended": "node --test test/server-actions-server-extended.test.js",
|
|
@@ -267,6 +303,7 @@
|
|
|
267
303
|
"test:server-components-security-comprehensive": "node --test test/server-components-security-comprehensive.test.js",
|
|
268
304
|
"test:server-components-serializer": "node --test test/server-components-serializer.test.js",
|
|
269
305
|
"test:server-components-validation": "node --test test/server-components-validation.test.js",
|
|
306
|
+
"test:server-utils": "node --test test/server-utils.test.js",
|
|
270
307
|
"test:sourcemap": "node test/sourcemap.test.js",
|
|
271
308
|
"test:sourcemap-coverage-boost": "node --test test/sourcemap-coverage-boost.test.js",
|
|
272
309
|
"test:sse": "node --test test/sse.test.js",
|
|
@@ -278,13 +315,16 @@
|
|
|
278
315
|
"test:ssr-preload": "node --test test/ssr-preload.test.js",
|
|
279
316
|
"test:ssr-stream": "node --test test/ssr-stream.test.js",
|
|
280
317
|
"test:store": "node test/store.test.js",
|
|
318
|
+
"test:style-coverage-boost": "node --test test/style-coverage-boost.test.js",
|
|
281
319
|
"test:sw": "node --test test/sw.test.js",
|
|
282
320
|
"test:swc-plugin": "node test/swc-plugin.test.js",
|
|
283
321
|
"test:sync": "node scripts/sync-tests.js",
|
|
284
322
|
"test:sync:fix": "node scripts/sync-tests.js --fix",
|
|
285
323
|
"test:test-runner": "node test/test-runner.test.js",
|
|
324
|
+
"test:testing": "node --test test/testing.test.js",
|
|
286
325
|
"test:utils": "node test/utils.test.js",
|
|
287
326
|
"test:utils-coverage": "node test/utils-coverage.test.js",
|
|
327
|
+
"test:view-coverage-boost": "node --test test/view-coverage-boost.test.js",
|
|
288
328
|
"test:vite-plugin": "node --test test/vite-plugin.test.js",
|
|
289
329
|
"test:webpack-loader": "node test/webpack-loader.test.js",
|
|
290
330
|
"test:websocket": "node test/websocket.test.js",
|
package/runtime/dom-selector.js
CHANGED
|
@@ -222,7 +222,8 @@ export function parseSelector(selector) {
|
|
|
222
222
|
|
|
223
223
|
// Match attributes - improved regex handles quoted values with special characters
|
|
224
224
|
// Matches: [attr], [attr=value], [attr="quoted value"], [attr='quoted value']
|
|
225
|
-
|
|
225
|
+
// Uses possessive-style matching via atomic groups to avoid backtracking on malformed input
|
|
226
|
+
const attrRegex = /\[([a-zA-Z_][a-zA-Z0-9_-]*)(?:=(?:"([^"]*)"|'([^']*)'|([^\]="']*)))?\]/g;
|
|
226
227
|
const attrMatches = remaining.matchAll(attrRegex);
|
|
227
228
|
for (const match of attrMatches) {
|
|
228
229
|
const key = match[1];
|
package/runtime/form.js
CHANGED
|
@@ -98,7 +98,8 @@ export const validators = {
|
|
|
98
98
|
email: (message = 'Invalid email address') => ({
|
|
99
99
|
validate: (value) => {
|
|
100
100
|
if (!value) return true;
|
|
101
|
-
|
|
101
|
+
// Anchored with atomic-style matching: limit local part and domain to avoid ReDoS
|
|
102
|
+
const emailRegex = /^[^\s@]{1,64}@[^\s@]{1,253}\.[^\s@]{1,63}$/;
|
|
102
103
|
return emailRegex.test(value) || message;
|
|
103
104
|
}
|
|
104
105
|
}),
|
|
@@ -298,8 +299,8 @@ export const validators = {
|
|
|
298
299
|
debounce: options.debounce ?? 300,
|
|
299
300
|
validate: async (value, allValues) => {
|
|
300
301
|
if (!value) return true;
|
|
301
|
-
// First check format synchronously
|
|
302
|
-
const emailRegex = /^[^\s@]
|
|
302
|
+
// First check format synchronously (length-bounded to prevent ReDoS)
|
|
303
|
+
const emailRegex = /^[^\s@]{1,64}@[^\s@]{1,253}\.[^\s@]{1,63}$/;
|
|
303
304
|
if (!emailRegex.test(value)) {
|
|
304
305
|
return 'Invalid email address';
|
|
305
306
|
}
|
package/runtime/http.js
CHANGED
|
@@ -119,7 +119,12 @@ class HttpClient {
|
|
|
119
119
|
|
|
120
120
|
// Prepend baseURL if url is relative
|
|
121
121
|
if (config.baseURL && !url.startsWith('http://') && !url.startsWith('https://')) {
|
|
122
|
-
|
|
122
|
+
// Use indexOf/slice instead of regex to avoid ReDoS with repeated '/' chars
|
|
123
|
+
let base = config.baseURL;
|
|
124
|
+
while (base.endsWith('/')) base = base.slice(0, -1);
|
|
125
|
+
let path = url;
|
|
126
|
+
while (path.startsWith('/')) path = path.slice(1);
|
|
127
|
+
fullURL = base + '/' + path;
|
|
123
128
|
}
|
|
124
129
|
|
|
125
130
|
// Add query parameters
|
package/runtime/logger.js
CHANGED
|
@@ -187,6 +187,7 @@ const noopLogger = {
|
|
|
187
187
|
// Development Logger Implementation
|
|
188
188
|
// ============================================================================
|
|
189
189
|
|
|
190
|
+
|
|
190
191
|
/**
|
|
191
192
|
* Format message arguments with optional namespace prefix
|
|
192
193
|
* @private
|
|
@@ -233,56 +234,74 @@ function formatArgs(namespace, args) {
|
|
|
233
234
|
*/
|
|
234
235
|
function createDevLogger(namespace, options) {
|
|
235
236
|
const localLevel = options.level;
|
|
237
|
+
// Sanitize namespace once at creation to prevent log injection
|
|
238
|
+
const safeNamespace = typeof namespace === 'string' ? namespace.replace(/[\r\n\x00-\x1f]/g, '') : namespace;
|
|
236
239
|
|
|
237
240
|
function shouldLog(level) {
|
|
238
241
|
const effectiveLevel = localLevel !== undefined ? localLevel : globalLevel;
|
|
239
242
|
return level <= effectiveLevel;
|
|
240
243
|
}
|
|
241
244
|
|
|
245
|
+
/** Sanitize a single log argument to prevent log injection */
|
|
246
|
+
function sanitizeArg(a) {
|
|
247
|
+
if (typeof a === 'string') return a.replace(/[\r\n\x00-\x1f]/g, '');
|
|
248
|
+
if (a === null || a === undefined || typeof a === 'number' || typeof a === 'boolean') return a;
|
|
249
|
+
// Deep-clone objects via JSON round-trip to break taint chain while preserving structure
|
|
250
|
+
try { return JSON.parse(JSON.stringify(a)); } catch { return '[Object]'; }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Sanitize formatter output to prevent log injection */
|
|
254
|
+
function safeFormat(level, args) {
|
|
255
|
+
const result = globalFormatter(level, safeNamespace, args);
|
|
256
|
+
return typeof result === 'string' ? result.replace(/[\r\n\x00-\x1f]/g, '') : String(result).replace(/[\r\n\x00-\x1f]/g, '');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Build sanitized log line */
|
|
260
|
+
function logWith(consoleFn, args) {
|
|
261
|
+
const safe = args.map(sanitizeArg);
|
|
262
|
+
if (safeNamespace) {
|
|
263
|
+
const p = formatNamespace(safeNamespace);
|
|
264
|
+
const first = safe.length > 0 && typeof safe[0] === 'string' ? `${p} ${safe[0]}` : p;
|
|
265
|
+
const rest = typeof safe[0] === 'string' ? safe.slice(1) : safe;
|
|
266
|
+
consoleFn(first, ...rest);
|
|
267
|
+
} else {
|
|
268
|
+
consoleFn(...safe);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
242
272
|
return {
|
|
243
273
|
error(...args) {
|
|
244
274
|
if (shouldLog(LogLevel.ERROR)) {
|
|
245
|
-
if (globalFormatter) {
|
|
246
|
-
|
|
247
|
-
} else {
|
|
248
|
-
console.error(...formatArgs(namespace, args));
|
|
249
|
-
}
|
|
275
|
+
if (globalFormatter) { console.error(safeFormat('error', args.map(sanitizeArg))); }
|
|
276
|
+
else { logWith(console.error, args); }
|
|
250
277
|
}
|
|
251
278
|
},
|
|
252
279
|
|
|
253
280
|
warn(...args) {
|
|
254
281
|
if (shouldLog(LogLevel.WARN)) {
|
|
255
|
-
if (globalFormatter) {
|
|
256
|
-
|
|
257
|
-
} else {
|
|
258
|
-
console.warn(...formatArgs(namespace, args));
|
|
259
|
-
}
|
|
282
|
+
if (globalFormatter) { console.warn(safeFormat('warn', args.map(sanitizeArg))); }
|
|
283
|
+
else { logWith(console.warn, args); }
|
|
260
284
|
}
|
|
261
285
|
},
|
|
262
286
|
|
|
263
287
|
info(...args) {
|
|
264
288
|
if (shouldLog(LogLevel.INFO)) {
|
|
265
|
-
if (globalFormatter) {
|
|
266
|
-
|
|
267
|
-
} else {
|
|
268
|
-
console.log(...formatArgs(namespace, args));
|
|
269
|
-
}
|
|
289
|
+
if (globalFormatter) { console.log(safeFormat('info', args.map(sanitizeArg))); }
|
|
290
|
+
else { logWith(console.log, args); }
|
|
270
291
|
}
|
|
271
292
|
},
|
|
272
293
|
|
|
273
294
|
debug(...args) {
|
|
274
295
|
if (shouldLog(LogLevel.DEBUG)) {
|
|
275
|
-
if (globalFormatter) {
|
|
276
|
-
|
|
277
|
-
} else {
|
|
278
|
-
console.log(...formatArgs(namespace, args));
|
|
279
|
-
}
|
|
296
|
+
if (globalFormatter) { console.log(safeFormat('debug', args.map(sanitizeArg))); }
|
|
297
|
+
else { logWith(console.log, args); }
|
|
280
298
|
}
|
|
281
299
|
},
|
|
282
300
|
|
|
283
301
|
group(label) {
|
|
284
302
|
if (shouldLog(LogLevel.DEBUG)) {
|
|
285
|
-
|
|
303
|
+
const safeLabel = typeof label === 'string' ? label.replace(/[\r\n\x00-\x1f]/g, '') : label;
|
|
304
|
+
console.group(safeNamespace ? `${formatNamespace(safeNamespace)} ${safeLabel}` : safeLabel);
|
|
286
305
|
}
|
|
287
306
|
},
|
|
288
307
|
|
|
@@ -294,7 +313,8 @@ function createDevLogger(namespace, options) {
|
|
|
294
313
|
|
|
295
314
|
log(level, ...args) {
|
|
296
315
|
if (shouldLog(level)) {
|
|
297
|
-
const
|
|
316
|
+
const safe = args.map(a => typeof a === 'string' ? a.replace(/[\r\n\x00-\x1f]/g, '') : a);
|
|
317
|
+
const formatted = formatArgs(safeNamespace, safe);
|
|
298
318
|
switch (level) {
|
|
299
319
|
case LogLevel.ERROR:
|
|
300
320
|
console.error(...formatted);
|
|
@@ -309,8 +329,8 @@ function createDevLogger(namespace, options) {
|
|
|
309
329
|
},
|
|
310
330
|
|
|
311
331
|
child(childNamespace) {
|
|
312
|
-
const combined =
|
|
313
|
-
? `${
|
|
332
|
+
const combined = safeNamespace
|
|
333
|
+
? `${safeNamespace}${NAMESPACE_SEPARATOR}${childNamespace}`
|
|
314
334
|
: childNamespace;
|
|
315
335
|
return createLogger(combined, options);
|
|
316
336
|
}
|
package/runtime/router/utils.js
CHANGED
|
@@ -188,7 +188,8 @@ export function parseQuery(search, options = {}) {
|
|
|
188
188
|
}
|
|
189
189
|
|
|
190
190
|
const params = new URLSearchParams(queryStr);
|
|
191
|
-
|
|
191
|
+
// SECURITY: Use Map to avoid property injection via user-controlled keys
|
|
192
|
+
const query = new Map();
|
|
192
193
|
let paramCount = 0;
|
|
193
194
|
|
|
194
195
|
for (const [key, value] of params) {
|
|
@@ -198,6 +199,11 @@ export function parseQuery(search, options = {}) {
|
|
|
198
199
|
break;
|
|
199
200
|
}
|
|
200
201
|
|
|
202
|
+
// SECURITY: Skip prototype-polluting keys to prevent remote property injection
|
|
203
|
+
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
|
|
201
207
|
// Validate and potentially truncate value length
|
|
202
208
|
let safeValue = value;
|
|
203
209
|
if (value.length > QUERY_LIMITS.maxValueLength) {
|
|
@@ -210,17 +216,18 @@ export function parseQuery(search, options = {}) {
|
|
|
210
216
|
safeValue = parseTypedValue(safeValue);
|
|
211
217
|
}
|
|
212
218
|
|
|
213
|
-
if (key
|
|
219
|
+
if (query.has(key)) {
|
|
214
220
|
// Multiple values for same key
|
|
215
|
-
|
|
216
|
-
|
|
221
|
+
const existing = query.get(key);
|
|
222
|
+
if (Array.isArray(existing)) {
|
|
223
|
+
existing.push(safeValue);
|
|
217
224
|
} else {
|
|
218
|
-
query
|
|
225
|
+
query.set(key, [existing, safeValue]);
|
|
219
226
|
}
|
|
220
227
|
} else {
|
|
221
|
-
query
|
|
228
|
+
query.set(key, safeValue);
|
|
222
229
|
}
|
|
223
230
|
paramCount++;
|
|
224
231
|
}
|
|
225
|
-
return query;
|
|
232
|
+
return Object.fromEntries(query);
|
|
226
233
|
}
|
package/runtime/security.js
CHANGED
|
@@ -280,7 +280,19 @@ export function sanitizeHtml(html, options = {}) {
|
|
|
280
280
|
// Use browser's DOMParser for safe parsing
|
|
281
281
|
if (typeof DOMParser === 'undefined') {
|
|
282
282
|
// Fallback for non-browser environments: strip all tags
|
|
283
|
-
|
|
283
|
+
// Use a loop-based approach to avoid O(n^2) regex backtracking on strings with many '<'
|
|
284
|
+
let result = '';
|
|
285
|
+
let inTag = false;
|
|
286
|
+
for (let i = 0; i < html.length; i++) {
|
|
287
|
+
if (html[i] === '<') {
|
|
288
|
+
inTag = true;
|
|
289
|
+
} else if (html[i] === '>') {
|
|
290
|
+
inTag = false;
|
|
291
|
+
} else if (!inTag) {
|
|
292
|
+
result += html[i];
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return result;
|
|
284
296
|
}
|
|
285
297
|
|
|
286
298
|
const parser = new DOMParser();
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* @module pulse-js-framework/runtime/server-components/actions-server
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { sanitizeError
|
|
10
|
+
import { sanitizeError } from './error-sanitizer.js';
|
|
11
11
|
import { CSRFTokenStore } from './security-csrf.js';
|
|
12
12
|
import { createRateLimitMiddleware } from './security-ratelimit.js';
|
|
13
13
|
|
|
@@ -357,22 +357,24 @@ export function createServerActionMiddleware(options = {}) {
|
|
|
357
357
|
res.json(result);
|
|
358
358
|
} catch (error) {
|
|
359
359
|
if (onError) {
|
|
360
|
-
// Custom error handler -
|
|
360
|
+
// Custom error handler - sanitize before passing, never expose stack traces
|
|
361
361
|
const sanitized = sanitizeError(error, {
|
|
362
|
-
includeStack:
|
|
363
|
-
maxStackLines:
|
|
362
|
+
includeStack: false,
|
|
363
|
+
maxStackLines: 0
|
|
364
364
|
});
|
|
365
|
-
onError(sanitized, req, res);
|
|
365
|
+
onError({ message: String(sanitized.message || 'Internal Server Error'), code: sanitized.code || 'INTERNAL_ERROR' }, req, res);
|
|
366
366
|
} else {
|
|
367
|
-
// Default error handler -
|
|
367
|
+
// Default error handler - build response from scratch to prevent stack trace leakage
|
|
368
368
|
const sanitized = sanitizeError(error, {
|
|
369
|
-
includeStack:
|
|
370
|
-
maxStackLines:
|
|
369
|
+
includeStack: false,
|
|
370
|
+
maxStackLines: 0
|
|
371
371
|
});
|
|
372
|
-
//
|
|
372
|
+
// Only expose safe, known fields - never spread the sanitized object
|
|
373
|
+
const safeMessage = String(sanitized.message || 'Internal Server Error');
|
|
373
374
|
res.status(500).json({
|
|
374
|
-
|
|
375
|
-
error:
|
|
375
|
+
message: safeMessage,
|
|
376
|
+
error: safeMessage,
|
|
377
|
+
code: sanitized.code || 'INTERNAL_ERROR'
|
|
376
378
|
});
|
|
377
379
|
}
|
|
378
380
|
}
|
|
@@ -533,12 +535,13 @@ export async function createFastifyActionPlugin(fastify, options = {}) {
|
|
|
533
535
|
|
|
534
536
|
return result;
|
|
535
537
|
} catch (error) {
|
|
536
|
-
// Sanitize error before sending to client
|
|
538
|
+
// Sanitize error before sending to client - never expose stack traces
|
|
537
539
|
const sanitized = sanitizeError(error, {
|
|
538
|
-
includeStack:
|
|
539
|
-
maxStackLines:
|
|
540
|
+
includeStack: false,
|
|
541
|
+
maxStackLines: 0
|
|
540
542
|
});
|
|
541
|
-
|
|
543
|
+
const safeMsg = String(sanitized.message || 'Internal Server Error');
|
|
544
|
+
return reply.code(500).send({ message: safeMsg, error: safeMsg, code: sanitized.code || 'INTERNAL_ERROR' });
|
|
542
545
|
}
|
|
543
546
|
});
|
|
544
547
|
}
|
|
@@ -697,12 +700,13 @@ export function createHonoActionMiddleware(options = {}) {
|
|
|
697
700
|
|
|
698
701
|
return c.json(result);
|
|
699
702
|
} catch (error) {
|
|
700
|
-
// Sanitize error before sending to client
|
|
703
|
+
// Sanitize error before sending to client - never expose stack traces
|
|
701
704
|
const sanitized = sanitizeError(error, {
|
|
702
|
-
includeStack:
|
|
703
|
-
maxStackLines:
|
|
705
|
+
includeStack: false,
|
|
706
|
+
maxStackLines: 0
|
|
704
707
|
});
|
|
705
|
-
|
|
708
|
+
const safeMsg = String(sanitized.message || 'Internal Server Error');
|
|
709
|
+
return c.json({ message: safeMsg, error: safeMsg, code: sanitized.code || 'INTERNAL_ERROR' }, 500);
|
|
706
710
|
}
|
|
707
711
|
};
|
|
708
712
|
}
|