pulse-js-framework 1.10.4 → 1.11.1

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 (65) 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 +124 -82
  21. package/runtime/async.js +4 -0
  22. package/runtime/context.js +16 -3
  23. package/runtime/dom-adapter.js +5 -3
  24. package/runtime/dom-virtual-list.js +2 -1
  25. package/runtime/form.js +8 -3
  26. package/runtime/graphql/cache.js +1 -1
  27. package/runtime/graphql/client.js +22 -0
  28. package/runtime/graphql/hooks.js +12 -6
  29. package/runtime/graphql/subscriptions.js +2 -0
  30. package/runtime/hmr.js +6 -3
  31. package/runtime/http.js +1 -0
  32. package/runtime/i18n.js +2 -0
  33. package/runtime/lru-cache.js +3 -1
  34. package/runtime/native.js +46 -20
  35. package/runtime/pulse.js +3 -0
  36. package/runtime/router/core.js +5 -1
  37. package/runtime/router/index.js +17 -1
  38. package/runtime/router/psc-integration.js +301 -0
  39. package/runtime/security.js +58 -29
  40. package/runtime/server-components/actions-server.js +798 -0
  41. package/runtime/server-components/actions.js +389 -0
  42. package/runtime/server-components/client.js +447 -0
  43. package/runtime/server-components/error-sanitizer.js +438 -0
  44. package/runtime/server-components/index.js +275 -0
  45. package/runtime/server-components/security-csrf.js +593 -0
  46. package/runtime/server-components/security-errors.js +227 -0
  47. package/runtime/server-components/security-ratelimit.js +733 -0
  48. package/runtime/server-components/security-validation.js +467 -0
  49. package/runtime/server-components/security.js +598 -0
  50. package/runtime/server-components/serializer.js +617 -0
  51. package/runtime/server-components/server.js +382 -0
  52. package/runtime/server-components/types.js +383 -0
  53. package/runtime/server-components/utils/mutex.js +60 -0
  54. package/runtime/server-components/utils/path-sanitizer.js +109 -0
  55. package/runtime/ssr.js +2 -1
  56. package/runtime/store.js +19 -10
  57. package/runtime/utils.js +12 -128
  58. package/types/animation.d.ts +300 -0
  59. package/types/i18n.d.ts +283 -0
  60. package/types/persistence.d.ts +267 -0
  61. package/types/sse.d.ts +248 -0
  62. package/types/sw.d.ts +150 -0
  63. package/runtime/a11y.js.original +0 -1844
  64. package/runtime/graphql.js.original +0 -1326
  65. package/runtime/router.js.original +0 -1605
