pulse-js-framework 1.5.1 → 1.5.3
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/index.js +2 -0
- package/cli/release.js +245 -33
- package/package.json +1 -1
- package/runtime/dom.js +76 -26
- package/runtime/logger.js +12 -4
- package/runtime/lru-cache.js +53 -1
- package/runtime/pulse.js +158 -13
- package/runtime/router.js +183 -28
- package/runtime/store.js +128 -3
- package/runtime/utils.js +2 -2
package/runtime/store.js
CHANGED
|
@@ -20,6 +20,12 @@ import { loggers, createLogger } from './logger.js';
|
|
|
20
20
|
|
|
21
21
|
const log = loggers.store;
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Maximum nesting depth for nested objects to prevent abuse
|
|
25
|
+
* @type {number}
|
|
26
|
+
*/
|
|
27
|
+
const MAX_NESTING_DEPTH = 10;
|
|
28
|
+
|
|
23
29
|
/**
|
|
24
30
|
* @typedef {Object} StoreOptions
|
|
25
31
|
* @property {boolean} [persist=false] - Persist state to localStorage
|
|
@@ -46,6 +52,102 @@ const log = loggers.store;
|
|
|
46
52
|
* @typedef {function(Store): Store} StorePlugin
|
|
47
53
|
*/
|
|
48
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Dangerous property names that could cause prototype pollution
|
|
57
|
+
* @type {Set<string>}
|
|
58
|
+
*/
|
|
59
|
+
const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Invalid value types that cannot be stored in state
|
|
63
|
+
* @type {Set<string>}
|
|
64
|
+
*/
|
|
65
|
+
const INVALID_TYPES = new Set(['function', 'symbol']);
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Validate state values, rejecting functions, symbols, and circular references
|
|
69
|
+
* @private
|
|
70
|
+
* @param {*} value - Value to validate
|
|
71
|
+
* @param {string} path - Current path for error messages
|
|
72
|
+
* @param {WeakSet} seen - Set of objects already visited (for circular detection)
|
|
73
|
+
* @throws {TypeError} If value contains invalid types or circular references
|
|
74
|
+
*/
|
|
75
|
+
function validateStateValue(value, path = 'state', seen = new WeakSet()) {
|
|
76
|
+
const valueType = typeof value;
|
|
77
|
+
|
|
78
|
+
// Check for invalid types
|
|
79
|
+
if (INVALID_TYPES.has(valueType)) {
|
|
80
|
+
throw new TypeError(
|
|
81
|
+
`Invalid state value at "${path}": ${valueType}s cannot be stored in state. ` +
|
|
82
|
+
`State values must be primitives, arrays, or plain objects.`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Check objects for circular references and nested invalid types
|
|
87
|
+
if (value !== null && valueType === 'object') {
|
|
88
|
+
// Check for circular reference
|
|
89
|
+
if (seen.has(value)) {
|
|
90
|
+
throw new TypeError(
|
|
91
|
+
`Circular reference detected at "${path}". ` +
|
|
92
|
+
`State must not contain circular references.`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
seen.add(value);
|
|
96
|
+
|
|
97
|
+
// Validate array elements
|
|
98
|
+
if (Array.isArray(value)) {
|
|
99
|
+
for (let i = 0; i < value.length; i++) {
|
|
100
|
+
validateStateValue(value[i], `${path}[${i}]`, seen);
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
// Validate object properties
|
|
104
|
+
for (const [key, val] of Object.entries(value)) {
|
|
105
|
+
validateStateValue(val, `${path}.${key}`, seen);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Safely deserialize persisted state, preventing prototype pollution
|
|
113
|
+
* and property injection attacks.
|
|
114
|
+
* @private
|
|
115
|
+
* @param {Object} savedState - The parsed JSON state
|
|
116
|
+
* @param {Object} schema - The initial state defining allowed keys
|
|
117
|
+
* @returns {Object} Sanitized state object
|
|
118
|
+
*/
|
|
119
|
+
function safeDeserialize(savedState, schema) {
|
|
120
|
+
if (typeof savedState !== 'object' || savedState === null || Array.isArray(savedState)) {
|
|
121
|
+
return {};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const result = {};
|
|
125
|
+
for (const [key, value] of Object.entries(savedState)) {
|
|
126
|
+
// Block dangerous keys that could pollute prototypes
|
|
127
|
+
if (DANGEROUS_KEYS.has(key)) {
|
|
128
|
+
log.warn(`Blocked potentially dangerous key in persisted state: "${key}"`);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Only allow keys that exist in the schema (initial state)
|
|
133
|
+
if (!(key in schema)) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Recursively validate nested objects
|
|
138
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
139
|
+
if (typeof schema[key] === 'object' && schema[key] !== null && !Array.isArray(schema[key])) {
|
|
140
|
+
result[key] = safeDeserialize(value, schema[key]);
|
|
141
|
+
}
|
|
142
|
+
// If schema expects primitive but got object, skip it
|
|
143
|
+
} else {
|
|
144
|
+
result[key] = value;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
|
|
49
151
|
/**
|
|
50
152
|
* Create a global store with reactive state properties.
|
|
51
153
|
* @template T
|
|
@@ -70,13 +172,18 @@ const log = loggers.store;
|
|
|
70
172
|
export function createStore(initialState = {}, options = {}) {
|
|
71
173
|
const { persist = false, storageKey = 'pulse-store' } = options;
|
|
72
174
|
|
|
175
|
+
// Validate initial state
|
|
176
|
+
validateStateValue(initialState, 'initialState');
|
|
177
|
+
|
|
73
178
|
// Load persisted state if enabled
|
|
74
179
|
let state = initialState;
|
|
75
180
|
if (persist && typeof localStorage !== 'undefined') {
|
|
76
181
|
try {
|
|
77
182
|
const saved = localStorage.getItem(storageKey);
|
|
78
183
|
if (saved) {
|
|
79
|
-
|
|
184
|
+
const parsed = JSON.parse(saved);
|
|
185
|
+
const sanitized = safeDeserialize(parsed, initialState);
|
|
186
|
+
state = { ...initialState, ...sanitized };
|
|
80
187
|
}
|
|
81
188
|
} catch (e) {
|
|
82
189
|
log.warn('Failed to load persisted state:', e);
|
|
@@ -93,9 +200,18 @@ export function createStore(initialState = {}, options = {}) {
|
|
|
93
200
|
* @private
|
|
94
201
|
* @param {string} key - State key
|
|
95
202
|
* @param {*} value - Initial value
|
|
203
|
+
* @param {number} [depth=0] - Current nesting depth
|
|
96
204
|
* @returns {Pulse|Object} Pulse or nested object of pulses
|
|
97
205
|
*/
|
|
98
|
-
function createPulse(key, value) {
|
|
206
|
+
function createPulse(key, value, depth = 0) {
|
|
207
|
+
// Prevent excessive nesting depth
|
|
208
|
+
if (depth > MAX_NESTING_DEPTH) {
|
|
209
|
+
log.warn(`Max nesting depth (${MAX_NESTING_DEPTH}) exceeded for key: "${key}". Flattening to single pulse.`);
|
|
210
|
+
const p = pulse(value);
|
|
211
|
+
pulses[key] = p;
|
|
212
|
+
return p;
|
|
213
|
+
}
|
|
214
|
+
|
|
99
215
|
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
100
216
|
// Create a pulse for the nested object itself (for $setState support)
|
|
101
217
|
const objectPulse = pulse(value);
|
|
@@ -104,7 +220,7 @@ export function createStore(initialState = {}, options = {}) {
|
|
|
104
220
|
// Also create nested pulses for individual properties
|
|
105
221
|
const nested = {};
|
|
106
222
|
for (const [k, v] of Object.entries(value)) {
|
|
107
|
-
nested[k] = createPulse(`${key}.${k}`, v);
|
|
223
|
+
nested[k] = createPulse(`${key}.${k}`, v, depth + 1);
|
|
108
224
|
}
|
|
109
225
|
return nested;
|
|
110
226
|
}
|
|
@@ -119,6 +235,15 @@ export function createStore(initialState = {}, options = {}) {
|
|
|
119
235
|
store[key] = createPulse(key, value);
|
|
120
236
|
}
|
|
121
237
|
|
|
238
|
+
// Sync nested pulses for persisted state to ensure consistency
|
|
239
|
+
if (persist && typeof localStorage !== 'undefined') {
|
|
240
|
+
for (const [key, value] of Object.entries(state)) {
|
|
241
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
242
|
+
updateNestedPulses(key, value);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
122
247
|
// Persist state changes
|
|
123
248
|
if (persist) {
|
|
124
249
|
effect(() => {
|
package/runtime/utils.js
CHANGED
|
@@ -22,10 +22,10 @@ const HTML_ESCAPES = {
|
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
* Regex for HTML special characters
|
|
25
|
+
* Regex for HTML special characters (auto-generated from HTML_ESCAPES keys)
|
|
26
26
|
* @private
|
|
27
27
|
*/
|
|
28
|
-
const HTML_ESCAPE_REGEX =
|
|
28
|
+
const HTML_ESCAPE_REGEX = new RegExp(`[${Object.keys(HTML_ESCAPES).join('')}]`, 'g');
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Escape HTML special characters to prevent XSS attacks.
|