pulse-js-framework 1.10.3 → 1.11.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 (66) hide show
  1. package/README.md +11 -0
  2. package/cli/build.js +13 -3
  3. package/compiler/directives.js +356 -0
  4. package/compiler/lexer.js +18 -3
  5. package/compiler/parser/core.js +6 -0
  6. package/compiler/parser/view.js +2 -6
  7. package/compiler/preprocessor.js +43 -23
  8. package/compiler/sourcemap.js +3 -1
  9. package/compiler/transformer/actions.js +329 -0
  10. package/compiler/transformer/export.js +7 -0
  11. package/compiler/transformer/expressions.js +85 -33
  12. package/compiler/transformer/imports.js +3 -0
  13. package/compiler/transformer/index.js +2 -0
  14. package/compiler/transformer/store.js +1 -1
  15. package/compiler/transformer/style.js +45 -16
  16. package/compiler/transformer/view.js +23 -2
  17. package/loader/rollup-plugin-server-components.js +391 -0
  18. package/loader/vite-plugin-server-components.js +420 -0
  19. package/loader/webpack-loader-server-components.js +356 -0
  20. package/package.json +127 -74
  21. package/runtime/a11y/widgets.js +14 -1
  22. package/runtime/async.js +4 -0
  23. package/runtime/context.js +16 -3
  24. package/runtime/dom-adapter.js +5 -3
  25. package/runtime/dom-virtual-list.js +2 -1
  26. package/runtime/form.js +8 -3
  27. package/runtime/graphql/cache.js +1 -1
  28. package/runtime/graphql/client.js +22 -0
  29. package/runtime/graphql/hooks.js +12 -6
  30. package/runtime/graphql/subscriptions.js +4 -0
  31. package/runtime/hmr.js +6 -3
  32. package/runtime/http.js +1 -0
  33. package/runtime/i18n.js +2 -0
  34. package/runtime/lru-cache.js +3 -1
  35. package/runtime/native.js +46 -20
  36. package/runtime/pulse.js +3 -0
  37. package/runtime/router/core.js +5 -1
  38. package/runtime/router/index.js +17 -1
  39. package/runtime/router/psc-integration.js +301 -0
  40. package/runtime/security.js +58 -29
  41. package/runtime/server-components/actions-server.js +798 -0
  42. package/runtime/server-components/actions.js +389 -0
  43. package/runtime/server-components/client.js +447 -0
  44. package/runtime/server-components/error-sanitizer.js +438 -0
  45. package/runtime/server-components/index.js +275 -0
  46. package/runtime/server-components/security-csrf.js +593 -0
  47. package/runtime/server-components/security-errors.js +227 -0
  48. package/runtime/server-components/security-ratelimit.js +733 -0
  49. package/runtime/server-components/security-validation.js +467 -0
  50. package/runtime/server-components/security.js +598 -0
  51. package/runtime/server-components/serializer.js +617 -0
  52. package/runtime/server-components/server.js +382 -0
  53. package/runtime/server-components/types.js +383 -0
  54. package/runtime/server-components/utils/mutex.js +60 -0
  55. package/runtime/server-components/utils/path-sanitizer.js +109 -0
  56. package/runtime/ssr.js +2 -1
  57. package/runtime/store.js +19 -10
  58. package/runtime/utils.js +12 -128
  59. package/types/animation.d.ts +300 -0
  60. package/types/i18n.d.ts +283 -0
  61. package/types/persistence.d.ts +267 -0
  62. package/types/sse.d.ts +248 -0
  63. package/types/sw.d.ts +150 -0
  64. package/runtime/a11y.js.original +0 -1844
  65. package/runtime/graphql.js.original +0 -1326
  66. package/runtime/router.js.original +0 -1605
