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,593 @@
1
+ /**
2
+ * Pulse Server Components - CSRF Protection
3
+ *
4
+ * Cryptographically secure CSRF token generation and validation using HMAC-SHA256.
5
+ * Provides zero-dependency, edge-runtime compatible protection against cross-site
6
+ * request forgery attacks on Server Actions.
7
+ *
8
+ * Security Properties:
9
+ * - 128-bit random entropy per token
10
+ * - HMAC-SHA256 signatures (cryptographically secure)
11
+ * - Time-limited tokens (default: 1 hour)
12
+ * - Constant-time comparison (prevents timing attacks)
13
+ * - Automatic cleanup of expired tokens
14
+ * - Optional token rotation on use
15
+ *
16
+ * Token Format: <timestamp>.<random-bytes>.<hmac-signature>
17
+ * Example: 1676400000000.a1b2c3d4e5f6.abc123def456
18
+ *
19
+ * @module pulse-js-framework/runtime/server-components/security-csrf
20
+ */
21
+
22
+ // ============================================================================
23
+ // Crypto Utilities (Node.js and Edge Runtime Compatible)
24
+ // ============================================================================
25
+
26
+ /**
27
+ * Get crypto module (Node.js crypto or Web Crypto API)
28
+ * @returns {Object} Crypto utilities
29
+ */
30
+ async function getCrypto() {
31
+ // Check for Web Crypto API (edge runtimes: Cloudflare Workers, Deno Deploy)
32
+ if (typeof globalThis.crypto !== 'undefined' && globalThis.crypto.subtle) {
33
+ return {
34
+ randomBytes: (size) => {
35
+ const arr = new Uint8Array(size);
36
+ globalThis.crypto.getRandomValues(arr);
37
+ // Convert to Buffer-like object
38
+ return {
39
+ toString: (encoding) => {
40
+ if (encoding === 'hex') {
41
+ return Array.from(arr)
42
+ .map(b => b.toString(16).padStart(2, '0'))
43
+ .join('');
44
+ }
45
+ return arr;
46
+ }
47
+ };
48
+ },
49
+ createHmac: async (algorithm, secret) => {
50
+ const encoder = new TextEncoder();
51
+ const keyData = encoder.encode(secret);
52
+
53
+ const key = await globalThis.crypto.subtle.importKey(
54
+ 'raw',
55
+ keyData,
56
+ { name: 'HMAC', hash: 'SHA-256' },
57
+ false,
58
+ ['sign']
59
+ );
60
+
61
+ return {
62
+ update: (data) => {
63
+ const dataBytes = encoder.encode(data);
64
+ return {
65
+ digest: async (encoding) => {
66
+ const signature = await globalThis.crypto.subtle.sign('HMAC', key, dataBytes);
67
+ if (encoding === 'hex') {
68
+ return Array.from(new Uint8Array(signature))
69
+ .map(b => b.toString(16).padStart(2, '0'))
70
+ .join('');
71
+ }
72
+ return signature;
73
+ }
74
+ };
75
+ }
76
+ };
77
+ },
78
+ timingSafeEqual: (a, b) => {
79
+ // Constant-time string comparison for edge runtime
80
+ if (a.length !== b.length) return false;
81
+ let result = 0;
82
+ for (let i = 0; i < a.length; i++) {
83
+ result |= a.charCodeAt(i) ^ b.charCodeAt(i);
84
+ }
85
+ return result === 0;
86
+ }
87
+ };
88
+ } else {
89
+ // Node.js crypto module
90
+ const crypto = await import('crypto');
91
+ return {
92
+ randomBytes: crypto.randomBytes,
93
+ createHmac: (algorithm, secret) => {
94
+ const hmac = crypto.createHmac(algorithm, secret);
95
+ return {
96
+ update: (data) => {
97
+ hmac.update(data);
98
+ return {
99
+ digest: (encoding) => hmac.digest(encoding)
100
+ };
101
+ }
102
+ };
103
+ },
104
+ timingSafeEqual: (a, b) => {
105
+ // String-safe constant-time comparison for Node.js
106
+ if (a.length !== b.length) return false;
107
+
108
+ // Use Node.js crypto.timingSafeEqual if available
109
+ if (crypto.timingSafeEqual) {
110
+ try {
111
+ const bufA = Buffer.from(a, 'utf8');
112
+ const bufB = Buffer.from(b, 'utf8');
113
+ return crypto.timingSafeEqual(bufA, bufB);
114
+ } catch (e) {
115
+ // Fallback if buffer creation fails
116
+ }
117
+ }
118
+
119
+ // Manual constant-time comparison for strings
120
+ let result = 0;
121
+ for (let i = 0; i < a.length; i++) {
122
+ result |= a.charCodeAt(i) ^ b.charCodeAt(i);
123
+ }
124
+ return result === 0;
125
+ }
126
+ };
127
+ }
128
+ }
129
+
130
+ // Cache crypto instance
131
+ let cryptoInstance = null;
132
+
133
+ async function getCryptoInstance() {
134
+ if (!cryptoInstance) {
135
+ cryptoInstance = await getCrypto();
136
+ }
137
+ return cryptoInstance;
138
+ }
139
+
140
+ // ============================================================================
141
+ // CSRF Token Store
142
+ // ============================================================================
143
+
144
+ /**
145
+ * CSRF Token Store - In-memory storage with automatic cleanup
146
+ *
147
+ * Stores generated tokens and their metadata for validation.
148
+ * Automatically cleans up expired tokens to prevent memory leaks.
149
+ *
150
+ * @class CSRFTokenStore
151
+ */
152
+ export class CSRFTokenStore {
153
+ #tokens = new Map(); // token → { timestamp, used }
154
+ #secretKey = null;
155
+ #cleanupInterval = null;
156
+ #options;
157
+
158
+ /**
159
+ * @param {Object} [options] - Store options
160
+ * @param {string} [options.secret] - HMAC secret key (auto-generated if not provided)
161
+ * @param {number} [options.expiresIn=3600000] - Token expiration time in ms (default: 1 hour)
162
+ * @param {number} [options.cleanupInterval=600000] - Cleanup interval in ms (default: 10 minutes)
163
+ * @param {number} [options.maxTokens=10000] - Maximum stored tokens before cleanup
164
+ */
165
+ constructor(options = {}) {
166
+ this.#options = {
167
+ expiresIn: options.expiresIn || 3600000, // 1 hour
168
+ cleanupInterval: options.cleanupInterval || 600000, // 10 minutes
169
+ maxTokens: options.maxTokens || 10000
170
+ };
171
+
172
+ // Generate or use provided HMAC secret (32 bytes = 256 bits)
173
+ this.#secretKey = options.secret || this.#generateSecret();
174
+
175
+ // Start automatic cleanup
176
+ this.#startCleanup();
177
+ }
178
+
179
+ /**
180
+ * Generate cryptographically secure HMAC secret
181
+ * @private
182
+ * @returns {string} Hex-encoded secret key
183
+ */
184
+ #generateSecret() {
185
+ // Use synchronous random if available, otherwise will be async on first use
186
+ try {
187
+ if (typeof globalThis.crypto !== 'undefined' && globalThis.crypto.getRandomValues) {
188
+ const bytes = new Uint8Array(32);
189
+ globalThis.crypto.getRandomValues(bytes);
190
+ return Array.from(bytes)
191
+ .map(b => b.toString(16).padStart(2, '0'))
192
+ .join('');
193
+ }
194
+ } catch (e) {
195
+ // Fall through to async generation on first token generation
196
+ }
197
+
198
+ // Will be generated on first use if not available synchronously
199
+ return null;
200
+ }
201
+
202
+ /**
203
+ * Generate CSRF token
204
+ *
205
+ * Token format: <timestamp>.<random-bytes>.<hmac-signature>
206
+ *
207
+ * @param {Object} [options] - Generation options
208
+ * @param {number} [options.expiresIn] - Override default expiration
209
+ * @returns {Promise<string>} CSRF token
210
+ */
211
+ async generate(options = {}) {
212
+ const crypto = await getCryptoInstance();
213
+
214
+ // Generate secret on first use if needed
215
+ if (!this.#secretKey) {
216
+ const bytes = crypto.randomBytes(32);
217
+ this.#secretKey = bytes.toString('hex');
218
+ }
219
+
220
+ const timestamp = Date.now();
221
+
222
+ // Generate random bytes (16 bytes = 128 bits)
223
+ const randomBytes = crypto.randomBytes(16);
224
+ const random = randomBytes.toString('hex');
225
+
226
+ // Create HMAC signature
227
+ const data = `${timestamp}.${random}`;
228
+ const hmac = await crypto.createHmac('sha256', this.#secretKey);
229
+ const signature = await hmac.update(data).digest('hex');
230
+
231
+ const token = `${data}.${signature}`;
232
+
233
+ // Store token metadata
234
+ this.#tokens.set(token, {
235
+ timestamp,
236
+ used: false,
237
+ expiresIn: options.expiresIn || this.#options.expiresIn
238
+ });
239
+
240
+ // Trigger cleanup if max tokens exceeded
241
+ if (this.#tokens.size > this.#options.maxTokens) {
242
+ this.cleanup();
243
+ }
244
+
245
+ return token;
246
+ }
247
+
248
+ /**
249
+ * Validate CSRF token
250
+ *
251
+ * Performs comprehensive validation in constant-time to prevent timing attacks:
252
+ * 1. Format check (3 parts separated by dots)
253
+ * 2. Expiration check
254
+ * 3. HMAC signature verification (constant-time comparison)
255
+ * 4. Token existence check
256
+ * 5. Single-use check (if rotateOnUse enabled)
257
+ *
258
+ * SECURITY: All validation paths take the same time to prevent timing attacks
259
+ * that could leak token validity information.
260
+ *
261
+ * @param {string} token - Token to validate
262
+ * @param {Object} [options] - Validation options
263
+ * @param {number} [options.expiresIn] - Override default expiration
264
+ * @param {boolean} [options.rotateOnUse=false] - Enforce single-use tokens
265
+ * @returns {Promise<Object>} Validation result { valid: boolean, reason?: string, expired?: boolean }
266
+ */
267
+ async validate(token, options = {}) {
268
+ const crypto = await getCryptoInstance();
269
+
270
+ // Always perform full validation (constant-time - don't short-circuit)
271
+ let valid = true;
272
+ let reason = null;
273
+ let expired = false;
274
+
275
+ // Validation checks (always run all, don't early return)
276
+ const isString = token && typeof token === 'string';
277
+ const parts = isString ? token.split('.') : ['', '', ''];
278
+ const hasThreeParts = parts.length === 3;
279
+
280
+ const timestampStr = parts[0] || '';
281
+ const random = parts[1] || '';
282
+ const providedSignature = parts[2] || '';
283
+
284
+ const timestamp = parseInt(timestampStr, 10);
285
+ const isTimestampValid = !isNaN(timestamp) && timestamp > 0;
286
+
287
+ // Check if token exists in store (always check, even if format invalid)
288
+ const stored = this.#tokens.get(token || '');
289
+ const expiresIn = options.expiresIn || stored?.expiresIn || this.#options.expiresIn;
290
+
291
+ // Check expiration (always compute, even if invalid)
292
+ const now = Date.now();
293
+ const age = isTimestampValid ? (now - timestamp) : Infinity;
294
+ const isExpired = age > expiresIn;
295
+
296
+ // Always compute HMAC (even if earlier checks failed - constant time)
297
+ const data = `${timestampStr}.${random}`;
298
+ const hmac = await crypto.createHmac('sha256', this.#secretKey);
299
+ const digest = await hmac.update(data);
300
+ const expectedSignature = await digest.digest('hex');
301
+
302
+ // Constant-time comparison (always perform)
303
+ const signaturesMatch = crypto.timingSafeEqual(expectedSignature, providedSignature);
304
+
305
+ // Check single-use (always check, even if other validations failed)
306
+ const alreadyUsed = options.rotateOnUse && stored?.used;
307
+
308
+ // Determine result (after all checks - constant time)
309
+ if (!isString) {
310
+ valid = false;
311
+ reason = 'MISSING_TOKEN';
312
+ } else if (!hasThreeParts) {
313
+ valid = false;
314
+ reason = 'INVALID_FORMAT';
315
+ } else if (!isTimestampValid) {
316
+ valid = false;
317
+ reason = 'INVALID_FORMAT';
318
+ } else if (isExpired) {
319
+ valid = false;
320
+ reason = 'EXPIRED';
321
+ expired = true;
322
+ // Remove expired token
323
+ if (stored) {
324
+ this.#tokens.delete(token);
325
+ }
326
+ } else if (!signaturesMatch) {
327
+ valid = false;
328
+ reason = 'INVALID_SIGNATURE';
329
+ } else if (!stored) {
330
+ valid = false;
331
+ reason = 'UNKNOWN_TOKEN';
332
+ } else if (alreadyUsed) {
333
+ valid = false;
334
+ reason = 'TOKEN_ALREADY_USED';
335
+ }
336
+
337
+ // Mark as used (only if valid and stored)
338
+ if (valid && stored) {
339
+ stored.used = true;
340
+ }
341
+
342
+ return { valid, reason, expired };
343
+ }
344
+
345
+ /**
346
+ * Invalidate a specific token
347
+ *
348
+ * @param {string} token - Token to invalidate
349
+ */
350
+ invalidate(token) {
351
+ this.#tokens.delete(token);
352
+ }
353
+
354
+ /**
355
+ * Cleanup expired tokens
356
+ *
357
+ * Removes tokens older than their expiration time.
358
+ * Called automatically on interval, but can be called manually.
359
+ *
360
+ * @returns {number} Number of tokens removed
361
+ */
362
+ cleanup() {
363
+ const now = Date.now();
364
+ let removed = 0;
365
+
366
+ for (const [token, { timestamp, expiresIn }] of this.#tokens.entries()) {
367
+ if (now - timestamp > expiresIn) {
368
+ this.#tokens.delete(token);
369
+ removed++;
370
+ }
371
+ }
372
+
373
+ return removed;
374
+ }
375
+
376
+ /**
377
+ * Clear all tokens (for testing)
378
+ */
379
+ clear() {
380
+ this.#tokens.clear();
381
+ }
382
+
383
+ /**
384
+ * Get token count
385
+ * @returns {number} Number of stored tokens
386
+ */
387
+ size() {
388
+ return this.#tokens.size;
389
+ }
390
+
391
+ /**
392
+ * Start automatic cleanup interval
393
+ * @private
394
+ */
395
+ #startCleanup() {
396
+ if (this.#cleanupInterval) {
397
+ clearInterval(this.#cleanupInterval);
398
+ }
399
+
400
+ this.#cleanupInterval = setInterval(() => {
401
+ this.cleanup();
402
+ }, this.#options.cleanupInterval);
403
+
404
+ // Don't prevent process from exiting
405
+ if (this.#cleanupInterval.unref) {
406
+ this.#cleanupInterval.unref();
407
+ }
408
+ }
409
+
410
+ /**
411
+ * Stop automatic cleanup (for cleanup on shutdown)
412
+ */
413
+ dispose() {
414
+ if (this.#cleanupInterval) {
415
+ clearInterval(this.#cleanupInterval);
416
+ this.#cleanupInterval = null;
417
+ }
418
+ }
419
+ }
420
+
421
+ // ============================================================================
422
+ // Convenience Functions
423
+ // ============================================================================
424
+
425
+ // Global store for convenience functions (shared across generate/validate calls)
426
+ let convenienceStore = null;
427
+ let convenienceSecret = null;
428
+
429
+ /**
430
+ * Generate CSRF token
431
+ *
432
+ * Note: Uses a shared global store for the given secret. This allows
433
+ * validation of tokens generated with this function.
434
+ *
435
+ * @param {string} secret - HMAC secret key (32+ bytes recommended)
436
+ * @param {Object} [options] - Generation options
437
+ * @param {number} [options.expiresIn=3600000] - Token expiration in ms
438
+ * @returns {Promise<string>} CSRF token
439
+ */
440
+ export async function generateCSRFToken(secret, options = {}) {
441
+ // Create or reuse store for this secret
442
+ if (!convenienceStore || convenienceSecret !== secret) {
443
+ if (convenienceStore) {
444
+ convenienceStore.dispose();
445
+ }
446
+ convenienceStore = new CSRFTokenStore({ secret, ...options });
447
+ convenienceSecret = secret;
448
+ }
449
+
450
+ return convenienceStore.generate(options);
451
+ }
452
+
453
+ /**
454
+ * Validate CSRF token
455
+ *
456
+ * Note: Uses a shared global store. Can validate tokens generated with
457
+ * generateCSRFToken() if the same secret is used.
458
+ *
459
+ * @param {string} token - Token to validate
460
+ * @param {string} secret - HMAC secret key
461
+ * @param {Object} [options] - Validation options
462
+ * @param {number} [options.expiresIn=3600000] - Token expiration in ms
463
+ * @returns {Promise<Object>} Validation result { valid: boolean, reason?: string }
464
+ */
465
+ export async function validateCSRFToken(token, secret, options = {}) {
466
+ // Create or reuse store for this secret
467
+ if (!convenienceStore || convenienceSecret !== secret) {
468
+ if (convenienceStore) {
469
+ convenienceStore.dispose();
470
+ }
471
+ convenienceStore = new CSRFTokenStore({ secret, ...options });
472
+ convenienceSecret = secret;
473
+ }
474
+
475
+ return convenienceStore.validate(token, options);
476
+ }
477
+
478
+ // ============================================================================
479
+ // CSRF Middleware Factory
480
+ // ============================================================================
481
+
482
+ /**
483
+ * Create CSRF middleware for Server Actions
484
+ *
485
+ * Validates CSRF tokens from incoming requests and optionally rotates them.
486
+ *
487
+ * @param {Object} options - Middleware options
488
+ * @param {string} [options.secret] - HMAC secret (auto-generated if not provided)
489
+ * @param {CSRFTokenStore} [options.store] - Custom token store
490
+ * @param {boolean} [options.enabled=true] - Enable/disable CSRF validation
491
+ * @param {number} [options.expiresIn=3600000] - Token expiration in ms
492
+ * @param {boolean} [options.rotateOnUse=false] - Generate new token after validation
493
+ * @param {string} [options.headerName='x-csrf-token'] - Request header name
494
+ * @param {string} [options.cookieName='csrf-token'] - Cookie name for double-submit
495
+ * @returns {Function} Middleware function
496
+ *
497
+ * @example
498
+ * // Basic usage
499
+ * const csrfMiddleware = createCSRFMiddleware({
500
+ * secret: process.env.CSRF_SECRET
501
+ * });
502
+ *
503
+ * // With token rotation
504
+ * const csrfMiddleware = createCSRFMiddleware({
505
+ * secret: process.env.CSRF_SECRET,
506
+ * rotateOnUse: true
507
+ * });
508
+ *
509
+ * // Custom store for multi-server deployments
510
+ * const store = new CSRFTokenStore({ secret: 'shared-secret' });
511
+ * const csrfMiddleware = createCSRFMiddleware({ store });
512
+ */
513
+ export function createCSRFMiddleware(options = {}) {
514
+ const {
515
+ secret = null,
516
+ store = null,
517
+ enabled = true,
518
+ expiresIn = 3600000,
519
+ rotateOnUse = false,
520
+ headerName = 'x-csrf-token',
521
+ cookieName = 'csrf-token',
522
+ secureCookie = true
523
+ } = options;
524
+
525
+ // Use provided store or create new one
526
+ const tokenStore = store || new CSRFTokenStore({ secret, expiresIn });
527
+
528
+ return async (req, res, next) => {
529
+ // Skip if disabled
530
+ if (!enabled) {
531
+ return next ? next() : { valid: true };
532
+ }
533
+
534
+ // Extract token from header
535
+ const token = req.headers?.[headerName] || req.get?.(headerName);
536
+
537
+ // Validate token
538
+ const validation = await tokenStore.validate(token, {
539
+ expiresIn,
540
+ rotateOnUse
541
+ });
542
+
543
+ if (!validation.valid) {
544
+ // CSRF validation failed
545
+ const error = {
546
+ error: 'CSRF validation failed',
547
+ reason: validation.reason,
548
+ code: 'PSC_CSRF_INVALID'
549
+ };
550
+
551
+ if (res) {
552
+ // Express/Fastify/Hono response
553
+ return res.status?.(403).json(error) || res.json?.(error, 403);
554
+ } else {
555
+ // Return error object for manual handling
556
+ return { valid: false, ...error };
557
+ }
558
+ }
559
+
560
+ // Rotate token if configured
561
+ if (rotateOnUse) {
562
+ const newToken = await tokenStore.generate({ expiresIn });
563
+
564
+ // Set new token in response header
565
+ if (res) {
566
+ res.setHeader?.('X-New-CSRF-Token', newToken);
567
+ res.set?.('X-New-CSRF-Token', newToken);
568
+
569
+ // Update cookie if using double-submit pattern
570
+ res.cookie?.(cookieName, newToken, {
571
+ httpOnly: false, // Client needs to read it
572
+ secure: secureCookie,
573
+ sameSite: 'strict',
574
+ maxAge: expiresIn
575
+ });
576
+ }
577
+ }
578
+
579
+ // Continue to next middleware or return success
580
+ return next ? next() : { valid: true };
581
+ };
582
+ }
583
+
584
+ // ============================================================================
585
+ // Exports
586
+ // ============================================================================
587
+
588
+ export default {
589
+ CSRFTokenStore,
590
+ generateCSRFToken,
591
+ validateCSRFToken,
592
+ createCSRFMiddleware
593
+ };