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.
- package/README.md +11 -0
- package/cli/build.js +13 -3
- package/compiler/directives.js +356 -0
- package/compiler/lexer.js +18 -3
- package/compiler/parser/core.js +6 -0
- package/compiler/parser/view.js +2 -6
- package/compiler/preprocessor.js +43 -23
- package/compiler/sourcemap.js +3 -1
- package/compiler/transformer/actions.js +329 -0
- package/compiler/transformer/export.js +7 -0
- package/compiler/transformer/expressions.js +85 -33
- package/compiler/transformer/imports.js +3 -0
- package/compiler/transformer/index.js +2 -0
- package/compiler/transformer/store.js +1 -1
- package/compiler/transformer/style.js +45 -16
- package/compiler/transformer/view.js +23 -2
- package/loader/rollup-plugin-server-components.js +391 -0
- package/loader/vite-plugin-server-components.js +420 -0
- package/loader/webpack-loader-server-components.js +356 -0
- package/package.json +127 -74
- package/runtime/a11y/widgets.js +14 -1
- package/runtime/async.js +4 -0
- package/runtime/context.js +16 -3
- package/runtime/dom-adapter.js +5 -3
- package/runtime/dom-virtual-list.js +2 -1
- package/runtime/form.js +8 -3
- package/runtime/graphql/cache.js +1 -1
- package/runtime/graphql/client.js +22 -0
- package/runtime/graphql/hooks.js +12 -6
- package/runtime/graphql/subscriptions.js +4 -0
- package/runtime/hmr.js +6 -3
- package/runtime/http.js +1 -0
- package/runtime/i18n.js +2 -0
- package/runtime/lru-cache.js +3 -1
- package/runtime/native.js +46 -20
- package/runtime/pulse.js +3 -0
- package/runtime/router/core.js +5 -1
- package/runtime/router/index.js +17 -1
- package/runtime/router/psc-integration.js +301 -0
- package/runtime/security.js +58 -29
- package/runtime/server-components/actions-server.js +798 -0
- package/runtime/server-components/actions.js +389 -0
- package/runtime/server-components/client.js +447 -0
- package/runtime/server-components/error-sanitizer.js +438 -0
- package/runtime/server-components/index.js +275 -0
- package/runtime/server-components/security-csrf.js +593 -0
- package/runtime/server-components/security-errors.js +227 -0
- package/runtime/server-components/security-ratelimit.js +733 -0
- package/runtime/server-components/security-validation.js +467 -0
- package/runtime/server-components/security.js +598 -0
- package/runtime/server-components/serializer.js +617 -0
- package/runtime/server-components/server.js +382 -0
- package/runtime/server-components/types.js +383 -0
- package/runtime/server-components/utils/mutex.js +60 -0
- package/runtime/server-components/utils/path-sanitizer.js +109 -0
- package/runtime/ssr.js +2 -1
- package/runtime/store.js +19 -10
- package/runtime/utils.js +12 -128
- package/types/animation.d.ts +300 -0
- package/types/i18n.d.ts +283 -0
- package/types/persistence.d.ts +267 -0
- package/types/sse.d.ts +248 -0
- package/types/sw.d.ts +150 -0
- package/runtime/a11y.js.original +0 -1844
- package/runtime/graphql.js.original +0 -1326
- 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
|
+
};
|