@@ -0,0 +1,598 @@
1
+ /**
2
+ * Pulse Server Components - Security Validation
3
+ *
4
+ * Comprehensive security validation for props passed from Server to Client Components.
5
+ * Prevents secret leakage, XSS attacks, and DoS via oversized props.
6
+ *
7
+ * Security Layers:
8
+ * 1. Secret Detection - Detects API keys, tokens, passwords in keys/values
9
+ * 2. XSS Sanitization - Blocks script tags, event handlers, javascript: URLs
10
+ * 3. Size Validation - Enforces limits to prevent DoS attacks
11
+ *
12
+ * @module pulse-js-framework/runtime/server-components/security
13
+ */
14
+
15
+ import { RuntimeError } from '../errors.js';
16
+ import { loggers } from '../logger.js';
17
+ import { detectEnvironmentVariables } from './security-validation.js';
18
+
19
+ const log = loggers.dom;
20
+
21
+ // ============================================================================
22
+ // Constants - Secret Detection Patterns
23
+ // ============================================================================
24
+
25
+ /**
26
+ * Regex patterns for detecting secrets in prop keys and values.
27
+ * Covers common secret naming patterns and high-entropy tokens.
28
+ */
29
+ const SECRET_PATTERNS = [
30
+ // Generic key name patterns
31
+ /^(api[_-]?key|apikey|api[_-]?secret|apisecret)$/i,
32
+ /^(secret|secret[_-]?key|secretkey)$/i,
33
+ /^(token|auth[_-]?token|authtoken|access[_-]?token|accesstoken|refresh[_-]?token|refreshtoken)$/i,
34
+ /^(password|passwd|pwd)$/i,
35
+ /^(private[_-]?key|privatekey|priv[_-]?key|privkey)$/i,
36
+ /^(client[_-]?secret|clientsecret)$/i,
37
+ /^(session[_-]?secret|sessionsecret)$/i,
38
+ /^(encryption[_-]?key|encryptionkey)$/i,
39
+ /^(db[_-]?password|database[_-]?password|dbpassword|databasepassword)$/i,
40
+ /^(jwt[_-]?secret|jwtsecret)$/i,
41
+ /^(bearer[_-]?token|bearertoken)$/i,
42
+
43
+ // Service-specific value patterns
44
+ /^sk_live_[A-Za-z0-9]{24,}$/, // Stripe secret key (live)
45
+ /^sk_test_[A-Za-z0-9]{24,}$/, // Stripe secret key (test)
46
+ /^rk_live_[A-Za-z0-9]{24,}$/, // Stripe restricted key (live)
47
+ /^rk_test_[A-Za-z0-9]{24,}$/, // Stripe restricted key (test)
48
+ /^ghp_[A-Za-z0-9]{36}$/, // GitHub Personal Access Token
49
+ /^gho_[A-Za-z0-9]{36}$/, // GitHub OAuth Token
50
+ /^ghs_[A-Za-z0-9]{36}$/, // GitHub Server Token
51
+ /^github_pat_[A-Za-z0-9_]{82}$/, // GitHub Fine-grained PAT
52
+
53
+ // Generic high-entropy patterns (likely secrets)
54
+ /^[A-Za-z0-9+/]{40,}={0,2}$/, // Base64 40+ chars (likely token)
55
+ /^[A-Z0-9]{32,}$/, // All-caps alphanumeric 32+ chars
56
+ /^[a-f0-9]{64}$/, // 64-char hex (SHA-256, could be secret)
57
+ /^[a-f0-9]{128}$/, // 128-char hex (SHA-512)
58
+
59
+ // PEM-encoded private keys
60
+ /-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----/,
61
+ /-----BEGIN ENCRYPTED PRIVATE KEY-----/
62
+ ];
63
+
64
+ /**
65
+ * XSS detection patterns for string prop values.
66
+ */
67
+ const XSS_PATTERNS = {
68
+ scriptTag: /<script[\s>]/i,
69
+ eventHandler: /<[^>]+on\w+\s*=/i,
70
+ javascriptProtocol: /javascript:/i,
71
+ vbscriptProtocol: /vbscript:/i,
72
+ dataHtmlProtocol: /data:text\/html/i
73
+ };
74
+
75
+ // ============================================================================
76
+ // Constants - Size Limits
77
+ // ============================================================================
78
+
79
+ /**
80
+ * Size limits to prevent DoS attacks via oversized props.
81
+ * These limits are generous for normal use but prevent abuse.
82
+ */
83
+ export const PROP_SIZE_LIMITS = {
84
+ MAX_DEPTH: 20, // Max object nesting depth
85
+ MAX_STRING_LENGTH: 100_000, // 100KB per string value
86
+ MAX_ARRAY_LENGTH: 10_000, // 10K items per array
87
+ MAX_OBJECT_KEYS: 1_000, // 1K keys per object
88
+ MAX_TOTAL_SIZE: 1_000_000 // 1MB total JSON serialized size
89
+ };
90
+
91
+ // ============================================================================
92
+ // Secret Detection
93
+ // ============================================================================
94
+
95
+ /**
96
+ * Detected secret information.
97
+ *
98
+ * @typedef {Object} DetectedSecret
99
+ * @property {string} path - Path to the secret (e.g., 'props.user.apiKey')
100
+ * @property {string} value - Truncated value preview (first 20 chars)
101
+ * @property {string} pattern - Regex pattern that matched
102
+ * @property {'key'|'value'} type - Whether secret was in key name or value
103
+ */
104
+
105
+ /**
106
+ * Scan object for potential secrets (API keys, tokens, passwords).
107
+ *
108
+ * Detects secrets in both property keys and values using pattern matching.
109
+ * Returns a list of detected secrets for logging/warning purposes.
110
+ *
111
+ * @param {any} obj - Object to scan for secrets
112
+ * @param {string} [path='props'] - Current path for error messages
113
+ * @returns {DetectedSecret[]} Array of detected secrets
114
+ *
115
+ * @example
116
+ * const secrets = detectSecrets({ apiKey: 'sk_live_abc123...' });
117
+ * // [{ path: 'props.apiKey', value: 'sk_live_abc123...', pattern: '/^sk_live_/', type: 'value' }]
118
+ */
119
+ export function detectSecrets(obj, path = 'props') {
120
+ const detected = [];
121
+ const seen = new WeakSet();
122
+
123
+ function scan(value, currentPath) {
124
+ if (value === null || value === undefined) {
125
+ return;
126
+ }
127
+
128
+ const type = typeof value;
129
+
130
+ // Check string values against value patterns
131
+ if (type === 'string') {
132
+ for (const pattern of SECRET_PATTERNS) {
133
+ if (pattern.test(value)) {
134
+ detected.push({
135
+ path: currentPath,
136
+ value: value.length > 20 ? value.substring(0, 20) + '...' : value,
137
+ pattern: pattern.toString(),
138
+ type: 'value'
139
+ });
140
+ break; // One match per value is enough
141
+ }
142
+ }
143
+ }
144
+
145
+ // Check objects and arrays recursively
146
+ if (type === 'object') {
147
+ // Circular reference check
148
+ if (seen.has(value)) {
149
+ return;
150
+ }
151
+ seen.add(value);
152
+
153
+ if (Array.isArray(value)) {
154
+ value.forEach((item, i) => scan(item, `${currentPath}[${i}]`));
155
+ } else {
156
+ // Check each key and value
157
+ for (const [key, val] of Object.entries(value)) {
158
+ // Check KEY names against patterns
159
+ for (const pattern of SECRET_PATTERNS) {
160
+ if (pattern.test(key)) {
161
+ detected.push({
162
+ path: `${currentPath}.${key}`,
163
+ value: typeof val === 'string'
164
+ ? (val.length > 20 ? val.substring(0, 20) + '...' : val)
165
+ : String(val),
166
+ pattern: pattern.toString(),
167
+ type: 'key'
168
+ });
169
+ break;
170
+ }
171
+ }
172
+
173
+ // Scan value recursively
174
+ scan(val, `${currentPath}.${key}`);
175
+ }
176
+ }
177
+ }
178
+ }
179
+
180
+ scan(obj, path);
181
+ return detected;
182
+ }
183
+
184
+ // ============================================================================
185
+ // XSS Sanitization
186
+ // ============================================================================
187
+
188
+ /**
189
+ * Sanitize props for XSS attacks before serialization.
190
+ *
191
+ * Detects and neutralizes:
192
+ * - Script tags (<script>)
193
+ * - Event handlers (onclick=, onerror=, etc.)
194
+ * - javascript: and vbscript: protocols
195
+ * - data:text/html URLs
196
+ *
197
+ * Logs warnings for each detected pattern. Mutates the props object in place
198
+ * for performance (avoids deep cloning).
199
+ *
200
+ * @param {any} props - Props object to sanitize
201
+ * @param {string} componentId - Component ID for logging
202
+ * @returns {any} Sanitized props (mutated in place)
203
+ *
204
+ * @example
205
+ * const props = { html: '<script>alert("xss")</script>' };
206
+ * sanitizePropsForXSS(props, 'MyComponent');
207
+ * // props.html is now: '&lt;script>alert("xss")&lt;/script>'
208
+ */
209
+ export function sanitizePropsForXSS(props, componentId) {
210
+ const seen = new WeakSet();
211
+
212
+ function sanitize(value, path) {
213
+ if (value === null || value === undefined) {
214
+ return value;
215
+ }
216
+
217
+ const type = typeof value;
218
+
219
+ // Sanitize strings
220
+ if (type === 'string') {
221
+ let sanitized = value;
222
+ let modified = false;
223
+
224
+ // Check for script tags
225
+ if (XSS_PATTERNS.scriptTag.test(sanitized)) {
226
+ log.warn(`PSC: Script tag detected in prop '${path}' for '${componentId}'`);
227
+ sanitized = sanitized.replace(/<script[\s>]/gi, '&lt;script');
228
+ modified = true;
229
+ }
230
+
231
+ // Check for event handlers in HTML-like content
232
+ if (XSS_PATTERNS.eventHandler.test(sanitized)) {
233
+ log.warn(`PSC: Event handler detected in prop '${path}' for '${componentId}'`);
234
+ sanitized = sanitized.replace(/on\w+\s*=/gi, 'data-blocked-event=');
235
+ modified = true;
236
+ }
237
+
238
+ // Check for javascript: protocol
239
+ if (XSS_PATTERNS.javascriptProtocol.test(sanitized)) {
240
+ log.warn(`PSC: javascript: protocol detected in prop '${path}' for '${componentId}'`);
241
+ sanitized = sanitized.replace(/javascript:/gi, 'blocked:');
242
+ modified = true;
243
+ }
244
+
245
+ // Check for vbscript: protocol
246
+ if (XSS_PATTERNS.vbscriptProtocol.test(sanitized)) {
247
+ log.warn(`PSC: vbscript: protocol detected in prop '${path}' for '${componentId}'`);
248
+ sanitized = sanitized.replace(/vbscript:/gi, 'blocked:');
249
+ modified = true;
250
+ }
251
+
252
+ // Check for data:text/html (can execute scripts)
253
+ if (XSS_PATTERNS.dataHtmlProtocol.test(sanitized)) {
254
+ log.warn(`PSC: data:text/html protocol detected in prop '${path}' for '${componentId}'`);
255
+ sanitized = sanitized.replace(/data:text\/html/gi, 'data:text/plain');
256
+ modified = true;
257
+ }
258
+
259
+ return sanitized;
260
+ }
261
+
262
+ // Recursively sanitize objects and arrays
263
+ if (type === 'object') {
264
+ if (seen.has(value)) {
265
+ return value; // Already processed (circular ref)
266
+ }
267
+ seen.add(value);
268
+
269
+ if (Array.isArray(value)) {
270
+ for (let i = 0; i < value.length; i++) {
271
+ value[i] = sanitize(value[i], `${path}[${i}]`);
272
+ }
273
+ } else {
274
+ for (const [key, val] of Object.entries(value)) {
275
+ value[key] = sanitize(val, `${path}.${key}`);
276
+ }
277
+ }
278
+
279
+ return value;
280
+ }
281
+
282
+ return value;
283
+ }
284
+
285
+ return sanitize(props, 'props');
286
+ }
287
+
288
+ // ============================================================================
289
+ // Size Validation
290
+ // ============================================================================
291
+
292
+ /**
293
+ * Validate prop size limits to prevent DoS attacks.
294
+ *
295
+ * Enforces limits on:
296
+ * - Nesting depth (max 20 levels)
297
+ * - String length (max 100KB per string)
298
+ * - Array length (max 10K items)
299
+ * - Object keys (max 1K keys per object)
300
+ * - Total JSON size (max 1MB)
301
+ *
302
+ * Throws RuntimeError if any limit is exceeded.
303
+ *
304
+ * @param {any} props - Props to validate
305
+ * @param {string} componentId - Component ID for error messages
306
+ * @throws {RuntimeError} If size limits exceeded
307
+ *
308
+ * @example
309
+ * validatePropSizeLimits({ data: 'x'.repeat(200000) }, 'MyComponent');
310
+ * // Throws: RuntimeError('String prop too large...')
311
+ */
312
+ export function validatePropSizeLimits(props, componentId) {
313
+ let totalSize = 0;
314
+ const seen = new WeakSet();
315
+
316
+ function check(value, path, depth) {
317
+ // Check nesting depth
318
+ if (depth > PROP_SIZE_LIMITS.MAX_DEPTH) {
319
+ throw new RuntimeError(
320
+ `PSC: Prop nesting depth exceeded ${PROP_SIZE_LIMITS.MAX_DEPTH} at '${path}'`,
321
+ {
322
+ code: 'PSC_PROP_DEPTH_EXCEEDED',
323
+ context: `Component: ${componentId}`,
324
+ suggestion: 'Flatten your data structure or split into smaller props'
325
+ }
326
+ );
327
+ }
328
+
329
+ if (value === null || value === undefined) {
330
+ return;
331
+ }
332
+
333
+ const type = typeof value;
334
+
335
+ // Check string length
336
+ if (type === 'string') {
337
+ totalSize += value.length;
338
+ if (value.length > PROP_SIZE_LIMITS.MAX_STRING_LENGTH) {
339
+ throw new RuntimeError(
340
+ `PSC: String prop too large at '${path}' (${value.length} > ${PROP_SIZE_LIMITS.MAX_STRING_LENGTH})`,
341
+ {
342
+ code: 'PSC_STRING_TOO_LARGE',
343
+ context: `Component: ${componentId}`,
344
+ suggestion: 'Split large strings into chunks or store externally'
345
+ }
346
+ );
347
+ }
348
+ }
349
+
350
+ // Check numbers (count toward size)
351
+ if (type === 'number') {
352
+ totalSize += 8; // Approximate size
353
+ }
354
+
355
+ // Check boolean (count toward size)
356
+ if (type === 'boolean') {
357
+ totalSize += 4;
358
+ }
359
+
360
+ // Check objects and arrays
361
+ if (type === 'object' && value !== null) {
362
+ if (seen.has(value)) {
363
+ return; // Already counted (circular ref)
364
+ }
365
+ seen.add(value);
366
+
367
+ if (Array.isArray(value)) {
368
+ // Check array length
369
+ if (value.length > PROP_SIZE_LIMITS.MAX_ARRAY_LENGTH) {
370
+ throw new RuntimeError(
371
+ `PSC: Array too large at '${path}' (${value.length} > ${PROP_SIZE_LIMITS.MAX_ARRAY_LENGTH})`,
372
+ {
373
+ code: 'PSC_ARRAY_TOO_LARGE',
374
+ context: `Component: ${componentId}`,
375
+ suggestion: 'Use pagination or split into smaller arrays'
376
+ }
377
+ );
378
+ }
379
+
380
+ // Check each item recursively
381
+ value.forEach((item, i) => check(item, `${path}[${i}]`, depth + 1));
382
+ } else {
383
+ // Check object key count
384
+ const keys = Object.keys(value);
385
+ if (keys.length > PROP_SIZE_LIMITS.MAX_OBJECT_KEYS) {
386
+ throw new RuntimeError(
387
+ `PSC: Object has too many keys at '${path}' (${keys.length} > ${PROP_SIZE_LIMITS.MAX_OBJECT_KEYS})`,
388
+ {
389
+ code: 'PSC_OBJECT_TOO_LARGE',
390
+ context: `Component: ${componentId}`,
391
+ suggestion: 'Split into smaller objects or use a Map/Set'
392
+ }
393
+ );
394
+ }
395
+
396
+ // Check each property recursively
397
+ for (const [key, val] of Object.entries(value)) {
398
+ totalSize += key.length; // Count key names too
399
+ check(val, `${path}.${key}`, depth + 1);
400
+ }
401
+ }
402
+ }
403
+ }
404
+
405
+ // Check total depth and size
406
+ check(props, 'props', 0);
407
+
408
+ // Check total JSON size (approximate via stringification)
409
+ // Note: Circular references are already caught by check() via WeakSet,
410
+ // but JSON.stringify() can still throw if circular refs exist.
411
+ // If we reach here, props should be serializable.
412
+ try {
413
+ const jsonSize = JSON.stringify(props).length;
414
+ if (jsonSize > PROP_SIZE_LIMITS.MAX_TOTAL_SIZE) {
415
+ throw new RuntimeError(
416
+ `PSC: Total prop size too large (${jsonSize} bytes > ${PROP_SIZE_LIMITS.MAX_TOTAL_SIZE} bytes)`,
417
+ {
418
+ code: 'PSC_PROPS_TOO_LARGE',
419
+ context: `Component: ${componentId}`,
420
+ suggestion: 'Reduce prop size or fetch data on the client'
421
+ }
422
+ );
423
+ }
424
+ } catch (err) {
425
+ // If JSON.stringify throws (e.g., circular reference), throw specific error
426
+ if (err.message && err.message.includes('circular')) {
427
+ throw new RuntimeError(
428
+ `PSC: Props contain circular reference at '${componentId}'`,
429
+ {
430
+ code: 'PSC_CIRCULAR_PROP',
431
+ context: 'Props must not contain circular references',
432
+ suggestion: 'Check for circular object references in your props'
433
+ }
434
+ );
435
+ }
436
+ // Re-throw if it's our own RuntimeError (size limit exceeded)
437
+ if (err.code && err.code.startsWith('PSC_')) {
438
+ throw err;
439
+ }
440
+ // Unknown error during stringification
441
+ throw new RuntimeError(
442
+ `PSC: Failed to serialize props for '${componentId}'`,
443
+ {
444
+ code: 'PSC_SERIALIZATION_FAILED',
445
+ context: err.message
446
+ }
447
+ );
448
+ }
449
+ }
450
+
451
+ // ============================================================================
452
+ // Main Security Validator
453
+ // ============================================================================
454
+
455
+ /**
456
+ * Security validation result.
457
+ *
458
+ * @typedef {Object} SecurityValidationResult
459
+ * @property {boolean} valid - True if no blocking errors found
460
+ * @property {DetectedSecret[]} warnings - Detected secrets (warnings, not blocking)
461
+ * @property {Error[]} errors - Validation errors (blocking)
462
+ * @property {any} sanitized - Sanitized props (XSS patterns removed)
463
+ */
464
+
465
+ /**
466
+ * Validate prop security comprehensively.
467
+ *
468
+ * Orchestrates all security validations:
469
+ * 1. Secret detection (warns but doesn't block)
470
+ * 2. XSS sanitization (sanitizes and warns)
471
+ * 3. Size validation (throws on violation)
472
+ * 4. Environment variable detection (warns but doesn't block) - NEW
473
+ *
474
+ * @param {any} props - Props to validate
475
+ * @param {string} componentId - Component ID for logging
476
+ * @param {Object} [options] - Validation options
477
+ * @param {boolean} [options.detectSecrets=true] - Enable secret detection
478
+ * @param {boolean} [options.sanitizeXSS=true] - Enable XSS sanitization
479
+ * @param {boolean} [options.validateSizes=true] - Enable size validation
480
+ * @param {boolean} [options.detectEnvVars=true] - Enable environment variable detection
481
+ * @param {boolean} [options.throwOnSecrets=false] - Throw error if secrets detected (default: warn only)
482
+ * @returns {SecurityValidationResult} Validation result
483
+ *
484
+ * @example
485
+ * const result = validatePropSecurity(props, 'MyComponent');
486
+ * if (!result.valid) {
487
+ * console.error('Validation failed:', result.errors);
488
+ * }
489
+ * if (result.warnings.length > 0) {
490
+ * console.warn('Detected secrets:', result.warnings);
491
+ * }
492
+ * return result.sanitized;
493
+ */
494
+ export function validatePropSecurity(props, componentId, options = {}) {
495
+ const {
496
+ detectSecrets: enableSecretDetection = true,
497
+ sanitizeXSS: enableXSSSanitization = true,
498
+ validateSizes: enableSizeValidation = true,
499
+ detectEnvVars = true,
500
+ throwOnSecrets = false
501
+ } = options;
502
+
503
+ const warnings = [];
504
+ const errors = [];
505
+ let sanitized = props;
506
+
507
+ // 1. Detect secrets (warns only, doesn't modify props)
508
+ if (enableSecretDetection) {
509
+ try {
510
+ const secrets = detectSecrets(props);
511
+ if (secrets.length > 0) {
512
+ warnings.push(...secrets);
513
+ log.warn(
514
+ `PSC: Detected ${secrets.length} potential secret(s) in props for '${componentId}'`,
515
+ secrets
516
+ );
517
+
518
+ if (throwOnSecrets) {
519
+ throw new RuntimeError(
520
+ `PSC: Secrets detected in props for '${componentId}'`,
521
+ {
522
+ code: 'PSC_SECRETS_IN_PROPS',
523
+ context: `Detected ${secrets.length} secret pattern(s)`,
524
+ suggestion: 'Remove sensitive data from props or use Server Actions',
525
+ details: secrets
526
+ }
527
+ );
528
+ }
529
+ }
530
+ } catch (err) {
531
+ errors.push(err);
532
+ }
533
+ }
534
+
535
+ // 2. Sanitize XSS (modifies props in place)
536
+ if (enableXSSSanitization) {
537
+ try {
538
+ sanitized = sanitizePropsForXSS(sanitized, componentId);
539
+ } catch (err) {
540
+ errors.push(err);
541
+ }
542
+ }
543
+
544
+ // 3. Validate sizes (throws on violation)
545
+ if (enableSizeValidation) {
546
+ try {
547
+ validatePropSizeLimits(sanitized, componentId);
548
+ } catch (err) {
549
+ errors.push(err);
550
+ }
551
+ }
552
+
553
+ // 4. Detect environment variables (warns only, doesn't modify props) - NEW
554
+ if (detectEnvVars) {
555
+ try {
556
+ const envVarResult = detectEnvironmentVariables(sanitized, 'props');
557
+
558
+ if (envVarResult.detected) {
559
+ // Add env var detections to warnings with a specific type
560
+ const envVarWarnings = envVarResult.warnings.map(w => ({
561
+ ...w,
562
+ type: 'env-var', // Mark as env-var warning (different from secret warning)
563
+ severity: 'warning'
564
+ }));
565
+ warnings.push(...envVarWarnings);
566
+
567
+ log.warn(
568
+ `PSC: Environment variable(s) detected in props for '${componentId}':`,
569
+ envVarResult.warnings.map(w => `${w.path}: ${w.pattern} (${w.platform})`)
570
+ );
571
+ }
572
+ } catch (err) {
573
+ // Don't block on env var detection errors (validation is optional)
574
+ log.warn(`PSC: Environment variable detection failed for '${componentId}':`, err.message);
575
+ }
576
+ }
577
+
578
+ return {
579
+ valid: errors.length === 0,
580
+ warnings,
581
+ errors,
582
+ sanitized
583
+ };
584
+ }
585
+
586
+ // ============================================================================
587
+ // Exports
588
+ // ============================================================================
589
+
590
+ export default {
591
+ detectSecrets,
592
+ sanitizePropsForXSS,
593
+ validatePropSizeLimits,
594
+ validatePropSecurity,
595
+ PROP_SIZE_LIMITS,
596
+ SECRET_PATTERNS,
597
+ XSS_PATTERNS
598
+ };