@veloxts/auth 0.3.3 → 0.3.4

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 (54) hide show
  1. package/README.md +755 -30
  2. package/dist/adapter.d.ts +710 -0
  3. package/dist/adapter.d.ts.map +1 -0
  4. package/dist/adapter.js +581 -0
  5. package/dist/adapter.js.map +1 -0
  6. package/dist/adapters/better-auth.d.ts +271 -0
  7. package/dist/adapters/better-auth.d.ts.map +1 -0
  8. package/dist/adapters/better-auth.js +341 -0
  9. package/dist/adapters/better-auth.js.map +1 -0
  10. package/dist/adapters/index.d.ts +28 -0
  11. package/dist/adapters/index.d.ts.map +1 -0
  12. package/dist/adapters/index.js +28 -0
  13. package/dist/adapters/index.js.map +1 -0
  14. package/dist/csrf.d.ts +294 -0
  15. package/dist/csrf.d.ts.map +1 -0
  16. package/dist/csrf.js +396 -0
  17. package/dist/csrf.js.map +1 -0
  18. package/dist/guards.d.ts +139 -0
  19. package/dist/guards.d.ts.map +1 -0
  20. package/dist/guards.js +247 -0
  21. package/dist/guards.js.map +1 -0
  22. package/dist/hash.d.ts +85 -0
  23. package/dist/hash.d.ts.map +1 -0
  24. package/dist/hash.js +220 -0
  25. package/dist/hash.js.map +1 -0
  26. package/dist/index.d.ts +25 -32
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +63 -36
  29. package/dist/index.js.map +1 -1
  30. package/dist/jwt.d.ts +128 -0
  31. package/dist/jwt.d.ts.map +1 -0
  32. package/dist/jwt.js +363 -0
  33. package/dist/jwt.js.map +1 -0
  34. package/dist/middleware.d.ts +87 -0
  35. package/dist/middleware.d.ts.map +1 -0
  36. package/dist/middleware.js +241 -0
  37. package/dist/middleware.js.map +1 -0
  38. package/dist/plugin.d.ts +107 -0
  39. package/dist/plugin.d.ts.map +1 -0
  40. package/dist/plugin.js +174 -0
  41. package/dist/plugin.js.map +1 -0
  42. package/dist/policies.d.ts +137 -0
  43. package/dist/policies.d.ts.map +1 -0
  44. package/dist/policies.js +240 -0
  45. package/dist/policies.js.map +1 -0
  46. package/dist/session.d.ts +494 -0
  47. package/dist/session.d.ts.map +1 -0
  48. package/dist/session.js +795 -0
  49. package/dist/session.js.map +1 -0
  50. package/dist/types.d.ts +251 -0
  51. package/dist/types.d.ts.map +1 -0
  52. package/dist/types.js +33 -0
  53. package/dist/types.js.map +1 -0
  54. package/package.json +38 -7