@@ -0,0 +1,467 @@
1
+ /**
2
+ * Pulse Server Components - Enhanced Prop Serialization Validation
3
+ *
4
+ * Validates that props passed to Client Components are JSON-serializable and
5
+ * detects environment variable references that could leak secrets.
6
+ *
7
+ * Security Checks:
8
+ * 1. Non-Serializable Types - Functions, Symbols, class instances, Promises, etc.
9
+ * 2. Environment Variables - Detects process.env.*, import.meta.env.*, Deno.env.*
10
+ *
11
+ * @module pulse-js-framework/runtime/server-components/security-validation
12
+ */
13
+
14
+ import { loggers } from '../logger.js';
15
+ import { RuntimeError } from '../errors.js';
16
+ import { DANGEROUS_KEYS } from '../security.js';
17
+
18
+ const log = loggers.dom;
19
+
20
+ // ============================================================================
21
+ // Constants - Forbidden Types
22
+ // ============================================================================
23
+
24
+ /**
25
+ * Types that cannot be serialized to JSON.
26
+ * These will cause errors if passed as props to Client Components.
27
+ */
28
+ const FORBIDDEN_TYPES = new Set(['function', 'symbol']);
29
+
30
+ /**
31
+ * Class constructors that should not be serialized.
32
+ * These are detected via instanceof checks.
33
+ */
34
+ const FORBIDDEN_CLASSES = [
35
+ WeakMap,
36
+ WeakSet,
37
+ Promise,
38
+ Error,
39
+ RegExp,
40
+ Date // Serializes to string, loses type info - recommend explicit error
41
+ ];
42
+
43
+ // DANGEROUS_KEYS imported from ../security.js (single source of truth — 16 entries)
44
+
45
+ // ============================================================================
46
+ // Constants - Environment Variable Patterns
47
+ // ============================================================================
48
+
49
+ /**
50
+ * String patterns for detecting environment variable access.
51
+ * Using simple string matching to prevent ReDoS vulnerabilities.
52
+ * Covers common patterns across Node.js, Vite, and Deno.
53
+ */
54
+ const ENV_PATTERNS = [
55
+ {
56
+ prefix: 'process.env.',
57
+ platform: 'Node.js',
58
+ example: 'process.env.API_KEY'
59
+ },
60
+ {
61
+ prefix: 'import.meta.env.',
62
+ platform: 'Vite',
63
+ example: 'import.meta.env.VITE_API_KEY'
64
+ },
65
+ {
66
+ prefix: 'Deno.env.get(',
67
+ platform: 'Deno',
68
+ example: 'Deno.env.get("API_KEY")'
69
+ }
70
+ ];
71
+
72
+ /**
73
+ * Maximum safe size for environment variable detection (prevents DoS)
74
+ */
75
+ const MAX_ENV_SCAN_SIZE = 10000;
76
+
77
+ // ============================================================================
78
+ // Non-Serializable Type Detection
79
+ // ============================================================================
80
+
81
+ /**
82
+ * Validation error for non-serializable values.
83
+ *
84
+ * @typedef {Object} SerializationError
85
+ * @property {string} path - Property path (e.g., 'props.user.handler')
86
+ * @property {string} type - Type of non-serializable value ('function', 'symbol', 'class-instance', etc.)
87
+ * @property {string} [className] - Class name for class instances
88
+ * @property {string} message - Human-readable error message
89
+ */
90
+
91
+ /**
92
+ * Detect non-serializable values in props object.
93
+ *
94
+ * Checks for:
95
+ * - Functions (typeof === 'function')
96
+ * - Symbols (typeof === 'symbol')
97
+ * - Class instances (Promise, Error, RegExp, Date, WeakMap, WeakSet, custom classes)
98
+ * - Circular references (tracked via WeakSet)
99
+ *
100
+ * @param {any} value - Value to check for serializability
101
+ * @param {string} [path='props'] - Current property path for error messages
102
+ * @returns {{ valid: boolean, errors: SerializationError[] }} Validation result
103
+ *
104
+ * @example
105
+ * const result = detectNonSerializable({ onClick: () => {} }, 'props');
106
+ * // { valid: false, errors: [{ path: 'props.onClick', type: 'function', message: '...' }] }
107
+ */
108
+ export function detectNonSerializable(value, path = 'props') {
109
+ const errors = [];
110
+ const seen = new WeakSet();
111
+
112
+ function check(val, currentPath) {
113
+ if (val === null) {
114
+ return; // null is serializable
115
+ }
116
+
117
+ if (val === undefined) {
118
+ // undefined is technically omitted by JSON.stringify, but we want explicit error
119
+ // Only error if it's a property value, not the top-level (which might be intentional)
120
+ if (currentPath !== path) {
121
+ errors.push({
122
+ path: currentPath,
123
+ type: 'undefined',
124
+ message: 'Cannot serialize undefined (omitted by JSON.stringify)'
125
+ });
126
+ }
127
+ return;
128
+ }
129
+
130
+ const type = typeof val;
131
+
132
+ // Check primitive forbidden types
133
+ if (FORBIDDEN_TYPES.has(type)) {
134
+ errors.push({
135
+ path: currentPath,
136
+ type,
137
+ message: `Cannot serialize ${type} (not JSON-compatible)`
138
+ });
139
+ return;
140
+ }
141
+
142
+ // Check objects
143
+ if (type === 'object') {
144
+ // Circular reference check
145
+ if (seen.has(val)) {
146
+ errors.push({
147
+ path: currentPath,
148
+ type: 'circular',
149
+ message: 'Circular reference detected'
150
+ });
151
+ return;
152
+ }
153
+ seen.add(val);
154
+
155
+ // Check forbidden class instances
156
+ for (const ForbiddenClass of FORBIDDEN_CLASSES) {
157
+ if (val instanceof ForbiddenClass) {
158
+ errors.push({
159
+ path: currentPath,
160
+ type: 'class-instance',
161
+ className: ForbiddenClass.name,
162
+ message: `Cannot serialize ${ForbiddenClass.name} instance`
163
+ });
164
+ return;
165
+ }
166
+ }
167
+
168
+ // Check arrays
169
+ if (Array.isArray(val)) {
170
+ val.forEach((item, i) => check(item, `${currentPath}[${i}]`));
171
+ return;
172
+ }
173
+
174
+ // CRITICAL: Check for dangerous keys BEFORE processing object
175
+ // Use for..in to catch __proto__, constructor, prototype
176
+ // Object.keys() won't return __proto__ as it's not an own property
177
+ for (const key in val) {
178
+ if (DANGEROUS_KEYS.has(key)) {
179
+ errors.push({
180
+ path: `${currentPath}.${key}`,
181
+ type: 'dangerous-key',
182
+ message: `Dangerous property key "${key}" not allowed (prototype pollution risk)`
183
+ });
184
+ }
185
+ }
186
+
187
+ // Check if it's a plain object (Object.prototype or null prototype)
188
+ const proto = Object.getPrototypeOf(val);
189
+ if (proto === Object.prototype || proto === null) {
190
+ // Plain object - check own properties recursively (skip dangerous keys)
191
+ for (const key in val) {
192
+ if (Object.prototype.hasOwnProperty.call(val, key) && !DANGEROUS_KEYS.has(key)) {
193
+ check(val[key], `${currentPath}.${key}`);
194
+ }
195
+ }
196
+ } else {
197
+ // Custom class instance (not plain object)
198
+ const className = val.constructor?.name || 'Unknown';
199
+ errors.push({
200
+ path: currentPath,
201
+ type: 'class-instance',
202
+ className,
203
+ message: `Cannot serialize custom class instance (${className})`
204
+ });
205
+ }
206
+ }
207
+ }
208
+
209
+ check(value, path);
210
+
211
+ return {
212
+ valid: errors.length === 0,
213
+ errors
214
+ };
215
+ }
216
+
217
+ // ============================================================================
218
+ // Environment Variable Detection
219
+ // ============================================================================
220
+
221
+ /**
222
+ * Detected environment variable reference.
223
+ *
224
+ * @typedef {Object} EnvVarDetection
225
+ * @property {string} path - Property path where env var was found
226
+ * @property {string} variable - Environment variable name (e.g., 'API_KEY')
227
+ * @property {string} pattern - Pattern that matched (e.g., 'process.env.API_KEY')
228
+ * @property {string} platform - Platform (Node.js, Vite, Deno)
229
+ * @property {string} valuePreview - Truncated value preview
230
+ */
231
+
232
+ /**
233
+ * Detect environment variable references in prop values.
234
+ *
235
+ * Scans string values for patterns like:
236
+ * - process.env.API_KEY
237
+ * - import.meta.env.VITE_API_KEY
238
+ * - Deno.env.get('API_KEY')
239
+ *
240
+ * Uses safe string matching (no regex) to prevent ReDoS attacks.
241
+ * Returns warnings (not errors) since strings containing these patterns
242
+ * could be false positives (e.g., documentation, code snippets).
243
+ *
244
+ * @param {any} value - Props object to scan
245
+ * @param {string} [path='props'] - Current property path
246
+ * @returns {{ detected: boolean, warnings: EnvVarDetection[] }} Detection result
247
+ *
248
+ * @example
249
+ * const result = detectEnvironmentVariables({ apiKey: process.env.API_KEY });
250
+ * // { detected: true, warnings: [{ path: 'props.apiKey', variable: 'API_KEY', ... }] }
251
+ */
252
+ export function detectEnvironmentVariables(value, path = 'props') {
253
+ const warnings = [];
254
+ const seen = new WeakSet();
255
+
256
+ function scan(val, currentPath) {
257
+ if (val === null || val === undefined) {
258
+ return;
259
+ }
260
+
261
+ const type = typeof val;
262
+
263
+ // Check string values for env var patterns (safe string matching)
264
+ if (type === 'string') {
265
+ // Skip extremely large strings to prevent DoS
266
+ if (val.length > MAX_ENV_SCAN_SIZE) {
267
+ return;
268
+ }
269
+
270
+ for (const { prefix, platform } of ENV_PATTERNS) {
271
+ let index = val.indexOf(prefix);
272
+
273
+ while (index !== -1) {
274
+ // Extract variable name (alphanumeric, underscore, uppercase preferred)
275
+ let varName = '';
276
+ let i = index + prefix.length;
277
+
278
+ // Skip opening quote/paren for Deno.env.get(
279
+ if (prefix === 'Deno.env.get(' && i < val.length) {
280
+ const char = val[i];
281
+ if (char === '"' || char === "'") {
282
+ i++; // Skip quote
283
+ }
284
+ }
285
+
286
+ // Extract variable name (max 100 chars to prevent runaway)
287
+ // Accept both uppercase and lowercase for variable names
288
+ let maxLen = Math.min(i + 100, val.length);
289
+ while (i < maxLen) {
290
+ const char = val[i];
291
+ // Accept alphanumeric and underscore
292
+ if (/[A-Za-z0-9_]/.test(char)) {
293
+ varName += char;
294
+ i++;
295
+ } else {
296
+ break;
297
+ }
298
+ }
299
+
300
+ if (varName.length > 0) {
301
+ const fullPattern = prefix + varName;
302
+ const snippet = val.substring(index, Math.min(index + 50, val.length));
303
+
304
+ warnings.push({
305
+ path: currentPath,
306
+ variable: varName,
307
+ pattern: fullPattern,
308
+ platform,
309
+ valuePreview: snippet.replace(/[\r\n]/g, '') + (snippet.length < val.length - index ? '...' : '')
310
+ });
311
+ }
312
+
313
+ // Find next occurrence
314
+ index = val.indexOf(prefix, index + 1);
315
+ }
316
+ }
317
+ }
318
+
319
+ // Recursively scan objects and arrays
320
+ if (type === 'object') {
321
+ if (seen.has(val)) {
322
+ return; // Already scanned (circular ref)
323
+ }
324
+ seen.add(val);
325
+
326
+ if (Array.isArray(val)) {
327
+ val.forEach((item, i) => scan(item, `${currentPath}[${i}]`));
328
+ } else {
329
+ for (const [key, v] of Object.entries(val)) {
330
+ scan(v, `${currentPath}.${key}`);
331
+ }
332
+ }
333
+ }
334
+ }
335
+
336
+ scan(value, path);
337
+
338
+ return {
339
+ detected: warnings.length > 0,
340
+ warnings
341
+ };
342
+ }
343
+
344
+ // ============================================================================
345
+ // Main Validation Function
346
+ // ============================================================================
347
+
348
+ /**
349
+ * Prop serialization validation result.
350
+ *
351
+ * @typedef {Object} PropSerializationResult
352
+ * @property {boolean} valid - True if no blocking errors found
353
+ * @property {SerializationError[]} errors - Non-serializable type errors (blocking)
354
+ * @property {EnvVarDetection[]} warnings - Environment variable detections (non-blocking)
355
+ * @property {any} sanitized - Same as input (no sanitization for serialization, only validation)
356
+ */
357
+
358
+ /**
359
+ * Validate prop serialization comprehensively.
360
+ *
361
+ * Orchestrates all serialization validations:
362
+ * 1. Non-serializable type detection (errors, blocking)
363
+ * 2. Environment variable detection (warnings, non-blocking)
364
+ *
365
+ * @param {any} props - Props to validate
366
+ * @param {string} componentName - Component name for error messages
367
+ * @param {Object} [options] - Validation options
368
+ * @param {boolean} [options.throwOnError=false] - Throw error on non-serializable types
369
+ * @param {boolean} [options.detectEnvVars=true] - Detect environment variables
370
+ * @returns {PropSerializationResult} Validation result
371
+ *
372
+ * @example
373
+ * const result = validatePropSerialization(props, 'MyComponent');
374
+ * if (!result.valid) {
375
+ * console.error('Non-serializable props:', result.errors);
376
+ * }
377
+ * if (result.warnings.length > 0) {
378
+ * console.warn('Env vars detected:', result.warnings);
379
+ * }
380
+ */
381
+ export function validatePropSerialization(props, componentName, options = {}) {
382
+ const {
383
+ throwOnError = false,
384
+ detectEnvVars = true
385
+ } = options;
386
+
387
+ const errors = [];
388
+ const warnings = [];
389
+
390
+ // 1. Check for non-serializable types
391
+ const serializableCheck = detectNonSerializable(props, 'props');
392
+ if (!serializableCheck.valid) {
393
+ errors.push(...serializableCheck.errors);
394
+
395
+ // Log errors
396
+ log.error(
397
+ `PSC: Non-serializable props detected in '${componentName}':`,
398
+ serializableCheck.errors.map(err => `${err.path}: ${err.type} (${err.message})`)
399
+ );
400
+
401
+ if (throwOnError) {
402
+ const firstError = serializableCheck.errors[0];
403
+
404
+ // Create user-friendly error message
405
+ let errorMessage = `PSC: Non-serializable prop at '${firstError.path}' for Client Component '${componentName}'`;
406
+
407
+ // Add specific message based on type
408
+ if (firstError.type === 'function') {
409
+ errorMessage += ': Function props not allowed';
410
+ } else {
411
+ errorMessage += `: ${firstError.message}`;
412
+ }
413
+
414
+ throw new RuntimeError(
415
+ errorMessage,
416
+ {
417
+ code: 'PSC_NON_SERIALIZABLE',
418
+ context: `Type: ${firstError.type}`,
419
+ suggestion: firstError.className
420
+ ? `Cannot serialize ${firstError.className} instances. Pass plain objects instead.`
421
+ : `Cannot serialize ${firstError.type}. ${
422
+ firstError.type === 'function'
423
+ ? 'Use Server Actions instead of inline functions.'
424
+ : firstError.type === 'circular'
425
+ ? 'Remove circular references from props.'
426
+ : 'Ensure all props are JSON-serializable (strings, numbers, booleans, plain objects, arrays).'
427
+ }`,
428
+ details: serializableCheck.errors
429
+ }
430
+ );
431
+ }
432
+ }
433
+
434
+ // 2. Check for environment variable references
435
+ if (detectEnvVars) {
436
+ const envVarCheck = detectEnvironmentVariables(props, 'props');
437
+ if (envVarCheck.detected) {
438
+ warnings.push(...envVarCheck.warnings);
439
+
440
+ // Log warnings
441
+ log.warn(
442
+ `PSC: Environment variable(s) detected in props for '${componentName}':`,
443
+ envVarCheck.warnings.map(w => `${w.path}: ${w.pattern} (${w.platform})`)
444
+ );
445
+ }
446
+ }
447
+
448
+ return {
449
+ valid: errors.length === 0,
450
+ errors,
451
+ warnings,
452
+ sanitized: props // No sanitization for serialization validation
453
+ };
454
+ }
455
+
456
+ // ============================================================================
457
+ // Exports
458
+ // ============================================================================
459
+
460
+ export default {
461
+ detectNonSerializable,
462
+ detectEnvironmentVariables,
463
+ validatePropSerialization,
464
+ FORBIDDEN_TYPES,
465
+ FORBIDDEN_CLASSES,
466
+ ENV_PATTERNS
467
+ };