package/dist/csrf.d.ts ADDED
@@ -0,0 +1,294 @@
1
+ /**
2
+ * CSRF (Cross-Site Request Forgery) Protection for @veloxts/auth
3
+ *
4
+ * Implements the Signed Double Submit Cookie Pattern:
5
+ * - Stateless design (no server-side session storage)
6
+ * - HMAC-signed tokens prevent cookie tampering
7
+ * - Horizontally scalable across server instances
8
+ *
9
+ * @module auth/csrf
10
+ */
11
+ import type { BaseContext } from '@veloxts/core';
12
+ import type { MiddlewareFunction } from '@veloxts/router';
13
+ import type { FastifyReply, FastifyRequest } from 'fastify';
14
+ interface CookieSerializeOptions {
15
+ domain?: string;
16
+ path?: string;
17
+ sameSite?: 'strict' | 'lax' | 'none' | boolean;
18
+ secure?: boolean;
19
+ httpOnly?: boolean;
20
+ maxAge?: number;
21
+ expires?: Date;
22
+ }
23
+ interface FastifyReplyWithCookies extends FastifyReply {
24
+ cookie(name: string, value: string, options?: CookieSerializeOptions): FastifyReply;
25
+ clearCookie(name: string, options?: CookieSerializeOptions): FastifyReply;
26
+ }
27
+ interface FastifyRequestWithCookies extends FastifyRequest {
28
+ cookies: Record<string, string | undefined>;
29
+ }
30
+ /**
31
+ * CSRF validation failure codes
32
+ */
33
+ export type CsrfErrorCode = 'CSRF_MISSING_TOKEN' | 'CSRF_MISSING_COOKIE' | 'CSRF_TOKEN_MISMATCH' | 'CSRF_INVALID_SIGNATURE' | 'CSRF_TOKEN_EXPIRED' | 'CSRF_ORIGIN_MISMATCH' | 'CSRF_INVALID_FORMAT';
34
+ /**
35
+ * CSRF-specific error class
36
+ */
37
+ export declare class CsrfError extends Error {
38
+ readonly statusCode: number;
39
+ readonly code: CsrfErrorCode;
40
+ constructor(message: string, code: CsrfErrorCode);
41
+ }
42
+ /**
43
+ * Cookie configuration for CSRF tokens
44
+ */
45
+ export interface CsrfCookieConfig {
46
+ /**
47
+ * Cookie name for the CSRF token
48
+ * @default 'velox.csrf'
49
+ */
50
+ name?: string;
51
+ /**
52
+ * Cookie path
53
+ * @default '/'
54
+ */
55
+ path?: string;
56
+ /**
57
+ * SameSite policy
58
+ * - 'strict': Only same-site requests (most secure)
59
+ * - 'lax': Same-site + top-level navigation from external sites
60
+ * - 'none': All requests (requires Secure; use only with CORS)
61
+ * @default 'lax'
62
+ */
63
+ sameSite?: 'strict' | 'lax' | 'none';
64
+ /**
65
+ * Require HTTPS (should be true in production)
66
+ * @default process.env.NODE_ENV === 'production'
67
+ */
68
+ secure?: boolean;
69
+ /**
70
+ * HttpOnly flag - set to false for header-based CSRF
71
+ * When false: JavaScript can read token for header submission
72
+ * When true: Use hidden form fields only
73
+ * @default false
74
+ */
75
+ httpOnly?: boolean;
76
+ /**
77
+ * Domain for the cookie (optional)
78
+ */
79
+ domain?: string;
80
+ }
81
+ /**
82
+ * Token generation and validation configuration
83
+ */
84
+ export interface CsrfTokenConfig {
85
+ /**
86
+ * Token entropy in bytes (random portion)
87
+ * @default 32 (256 bits)
88
+ */
89
+ tokenBytes?: number;
90
+ /**
91
+ * Token expiration in seconds (0 for no expiration)
92
+ * @default 3600 (1 hour)
93
+ */
94
+ expiresIn?: number;
95
+ /**
96
+ * Secret key for HMAC signature
97
+ * Minimum length: 32 characters
98
+ */
99
+ secret: string;
100
+ }
101
+ /**
102
+ * Request validation configuration
103
+ */
104
+ export interface CsrfValidationConfig {
105
+ /**
106
+ * Header name to check for token
107
+ * @default 'x-csrf-token'
108
+ */
109
+ headerName?: string;
110
+ /**
111
+ * Body field name to check for token
112
+ * @default '_csrf'
113
+ */
114
+ bodyFieldName?: string;
115
+ /**
116
+ * Query parameter name (disabled by default)
117
+ */
118
+ queryFieldName?: string;
119
+ /**
120
+ * HTTP methods to validate
121
+ * @default ['POST', 'PUT', 'PATCH', 'DELETE']
122
+ */
123
+ methods?: ReadonlyArray<string>;
124
+ /**
125
+ * Paths to exclude from CSRF validation
126
+ * @example [/^\/api\/webhooks\//, '/health']
127
+ */
128
+ excludePaths?: ReadonlyArray<RegExp | string>;
129
+ /**
130
+ * Validate Origin/Referer headers
131
+ * @default true
132
+ */
133
+ checkOrigin?: boolean;
134
+ /**
135
+ * Allowed origins for validation
136
+ */
137
+ allowedOrigins?: ReadonlyArray<string>;
138
+ }
139
+ /**
140
+ * Complete CSRF configuration
141
+ */
142
+ export interface CsrfConfig {
143
+ /**
144
+ * Enable CSRF protection
145
+ * @default true
146
+ */
147
+ enabled?: boolean;
148
+ /**
149
+ * Cookie configuration
150
+ */
151
+ cookie?: CsrfCookieConfig;
152
+ /**
153
+ * Token configuration
154
+ */
155
+ token: CsrfTokenConfig;
156
+ /**
157
+ * Validation configuration
158
+ */
159
+ validation?: CsrfValidationConfig;
160
+ }
161
+ /**
162
+ * Parsed CSRF token structure
163
+ */
164
+ export interface CsrfTokenData {
165
+ /** Random token value (base64url encoded) */
166
+ value: string;
167
+ /** Token creation timestamp (Unix seconds) */
168
+ issuedAt: number;
169
+ /** Token expiration timestamp (Unix seconds) */
170
+ expiresAt: number;
171
+ /** HMAC signature (base64url encoded) */
172
+ signature: string;
173
+ }
174
+ /**
175
+ * Token generation result
176
+ */
177
+ export interface CsrfTokenResult {
178
+ /** Complete signed token string */
179
+ token: string;
180
+ /** Token expiration timestamp */
181
+ expiresAt: number;
182
+ }
183
+ /**
184
+ * CSRF token manager for generating and validating tokens
185
+ */
186
+ export interface CsrfManager {
187
+ /** Generate a new CSRF token */
188
+ generateToken(reply: FastifyReplyWithCookies): CsrfTokenResult;
189
+ /** Validate a CSRF token from request */
190
+ validateToken(request: FastifyRequestWithCookies): void;
191
+ /** Extract token from request */
192
+ extractToken(request: FastifyRequest): string | null;
193
+ /** Parse token string into components */
194
+ parseToken(token: string): CsrfTokenData | null;
195
+ /** Verify token signature */
196
+ verifySignature(token: string): boolean;
197
+ /** Clear the CSRF cookie */
198
+ clearCookie(reply: FastifyReplyWithCookies): void;
199
+ }
200
+ /**
201
+ * CSRF middleware options for procedures
202
+ */
203
+ export interface CsrfMiddlewareOptions {
204
+ /**
205
+ * Skip CSRF validation for this procedure
206
+ * @default false
207
+ */
208
+ skip?: boolean;
209
+ /**
210
+ * Override excluded paths for this middleware instance
211
+ */
212
+ excludePaths?: ReadonlyArray<RegExp | string>;
213
+ }
214
+ /**
215
+ * Extended context with CSRF capabilities
216
+ */
217
+ export interface CsrfContext {
218
+ csrf: {
219
+ /** Generate a new token */
220
+ generateToken: () => CsrfTokenResult;
221
+ /** Current token from request (if valid) */
222
+ token?: string;
223
+ };
224
+ }
225
+ declare module '@veloxts/core' {
226
+ interface BaseContext {
227
+ csrf?: CsrfContext['csrf'];
228
+ }
229
+ }
230
+ declare module 'fastify' {
231
+ interface FastifyRequest {
232
+ csrfToken?: string;
233
+ }
234
+ }
235
+ /**
236
+ * Creates a CSRF token manager
237
+ *
238
+ * @example
239
+ * ```typescript
240
+ * const csrfManager = createCsrfManager({
241
+ * token: { secret: process.env.CSRF_SECRET! },
242
+ * cookie: { secure: true, sameSite: 'strict' },
243
+ * });
244
+ *
245
+ * // Generate token
246
+ * const { token } = csrfManager.generateToken(reply);
247
+ *
248
+ * // Validate token
249
+ * csrfManager.validateToken(request); // Throws on failure
250
+ * ```
251
+ */
252
+ export declare function createCsrfManager(config: CsrfConfig): CsrfManager;
253
+ /**
254
+ * Creates CSRF protection middleware for procedures
255
+ *
256
+ * @example
257
+ * ```typescript
258
+ * const csrf = createCsrfMiddleware({
259
+ * token: { secret: process.env.CSRF_SECRET! },
260
+ * });
261
+ *
262
+ * // Protect mutations
263
+ * const createPost = procedure()
264
+ * .use(auth.requireAuth())
265
+ * .use(csrf.protect())
266
+ * .input(CreatePostSchema)
267
+ * .mutation(async ({ input, ctx }) => {
268
+ * return db.post.create({ data: input });
269
+ * });
270
+ *
271
+ * // Provide token for forms
272
+ * const getForm = procedure()
273
+ * .use(csrf.provide())
274
+ * .query(async ({ ctx }) => {
275
+ * return { csrfToken: ctx.csrf.generateToken().token };
276
+ * });
277
+ * ```
278
+ */
279
+ export declare function createCsrfMiddleware(config: CsrfConfig): {
280
+ /** CSRF manager instance */
281
+ manager: CsrfManager;
282
+ /** Protection middleware (validates tokens) */
283
+ protect: <TInput, TContext extends BaseContext, TOutput>(options?: CsrfMiddlewareOptions) => MiddlewareFunction<TInput, TContext, TContext & CsrfContext, TOutput>;
284
+ /** Provider middleware (generates tokens only) */
285
+ provide: <TInput, TContext extends BaseContext, TOutput>() => MiddlewareFunction<TInput, TContext, TContext & CsrfContext, TOutput>;
286
+ /** Generate token directly */
287
+ generateToken: (reply: FastifyReplyWithCookies) => CsrfTokenResult;
288
+ /** Validate token directly */
289
+ validateToken: (request: FastifyRequestWithCookies) => void;
290
+ /** Clear CSRF cookie */
291
+ clearCookie: (reply: FastifyReplyWithCookies) => void;
292
+ };
293
+ export {};
294
+ //# sourceMappingURL=csrf.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"csrf.d.ts","sourceRoot":"","sources":["../src/csrf.ts"],"names":[],"mappings":"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"}
package/dist/csrf.js ADDED
@@ -0,0 +1,396 @@
1
+ /**
2
+ * CSRF (Cross-Site Request Forgery) Protection for @veloxts/auth
3
+ *
4
+ * Implements the Signed Double Submit Cookie Pattern:
5
+ * - Stateless design (no server-side session storage)
6
+ * - HMAC-signed tokens prevent cookie tampering
7
+ * - Horizontally scalable across server instances
8
+ *
9
+ * @module auth/csrf
10
+ */
11
+ import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
12
+ // ============================================================================
13
+ // Constants
14
+ // ============================================================================
15
+ const DEFAULT_TOKEN_BYTES = 32;
16
+ const DEFAULT_EXPIRES_IN = 3600; // 1 hour
17
+ const DEFAULT_HEADER_NAME = 'x-csrf-token';
18
+ const DEFAULT_BODY_FIELD = '_csrf';
19
+ const DEFAULT_COOKIE_NAME = 'velox.csrf';
20
+ const MIN_SECRET_LENGTH = 32;
21
+ const CSRF_METHODS = ['POST', 'PUT', 'PATCH', 'DELETE'];
22
+ /**
23
+ * CSRF-specific error class
24
+ */
25
+ export class CsrfError extends Error {
26
+ statusCode = 403;
27
+ code;
28
+ constructor(message, code) {
29
+ super(message);
30
+ this.name = 'CsrfError';
31
+ this.code = code;
32
+ Error.captureStackTrace?.(this, CsrfError);
33
+ }
34
+ }
35
+ // ============================================================================
36
+ // Helper Functions
37
+ // ============================================================================
38
+ /**
39
+ * Base64url encode
40
+ */
41
+ function base64urlEncode(data) {
42
+ return data.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
43
+ }
44
+ // ============================================================================
45
+ // CSRF Manager Implementation
46
+ // ============================================================================
47
+ /**
48
+ * Creates a CSRF token manager
49
+ *
50
+ * @example
51
+ * ```typescript
52
+ * const csrfManager = createCsrfManager({
53
+ * token: { secret: process.env.CSRF_SECRET! },
54
+ * cookie: { secure: true, sameSite: 'strict' },
55
+ * });
56
+ *
57
+ * // Generate token
58
+ * const { token } = csrfManager.generateToken(reply);
59
+ *
60
+ * // Validate token
61
+ * csrfManager.validateToken(request); // Throws on failure
62
+ * ```
63
+ */
64
+ export function createCsrfManager(config) {
65
+ // Validate secret
66
+ const secret = config.token.secret;
67
+ if (!secret || secret.length < MIN_SECRET_LENGTH) {
68
+ throw new Error(`CSRF secret must be at least ${MIN_SECRET_LENGTH} characters. ` +
69
+ 'Generate with: openssl rand -base64 32');
70
+ }
71
+ // Resolve configuration with defaults
72
+ const tokenBytes = config.token.tokenBytes ?? DEFAULT_TOKEN_BYTES;
73
+ const expiresIn = config.token.expiresIn ?? DEFAULT_EXPIRES_IN;
74
+ const headerName = (config.validation?.headerName ?? DEFAULT_HEADER_NAME).toLowerCase();
75
+ const bodyFieldName = config.validation?.bodyFieldName ?? DEFAULT_BODY_FIELD;
76
+ const queryFieldName = config.validation?.queryFieldName;
77
+ const methods = config.validation?.methods ?? CSRF_METHODS;
78
+ const checkOrigin = config.validation?.checkOrigin ?? true;
79
+ const allowedOrigins = config.validation?.allowedOrigins ?? [];
80
+ const excludePaths = config.validation?.excludePaths ?? [];
81
+ // Cookie config
82
+ const cookieName = config.cookie?.name ?? DEFAULT_COOKIE_NAME;
83
+ const cookiePath = config.cookie?.path ?? '/';
84
+ const cookieSameSite = config.cookie?.sameSite ?? 'lax';
85
+ const cookieSecure = config.cookie?.secure ?? process.env.NODE_ENV === 'production';
86
+ const cookieHttpOnly = config.cookie?.httpOnly ?? false;
87
+ const cookieDomain = config.cookie?.domain;
88
+ // Security validation: SameSite=none requires Secure flag
89
+ // Per RFC 6265bis, cookies with SameSite=none must be Secure
90
+ if (cookieSameSite === 'none' && !cookieSecure) {
91
+ throw new Error('CSRF cookie with SameSite=none requires Secure flag. ' +
92
+ 'Set cookie.secure: true or use a different SameSite policy.');
93
+ }
94
+ /**
95
+ * Create HMAC signature for token data
96
+ */
97
+ function createSignature(value, issuedAt, expiresAt) {
98
+ const hmac = createHmac('sha256', secret);
99
+ hmac.update(`${value}.${issuedAt}.${expiresAt}`);
100
+ return base64urlEncode(hmac.digest());
101
+ }
102
+ /**
103
+ * Generate a new CSRF token
104
+ */
105
+ function generateToken(reply) {
106
+ const value = base64urlEncode(randomBytes(tokenBytes));
107
+ const issuedAt = Math.floor(Date.now() / 1000);
108
+ const expiresAt = expiresIn > 0 ? issuedAt + expiresIn : 0;
109
+ const signature = createSignature(value, issuedAt, expiresAt);
110
+ // Token format: value.issuedAt.expiresAt.signature
111
+ const token = `${value}.${issuedAt}.${expiresAt}.${signature}`;
112
+ // Set cookie using Fastify's cookie API
113
+ reply.cookie(cookieName, token, {
114
+ path: cookiePath,
115
+ sameSite: cookieSameSite,
116
+ secure: cookieSecure,
117
+ httpOnly: cookieHttpOnly,
118
+ domain: cookieDomain,
119
+ maxAge: expiresIn > 0 ? expiresIn : undefined,
120
+ });
121
+ return { token, expiresAt };
122
+ }
123
+ /**
124
+ * Parse token string into components
125
+ */
126
+ function parseToken(token) {
127
+ const parts = token.split('.');
128
+ if (parts.length !== 4) {
129
+ return null;
130
+ }
131
+ const [value, issuedAtStr, expiresAtStr, signature] = parts;
132
+ const issuedAt = parseInt(issuedAtStr, 10);
133
+ const expiresAt = parseInt(expiresAtStr, 10);
134
+ if (Number.isNaN(issuedAt) || Number.isNaN(expiresAt)) {
135
+ return null;
136
+ }
137
+ return { value, issuedAt, expiresAt, signature };
138
+ }
139
+ /**
140
+ * Verify token signature using timing-safe comparison
141
+ */
142
+ function verifySignature(token) {
143
+ const parsed = parseToken(token);
144
+ if (!parsed) {
145
+ return false;
146
+ }
147
+ const expectedSignature = createSignature(parsed.value, parsed.issuedAt, parsed.expiresAt);
148
+ const sigBuffer = Buffer.from(parsed.signature, 'utf8');
149
+ const expectedBuffer = Buffer.from(expectedSignature, 'utf8');
150
+ if (sigBuffer.length !== expectedBuffer.length) {
151
+ return false;
152
+ }
153
+ return timingSafeEqual(sigBuffer, expectedBuffer);
154
+ }
155
+ /**
156
+ * Extract token from request (header, body, or query)
157
+ */
158
+ function extractToken(request) {
159
+ // 1. Check header
160
+ const headerToken = request.headers[headerName];
161
+ if (typeof headerToken === 'string' && headerToken.length > 0) {
162
+ return headerToken;
163
+ }
164
+ // 2. Check body
165
+ const body = request.body;
166
+ if (body && typeof body[bodyFieldName] === 'string') {
167
+ return body[bodyFieldName];
168
+ }
169
+ // 3. Check query (if enabled)
170
+ if (queryFieldName) {
171
+ const query = request.query;
172
+ if (query && typeof query[queryFieldName] === 'string') {
173
+ return query[queryFieldName];
174
+ }
175
+ }
176
+ return null;
177
+ }
178
+ /**
179
+ * Validate Origin/Referer headers
180
+ */
181
+ function validateOrigin(request) {
182
+ if (!checkOrigin) {
183
+ return;
184
+ }
185
+ const origin = request.headers.origin;
186
+ const referer = request.headers.referer;
187
+ const host = request.headers.host;
188
+ // If no origin or referer, might be same-origin
189
+ if (!origin && !referer) {
190
+ return;
191
+ }
192
+ let requestOrigin = null;
193
+ if (origin) {
194
+ requestOrigin = origin;
195
+ }
196
+ else if (referer) {
197
+ try {
198
+ requestOrigin = new URL(referer).origin;
199
+ }
200
+ catch {
201
+ // Invalid referer URL
202
+ }
203
+ }
204
+ if (!requestOrigin) {
205
+ return;
206
+ }
207
+ // Check against host
208
+ const protocol = request.protocol ?? 'http';
209
+ const expectedOrigin = `${protocol}://${host}`;
210
+ if (requestOrigin === expectedOrigin) {
211
+ return;
212
+ }
213
+ // Check against allowed origins
214
+ if (allowedOrigins.includes(requestOrigin)) {
215
+ return;
216
+ }
217
+ throw new CsrfError(`Origin mismatch: ${requestOrigin} not allowed`, 'CSRF_ORIGIN_MISMATCH');
218
+ }
219
+ /**
220
+ * Check if path should be excluded
221
+ */
222
+ function isPathExcluded(path) {
223
+ for (const pattern of excludePaths) {
224
+ if (typeof pattern === 'string') {
225
+ if (path === pattern)
226
+ return true;
227
+ }
228
+ else if (pattern.test(path)) {
229
+ return true;
230
+ }
231
+ }
232
+ return false;
233
+ }
234
+ /**
235
+ * Full token validation
236
+ */
237
+ function validateToken(request) {
238
+ // Skip non-mutating methods
239
+ const method = request.method.toUpperCase();
240
+ if (!methods.includes(method)) {
241
+ return;
242
+ }
243
+ // Check excluded paths
244
+ const path = request.url.split('?')[0];
245
+ if (isPathExcluded(path)) {
246
+ return;
247
+ }
248
+ // Validate origin first
249
+ validateOrigin(request);
250
+ // Get cookie token
251
+ const cookieToken = request.cookies[cookieName];
252
+ if (!cookieToken) {
253
+ throw new CsrfError('CSRF cookie not found', 'CSRF_MISSING_COOKIE');
254
+ }
255
+ // Get request token
256
+ const requestToken = extractToken(request);
257
+ if (!requestToken) {
258
+ throw new CsrfError('CSRF token not found in request', 'CSRF_MISSING_TOKEN');
259
+ }
260
+ // Tokens must match (double-submit validation)
261
+ // Use timing-safe comparison to prevent timing attacks
262
+ const cookieBuffer = Buffer.from(cookieToken, 'utf8');
263
+ const requestBuffer = Buffer.from(requestToken, 'utf8');
264
+ if (cookieBuffer.length !== requestBuffer.length ||
265
+ !timingSafeEqual(cookieBuffer, requestBuffer)) {
266
+ throw new CsrfError('CSRF token mismatch', 'CSRF_TOKEN_MISMATCH');
267
+ }
268
+ // Verify signature
269
+ if (!verifySignature(requestToken)) {
270
+ throw new CsrfError('Invalid CSRF token signature', 'CSRF_INVALID_SIGNATURE');
271
+ }
272
+ // Check expiration
273
+ const parsed = parseToken(requestToken);
274
+ if (parsed && parsed.expiresAt > 0) {
275
+ const now = Math.floor(Date.now() / 1000);
276
+ if (parsed.expiresAt < now) {
277
+ throw new CsrfError('CSRF token has expired', 'CSRF_TOKEN_EXPIRED');
278
+ }
279
+ }
280
+ }
281
+ /**
282
+ * Clear CSRF cookie
283
+ */
284
+ function clearCookie(reply) {
285
+ reply.clearCookie(cookieName, {
286
+ path: cookiePath,
287
+ domain: cookieDomain,
288
+ });
289
+ }
290
+ return {
291
+ generateToken,
292
+ validateToken,
293
+ extractToken,
294
+ parseToken,
295
+ verifySignature,
296
+ clearCookie,
297
+ };
298
+ }
299
+ // ============================================================================
300
+ // Middleware Factory
301
+ // ============================================================================
302
+ /**
303
+ * Creates CSRF protection middleware for procedures
304
+ *
305
+ * @example
306
+ * ```typescript
307
+ * const csrf = createCsrfMiddleware({
308
+ * token: { secret: process.env.CSRF_SECRET! },
309
+ * });
310
+ *
311
+ * // Protect mutations
312
+ * const createPost = procedure()
313
+ * .use(auth.requireAuth())
314
+ * .use(csrf.protect())
315
+ * .input(CreatePostSchema)
316
+ * .mutation(async ({ input, ctx }) => {
317
+ * return db.post.create({ data: input });
318
+ * });
319
+ *
320
+ * // Provide token for forms
321
+ * const getForm = procedure()
322
+ * .use(csrf.provide())
323
+ * .query(async ({ ctx }) => {
324
+ * return { csrfToken: ctx.csrf.generateToken().token };
325
+ * });
326
+ * ```
327
+ */
328
+ export function createCsrfMiddleware(config) {
329
+ const manager = createCsrfManager(config);
330
+ /**
331
+ * Middleware that validates CSRF tokens on mutations
332
+ */
333
+ function protect(options = {}) {
334
+ return async ({ ctx, next }) => {
335
+ const reply = ctx.reply;
336
+ const request = ctx.request;
337
+ if (options.skip) {
338
+ return next({
339
+ ctx: {
340
+ ...ctx,
341
+ csrf: {
342
+ generateToken: () => manager.generateToken(reply),
343
+ token: undefined,
344
+ },
345
+ },
346
+ });
347
+ }
348
+ // Validate token (throws CsrfError on failure)
349
+ manager.validateToken(request);
350
+ const token = manager.extractToken(ctx.request) ?? undefined;
351
+ // Continue with CSRF context
352
+ return next({
353
+ ctx: {
354
+ ...ctx,
355
+ csrf: {
356
+ generateToken: () => manager.generateToken(reply),
357
+ token,
358
+ },
359
+ },
360
+ });
361
+ };
362
+ }
363
+ /**
364
+ * Middleware that only provides token generation (no validation)
365
+ * Use for query procedures where you need to provide tokens
366
+ */
367
+ function provide() {
368
+ return async ({ ctx, next }) => {
369
+ const reply = ctx.reply;
370
+ return next({
371
+ ctx: {
372
+ ...ctx,
373
+ csrf: {
374
+ generateToken: () => manager.generateToken(reply),
375
+ token: manager.extractToken(ctx.request) ?? undefined,
376
+ },
377
+ },
378
+ });
379
+ };
380
+ }
381
+ return {
382
+ /** CSRF manager instance */
383
+ manager,
384
+ /** Protection middleware (validates tokens) */
385
+ protect,
386
+ /** Provider middleware (generates tokens only) */
387
+ provide,
388
+ /** Generate token directly */
389
+ generateToken: (reply) => manager.generateToken(reply),
390
+ /** Validate token directly */
391
+ validateToken: (request) => manager.validateToken(request),
392
+ /** Clear CSRF cookie */
393
+ clearCookie: (reply) => manager.clearCookie(reply),
394
+ };
395
+ }
396
+ //# sourceMappingURL=csrf.js.map