@veloxts/auth 0.6.68 → 0.6.70
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/CHANGELOG.md +87 -0
- package/dist/adapter.d.ts +10 -4
- package/dist/adapter.js +15 -9
- package/dist/adapters/index.d.ts +26 -6
- package/dist/adapters/index.js +25 -7
- package/dist/adapters/jwt-adapter.d.ts +261 -0
- package/dist/adapters/jwt-adapter.js +360 -0
- package/dist/decoration.d.ts +88 -0
- package/dist/decoration.js +112 -0
- package/dist/guards-narrowing.d.ts +123 -0
- package/dist/guards-narrowing.js +80 -0
- package/dist/index.d.ts +11 -8
- package/dist/index.js +8 -1
- package/dist/middleware.d.ts +4 -4
- package/dist/middleware.js +7 -1
- package/dist/plugin.d.ts +70 -5
- package/dist/plugin.js +172 -62
- package/dist/providers.js +3 -1
- package/dist/token-store.d.ts +105 -0
- package/dist/token-store.js +159 -0
- package/dist/types.d.ts +70 -33
- package/package.json +5 -5
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JWT Authentication Adapter for @veloxts/auth
|
|
3
|
+
*
|
|
4
|
+
* Implements the AuthAdapter interface using JWT tokens.
|
|
5
|
+
* This allows JWT auth to follow the same pattern as external providers,
|
|
6
|
+
* enabling easy swapping between authentication strategies.
|
|
7
|
+
*
|
|
8
|
+
* @module auth/adapters/jwt-adapter
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* import { createAuthAdapterPlugin } from '@veloxts/auth';
|
|
13
|
+
* import { createJwtAdapter, jwtAuth } from '@veloxts/auth/adapters/jwt-adapter';
|
|
14
|
+
*
|
|
15
|
+
* // Option 1: Using createJwtAdapter + createAuthAdapterPlugin
|
|
16
|
+
* const { adapter, config } = createJwtAdapter({
|
|
17
|
+
* jwt: {
|
|
18
|
+
* secret: process.env.JWT_SECRET!,
|
|
19
|
+
* accessTokenExpiry: '15m',
|
|
20
|
+
* refreshTokenExpiry: '7d',
|
|
21
|
+
* },
|
|
22
|
+
* userLoader: async (userId) => db.user.findUnique({ where: { id: userId } }),
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* const authPlugin = createAuthAdapterPlugin({ adapter, config });
|
|
26
|
+
* app.use(authPlugin);
|
|
27
|
+
*
|
|
28
|
+
* // Option 2: Using jwtAuth convenience function (recommended)
|
|
29
|
+
* import { jwtAuth } from '@veloxts/auth';
|
|
30
|
+
*
|
|
31
|
+
* app.use(jwtAuth({
|
|
32
|
+
* jwt: {
|
|
33
|
+
* secret: process.env.JWT_SECRET!,
|
|
34
|
+
* accessTokenExpiry: '15m',
|
|
35
|
+
* refreshTokenExpiry: '7d',
|
|
36
|
+
* },
|
|
37
|
+
* userLoader: async (userId) => db.user.findUnique({ where: { id: userId } }),
|
|
38
|
+
* }));
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
import { AuthAdapterError, BaseAuthAdapter } from '../adapter.js';
|
|
42
|
+
import { createInMemoryTokenStore, JwtManager } from '../jwt.js';
|
|
43
|
+
// ============================================================================
|
|
44
|
+
// JWT Adapter Implementation
|
|
45
|
+
// ============================================================================
|
|
46
|
+
/**
|
|
47
|
+
* JWT Authentication Adapter
|
|
48
|
+
*
|
|
49
|
+
* Implements the AuthAdapter interface using JWT tokens.
|
|
50
|
+
* Provides session loading from Authorization headers and
|
|
51
|
+
* optional routes for token refresh and logout.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```typescript
|
|
55
|
+
* const adapter = new JwtAdapter();
|
|
56
|
+
* await adapter.initialize(fastify, {
|
|
57
|
+
* name: 'jwt',
|
|
58
|
+
* jwt: { secret: process.env.JWT_SECRET! },
|
|
59
|
+
* });
|
|
60
|
+
*
|
|
61
|
+
* // Get session from request
|
|
62
|
+
* const session = await adapter.getSession(request);
|
|
63
|
+
* if (session) {
|
|
64
|
+
* console.log('User:', session.user.email);
|
|
65
|
+
* }
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export class JwtAdapter extends BaseAuthAdapter {
|
|
69
|
+
jwt = null;
|
|
70
|
+
tokenStore = null;
|
|
71
|
+
userLoader;
|
|
72
|
+
enableRoutes = true;
|
|
73
|
+
routePrefix = '/api/auth';
|
|
74
|
+
constructor() {
|
|
75
|
+
super('jwt', '1.0.0');
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Initialize the adapter with JWT configuration
|
|
79
|
+
*
|
|
80
|
+
* Sets up the JwtManager, token store, and configuration options.
|
|
81
|
+
* Also exposes `jwtManager` and `tokenStore` on the Fastify instance.
|
|
82
|
+
*/
|
|
83
|
+
async initialize(fastify, config) {
|
|
84
|
+
await super.initialize(fastify, config);
|
|
85
|
+
if (!config.jwt) {
|
|
86
|
+
throw new AuthAdapterError('JWT configuration is required in adapter config', 500, 'ADAPTER_NOT_CONFIGURED');
|
|
87
|
+
}
|
|
88
|
+
// Initialize JWT manager
|
|
89
|
+
this.jwt = new JwtManager(config.jwt);
|
|
90
|
+
/**
|
|
91
|
+
* Initialize token store (default: in-memory with warning)
|
|
92
|
+
*
|
|
93
|
+
* @example Production Redis store passed in config:
|
|
94
|
+
* ```typescript
|
|
95
|
+
* import { createRedisTokenStore } from '@veloxts/auth/redis';
|
|
96
|
+
* …
|
|
97
|
+
* tokenStore: createRedisTokenStore({ url: process.env.REDIS_URL })
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
this.tokenStore = config.tokenStore ?? createInMemoryTokenStore();
|
|
101
|
+
if (!config.tokenStore) {
|
|
102
|
+
this.debug('Using in-memory token store. Use Redis in production.');
|
|
103
|
+
}
|
|
104
|
+
this.userLoader = config.userLoader;
|
|
105
|
+
this.enableRoutes = config.enableRoutes ?? true;
|
|
106
|
+
this.routePrefix = config.routePrefix ?? '/api/auth';
|
|
107
|
+
// Expose JWT manager and token store on fastify for direct access
|
|
108
|
+
if (!fastify.hasDecorator('jwtManager')) {
|
|
109
|
+
fastify.decorate('jwtManager', this.jwt);
|
|
110
|
+
}
|
|
111
|
+
if (!fastify.hasDecorator('tokenStore')) {
|
|
112
|
+
fastify.decorate('tokenStore', this.tokenStore);
|
|
113
|
+
}
|
|
114
|
+
this.info('JWT adapter initialized');
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Get session from JWT token in Authorization header
|
|
118
|
+
*
|
|
119
|
+
* Extracts and verifies the JWT token, checks revocation status,
|
|
120
|
+
* and loads the user if a userLoader is configured.
|
|
121
|
+
*
|
|
122
|
+
* @returns Session result with user and session data, or null if not authenticated
|
|
123
|
+
*/
|
|
124
|
+
async getSession(request) {
|
|
125
|
+
if (!this.jwt || !this.tokenStore) {
|
|
126
|
+
throw new AuthAdapterError('JWT adapter not initialized', 500, 'ADAPTER_NOT_CONFIGURED');
|
|
127
|
+
}
|
|
128
|
+
// Extract token from Authorization header
|
|
129
|
+
const authHeader = request.headers.authorization;
|
|
130
|
+
const token = this.jwt.extractFromHeader(authHeader);
|
|
131
|
+
if (!token) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
// Verify token
|
|
136
|
+
const payload = this.jwt.verifyToken(token);
|
|
137
|
+
// Check for access token type
|
|
138
|
+
if (payload.type !== 'access') {
|
|
139
|
+
this.debug('Non-access token in Authorization header');
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
// Check if token is revoked
|
|
143
|
+
if (payload.jti) {
|
|
144
|
+
const isRevoked = await this.tokenStore.isRevoked(payload.jti);
|
|
145
|
+
if (isRevoked) {
|
|
146
|
+
this.debug('Token has been revoked');
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// Load user if loader provided
|
|
151
|
+
let user;
|
|
152
|
+
if (this.userLoader) {
|
|
153
|
+
user = await this.userLoader(payload.sub);
|
|
154
|
+
if (!user) {
|
|
155
|
+
this.debug('User not found for token');
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
user = { id: payload.sub, email: payload.email };
|
|
161
|
+
}
|
|
162
|
+
// Store token and payload on request for middleware access
|
|
163
|
+
// Using type assertion to avoid modifying Fastify types
|
|
164
|
+
const requestWithJwt = request;
|
|
165
|
+
requestWithJwt.__jwtToken = token;
|
|
166
|
+
requestWithJwt.__jwtPayload = payload;
|
|
167
|
+
return {
|
|
168
|
+
user: {
|
|
169
|
+
id: user.id,
|
|
170
|
+
email: user.email,
|
|
171
|
+
name: user.name,
|
|
172
|
+
emailVerified: user.emailVerified,
|
|
173
|
+
providerData: { roles: user.roles, permissions: user.permissions },
|
|
174
|
+
},
|
|
175
|
+
session: {
|
|
176
|
+
sessionId: payload.jti ?? `jwt-${payload.sub}`,
|
|
177
|
+
userId: payload.sub,
|
|
178
|
+
expiresAt: payload.exp * 1000, // Convert to ms
|
|
179
|
+
isActive: true,
|
|
180
|
+
providerData: { token, payload },
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
this.debug(`Token verification failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Get routes for token refresh and logout
|
|
191
|
+
*
|
|
192
|
+
* Returns routes only if `enableRoutes` is true in config.
|
|
193
|
+
*/
|
|
194
|
+
getRoutes() {
|
|
195
|
+
if (!this.enableRoutes) {
|
|
196
|
+
return [];
|
|
197
|
+
}
|
|
198
|
+
return [
|
|
199
|
+
// Refresh token endpoint
|
|
200
|
+
{
|
|
201
|
+
path: `${this.routePrefix}/refresh`,
|
|
202
|
+
methods: ['POST'],
|
|
203
|
+
handler: this.handleRefresh.bind(this),
|
|
204
|
+
description: 'Refresh access token using refresh token',
|
|
205
|
+
},
|
|
206
|
+
// Logout endpoint (revoke token)
|
|
207
|
+
{
|
|
208
|
+
path: `${this.routePrefix}/logout`,
|
|
209
|
+
methods: ['POST'],
|
|
210
|
+
handler: this.handleLogout.bind(this),
|
|
211
|
+
description: 'Revoke current access token',
|
|
212
|
+
},
|
|
213
|
+
];
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Handle token refresh requests
|
|
217
|
+
*
|
|
218
|
+
* Expects `refreshToken` in request body.
|
|
219
|
+
* Returns new token pair on success.
|
|
220
|
+
*/
|
|
221
|
+
async handleRefresh(request, reply) {
|
|
222
|
+
if (!this.jwt) {
|
|
223
|
+
throw new AuthAdapterError('JWT adapter not initialized', 500, 'ADAPTER_NOT_CONFIGURED');
|
|
224
|
+
}
|
|
225
|
+
const body = request.body;
|
|
226
|
+
const refreshToken = body?.refreshToken;
|
|
227
|
+
if (!refreshToken) {
|
|
228
|
+
reply.status(400).send({ error: 'Missing refreshToken in request body' });
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
const tokens = await this.jwt.refreshTokens(refreshToken, this.userLoader);
|
|
233
|
+
reply.send(tokens);
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
reply.status(401).send({
|
|
237
|
+
error: error instanceof Error ? error.message : 'Token refresh failed',
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Handle logout requests
|
|
243
|
+
*
|
|
244
|
+
* Revokes the current access token by adding its JTI to the token store.
|
|
245
|
+
* The token is extracted from the Authorization header.
|
|
246
|
+
*/
|
|
247
|
+
async handleLogout(request, reply) {
|
|
248
|
+
if (!this.tokenStore) {
|
|
249
|
+
throw new AuthAdapterError('JWT adapter not initialized', 500, 'ADAPTER_NOT_CONFIGURED');
|
|
250
|
+
}
|
|
251
|
+
// Get payload from request (set during getSession in preHandler)
|
|
252
|
+
const requestWithJwt = request;
|
|
253
|
+
const payload = requestWithJwt.__jwtPayload;
|
|
254
|
+
if (payload?.jti) {
|
|
255
|
+
await this.tokenStore.revoke(payload.jti);
|
|
256
|
+
this.debug(`Token ${payload.jti} revoked`);
|
|
257
|
+
}
|
|
258
|
+
reply.status(200).send({ success: true });
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Clean up adapter resources
|
|
262
|
+
*/
|
|
263
|
+
async cleanup() {
|
|
264
|
+
await super.cleanup();
|
|
265
|
+
this.jwt = null;
|
|
266
|
+
this.tokenStore = null;
|
|
267
|
+
this.userLoader = undefined;
|
|
268
|
+
this.info('JWT adapter cleaned up');
|
|
269
|
+
}
|
|
270
|
+
// ============================================================================
|
|
271
|
+
// Public API Methods
|
|
272
|
+
// ============================================================================
|
|
273
|
+
/**
|
|
274
|
+
* Create a token pair for a user
|
|
275
|
+
*
|
|
276
|
+
* Convenience method that delegates to the underlying JwtManager.
|
|
277
|
+
* Can be accessed via `fastify.jwtManager.createTokenPair()` as well.
|
|
278
|
+
*
|
|
279
|
+
* @param user - The user to create tokens for
|
|
280
|
+
* @param additionalClaims - Custom claims to include in the token
|
|
281
|
+
* @returns Token pair with access and refresh tokens
|
|
282
|
+
*
|
|
283
|
+
* @example
|
|
284
|
+
* ```typescript
|
|
285
|
+
* const tokens = adapter.createTokenPair(user);
|
|
286
|
+
* // { accessToken, refreshToken, expiresIn, tokenType }
|
|
287
|
+
* ```
|
|
288
|
+
*/
|
|
289
|
+
createTokenPair(user, additionalClaims) {
|
|
290
|
+
if (!this.jwt) {
|
|
291
|
+
throw new AuthAdapterError('JWT adapter not initialized', 500, 'ADAPTER_NOT_CONFIGURED');
|
|
292
|
+
}
|
|
293
|
+
return this.jwt.createTokenPair(user, additionalClaims);
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Get the underlying JwtManager instance
|
|
297
|
+
*
|
|
298
|
+
* Useful for advanced token operations.
|
|
299
|
+
*/
|
|
300
|
+
getJwtManager() {
|
|
301
|
+
if (!this.jwt) {
|
|
302
|
+
throw new AuthAdapterError('JWT adapter not initialized', 500, 'ADAPTER_NOT_CONFIGURED');
|
|
303
|
+
}
|
|
304
|
+
return this.jwt;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Get the token store instance
|
|
308
|
+
*
|
|
309
|
+
* Useful for manual token revocation.
|
|
310
|
+
*/
|
|
311
|
+
getTokenStore() {
|
|
312
|
+
if (!this.tokenStore) {
|
|
313
|
+
throw new AuthAdapterError('JWT adapter not initialized', 500, 'ADAPTER_NOT_CONFIGURED');
|
|
314
|
+
}
|
|
315
|
+
return this.tokenStore;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
// ============================================================================
|
|
319
|
+
// Factory Function
|
|
320
|
+
// ============================================================================
|
|
321
|
+
/**
|
|
322
|
+
* Create a JWT auth adapter
|
|
323
|
+
*
|
|
324
|
+
* This is the recommended way to create a JWT adapter for use with
|
|
325
|
+
* createAuthAdapterPlugin. Returns both the adapter instance and
|
|
326
|
+
* the configuration for convenience.
|
|
327
|
+
*
|
|
328
|
+
* @param config - JWT adapter configuration (without name, which is auto-set to 'jwt')
|
|
329
|
+
* @returns Object with adapter and config
|
|
330
|
+
*
|
|
331
|
+
* @example
|
|
332
|
+
* ```typescript
|
|
333
|
+
* import { createJwtAdapter } from '@veloxts/auth/adapters/jwt-adapter';
|
|
334
|
+
* import { createAuthAdapterPlugin } from '@veloxts/auth';
|
|
335
|
+
*
|
|
336
|
+
* const { adapter, config } = createJwtAdapter({
|
|
337
|
+
* jwt: {
|
|
338
|
+
* secret: process.env.JWT_SECRET!,
|
|
339
|
+
* accessTokenExpiry: '15m',
|
|
340
|
+
* refreshTokenExpiry: '7d',
|
|
341
|
+
* },
|
|
342
|
+
* userLoader: async (userId) => db.user.findUnique({ where: { id: userId } }),
|
|
343
|
+
* });
|
|
344
|
+
*
|
|
345
|
+
* const authPlugin = createAuthAdapterPlugin({ adapter, config });
|
|
346
|
+
* app.use(authPlugin);
|
|
347
|
+
* ```
|
|
348
|
+
*/
|
|
349
|
+
export function createJwtAdapter(config) {
|
|
350
|
+
const adapter = new JwtAdapter();
|
|
351
|
+
const fullConfig = {
|
|
352
|
+
name: 'jwt',
|
|
353
|
+
...config,
|
|
354
|
+
};
|
|
355
|
+
return { adapter, config: fullConfig };
|
|
356
|
+
}
|
|
357
|
+
// ============================================================================
|
|
358
|
+
// Re-exports
|
|
359
|
+
// ============================================================================
|
|
360
|
+
export { AuthAdapterError } from '../adapter.js';
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared decoration utilities for @veloxts/auth
|
|
3
|
+
*
|
|
4
|
+
* This module provides common functionality for decorating Fastify instances
|
|
5
|
+
* and requests with authentication state, shared between the native auth plugin
|
|
6
|
+
* and external auth adapters.
|
|
7
|
+
*
|
|
8
|
+
* @module auth/decoration
|
|
9
|
+
*/
|
|
10
|
+
import type { FastifyInstance, FastifyRequest } from 'fastify';
|
|
11
|
+
import type { AuthContext, User } from './types.js';
|
|
12
|
+
/**
|
|
13
|
+
* Symbol used to mark a Fastify instance as having auth already registered.
|
|
14
|
+
*
|
|
15
|
+
* This prevents double-registration of conflicting auth systems (e.g., using
|
|
16
|
+
* both authPlugin and an AuthAdapter on the same server).
|
|
17
|
+
*/
|
|
18
|
+
export declare const AUTH_REGISTERED: unique symbol;
|
|
19
|
+
/**
|
|
20
|
+
* Checks for double-registration of auth systems and throws if detected.
|
|
21
|
+
*
|
|
22
|
+
* Call this at the start of both `authPlugin` and `createAuthAdapterPlugin`
|
|
23
|
+
* registration to ensure only one auth system is active.
|
|
24
|
+
*
|
|
25
|
+
* @param fastify - Fastify server instance
|
|
26
|
+
* @param source - Identifier for the auth system being registered (e.g., 'authPlugin', 'adapter:better-auth')
|
|
27
|
+
* @throws {Error} If auth has already been registered by another source
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```typescript
|
|
31
|
+
* // In authPlugin registration
|
|
32
|
+
* checkDoubleRegistration(fastify, 'authPlugin');
|
|
33
|
+
*
|
|
34
|
+
* // In adapter plugin registration
|
|
35
|
+
* checkDoubleRegistration(fastify, `adapter:${adapter.name}`);
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare function checkDoubleRegistration(fastify: FastifyInstance, source: string): void;
|
|
39
|
+
/**
|
|
40
|
+
* Decorates a Fastify instance with auth-related request decorators.
|
|
41
|
+
*
|
|
42
|
+
* This function safely adds `auth` and `user` properties to requests,
|
|
43
|
+
* checking if they already exist (idempotent operation).
|
|
44
|
+
*
|
|
45
|
+
* @param fastify - Fastify server instance to decorate
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```typescript
|
|
49
|
+
* decorateAuth(fastify);
|
|
50
|
+
* // Now all requests will have request.auth and request.user available
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export declare function decorateAuth(fastify: FastifyInstance): void;
|
|
54
|
+
/**
|
|
55
|
+
* Sets the auth context and user on a request.
|
|
56
|
+
*
|
|
57
|
+
* This is a type-safe helper that properly casts the request to include
|
|
58
|
+
* auth properties before setting them.
|
|
59
|
+
*
|
|
60
|
+
* @param request - Fastify request object
|
|
61
|
+
* @param auth - Auth context to set
|
|
62
|
+
* @param user - User to set (optional, defaults to auth.user if NativeAuthContext)
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```typescript
|
|
66
|
+
* setRequestAuth(request, {
|
|
67
|
+
* authMode: 'native',
|
|
68
|
+
* isAuthenticated: true,
|
|
69
|
+
* token: tokenPayload,
|
|
70
|
+
* payload: tokenPayload,
|
|
71
|
+
* }, user);
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export declare function setRequestAuth(request: FastifyRequest, auth: AuthContext, user?: User): void;
|
|
75
|
+
/**
|
|
76
|
+
* Gets the current auth context from a request.
|
|
77
|
+
*
|
|
78
|
+
* @param request - Fastify request object
|
|
79
|
+
* @returns The auth context, or undefined if not set
|
|
80
|
+
*/
|
|
81
|
+
export declare function getRequestAuth(request: FastifyRequest): AuthContext | undefined;
|
|
82
|
+
/**
|
|
83
|
+
* Gets the current user from a request.
|
|
84
|
+
*
|
|
85
|
+
* @param request - Fastify request object
|
|
86
|
+
* @returns The user, or undefined if not authenticated
|
|
87
|
+
*/
|
|
88
|
+
export declare function getRequestUser(request: FastifyRequest): User | undefined;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared decoration utilities for @veloxts/auth
|
|
3
|
+
*
|
|
4
|
+
* This module provides common functionality for decorating Fastify instances
|
|
5
|
+
* and requests with authentication state, shared between the native auth plugin
|
|
6
|
+
* and external auth adapters.
|
|
7
|
+
*
|
|
8
|
+
* @module auth/decoration
|
|
9
|
+
*/
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Registration Protection
|
|
12
|
+
// ============================================================================
|
|
13
|
+
/**
|
|
14
|
+
* Symbol used to mark a Fastify instance as having auth already registered.
|
|
15
|
+
*
|
|
16
|
+
* This prevents double-registration of conflicting auth systems (e.g., using
|
|
17
|
+
* both authPlugin and an AuthAdapter on the same server).
|
|
18
|
+
*/
|
|
19
|
+
export const AUTH_REGISTERED = Symbol.for('@veloxts/auth/registered');
|
|
20
|
+
/**
|
|
21
|
+
* Checks for double-registration of auth systems and throws if detected.
|
|
22
|
+
*
|
|
23
|
+
* Call this at the start of both `authPlugin` and `createAuthAdapterPlugin`
|
|
24
|
+
* registration to ensure only one auth system is active.
|
|
25
|
+
*
|
|
26
|
+
* @param fastify - Fastify server instance
|
|
27
|
+
* @param source - Identifier for the auth system being registered (e.g., 'authPlugin', 'adapter:better-auth')
|
|
28
|
+
* @throws {Error} If auth has already been registered by another source
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```typescript
|
|
32
|
+
* // In authPlugin registration
|
|
33
|
+
* checkDoubleRegistration(fastify, 'authPlugin');
|
|
34
|
+
*
|
|
35
|
+
* // In adapter plugin registration
|
|
36
|
+
* checkDoubleRegistration(fastify, `adapter:${adapter.name}`);
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export function checkDoubleRegistration(fastify, source) {
|
|
40
|
+
const decorated = fastify;
|
|
41
|
+
if (decorated[AUTH_REGISTERED]) {
|
|
42
|
+
throw new Error(`Auth already registered by "${decorated[AUTH_REGISTERED]}". ` +
|
|
43
|
+
`Cannot register "${source}". ` +
|
|
44
|
+
`Use either authPlugin OR an AuthAdapter, not both.`);
|
|
45
|
+
}
|
|
46
|
+
decorated[AUTH_REGISTERED] = source;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Decorates a Fastify instance with auth-related request decorators.
|
|
50
|
+
*
|
|
51
|
+
* This function safely adds `auth` and `user` properties to requests,
|
|
52
|
+
* checking if they already exist (idempotent operation).
|
|
53
|
+
*
|
|
54
|
+
* @param fastify - Fastify server instance to decorate
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```typescript
|
|
58
|
+
* decorateAuth(fastify);
|
|
59
|
+
* // Now all requests will have request.auth and request.user available
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export function decorateAuth(fastify) {
|
|
63
|
+
if (!fastify.hasRequestDecorator('auth')) {
|
|
64
|
+
fastify.decorateRequest('auth', undefined);
|
|
65
|
+
}
|
|
66
|
+
if (!fastify.hasRequestDecorator('user')) {
|
|
67
|
+
fastify.decorateRequest('user', undefined);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Sets the auth context and user on a request.
|
|
72
|
+
*
|
|
73
|
+
* This is a type-safe helper that properly casts the request to include
|
|
74
|
+
* auth properties before setting them.
|
|
75
|
+
*
|
|
76
|
+
* @param request - Fastify request object
|
|
77
|
+
* @param auth - Auth context to set
|
|
78
|
+
* @param user - User to set (optional, defaults to auth.user if NativeAuthContext)
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```typescript
|
|
82
|
+
* setRequestAuth(request, {
|
|
83
|
+
* authMode: 'native',
|
|
84
|
+
* isAuthenticated: true,
|
|
85
|
+
* token: tokenPayload,
|
|
86
|
+
* payload: tokenPayload,
|
|
87
|
+
* }, user);
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
export function setRequestAuth(request, auth, user) {
|
|
91
|
+
const decoratedRequest = request;
|
|
92
|
+
decoratedRequest.auth = auth;
|
|
93
|
+
decoratedRequest.user = user ?? (auth.isAuthenticated && 'user' in auth ? auth.user : undefined);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Gets the current auth context from a request.
|
|
97
|
+
*
|
|
98
|
+
* @param request - Fastify request object
|
|
99
|
+
* @returns The auth context, or undefined if not set
|
|
100
|
+
*/
|
|
101
|
+
export function getRequestAuth(request) {
|
|
102
|
+
return request.auth;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Gets the current user from a request.
|
|
106
|
+
*
|
|
107
|
+
* @param request - Fastify request object
|
|
108
|
+
* @returns The user, or undefined if not authenticated
|
|
109
|
+
*/
|
|
110
|
+
export function getRequestUser(request) {
|
|
111
|
+
return request.user;
|
|
112
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Narrowing Guards (Experimental)
|
|
3
|
+
*
|
|
4
|
+
* These guards provide TypeScript type narrowing after they pass.
|
|
5
|
+
* When using `guardNarrow(authenticatedNarrow)`, the context type
|
|
6
|
+
* is narrowed to guarantee `ctx.user` is non-null.
|
|
7
|
+
*
|
|
8
|
+
* EXPERIMENTAL: This API may change. The current recommended approach
|
|
9
|
+
* is to use middleware for context type extension.
|
|
10
|
+
*
|
|
11
|
+
* @module auth/guards-narrowing
|
|
12
|
+
*/
|
|
13
|
+
import type { AuthContext, GuardFunction, User } from './types.js';
|
|
14
|
+
/**
|
|
15
|
+
* A guard that narrows the context type after passing.
|
|
16
|
+
*
|
|
17
|
+
* The `_narrows` phantom type indicates what the guard guarantees
|
|
18
|
+
* about the context after it passes.
|
|
19
|
+
*
|
|
20
|
+
* @template TRequired - Context properties required to run the guard
|
|
21
|
+
* @template TGuaranteed - Context properties guaranteed after guard passes
|
|
22
|
+
*/
|
|
23
|
+
export interface NarrowingGuard<TRequired, TGuaranteed> {
|
|
24
|
+
/** Guard name for error messages */
|
|
25
|
+
name: string;
|
|
26
|
+
/** Guard check function (matches GuardFunction signature) */
|
|
27
|
+
check: GuardFunction<TRequired>;
|
|
28
|
+
/** Custom error message */
|
|
29
|
+
message?: string;
|
|
30
|
+
/** HTTP status code for guard failures */
|
|
31
|
+
statusCode?: number;
|
|
32
|
+
/**
|
|
33
|
+
* Phantom type declaring what the guard guarantees.
|
|
34
|
+
* Used by ProcedureBuilder.guardNarrow() for type narrowing.
|
|
35
|
+
* @internal
|
|
36
|
+
*/
|
|
37
|
+
readonly _narrows: TGuaranteed;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Context type with a guaranteed authenticated user.
|
|
41
|
+
*
|
|
42
|
+
* After `authenticatedNarrow` passes, the context is narrowed to this type.
|
|
43
|
+
*/
|
|
44
|
+
export interface AuthenticatedContext {
|
|
45
|
+
auth: AuthContext & {
|
|
46
|
+
isAuthenticated: true;
|
|
47
|
+
};
|
|
48
|
+
user: User;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Context type with a guaranteed user having specific roles.
|
|
52
|
+
*/
|
|
53
|
+
export interface RoleNarrowedContext {
|
|
54
|
+
user: User & {
|
|
55
|
+
roles: string[];
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Authenticated guard with type narrowing.
|
|
60
|
+
*
|
|
61
|
+
* When used with `guardNarrow()`, narrows `ctx.user` from `User | undefined`
|
|
62
|
+
* to `User`, eliminating the need for null checks in the handler.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```typescript
|
|
66
|
+
* import { authenticatedNarrow } from '@veloxts/auth';
|
|
67
|
+
*
|
|
68
|
+
* // With guardNarrow (experimental):
|
|
69
|
+
* procedure()
|
|
70
|
+
* .guardNarrow(authenticatedNarrow)
|
|
71
|
+
* .query(({ ctx }) => {
|
|
72
|
+
* // ctx.user is typed as User (non-null)
|
|
73
|
+
* return { email: ctx.user.email };
|
|
74
|
+
* });
|
|
75
|
+
*
|
|
76
|
+
* // Current recommended alternative using middleware:
|
|
77
|
+
* procedure()
|
|
78
|
+
* .guard(authenticated)
|
|
79
|
+
* .use(async ({ ctx, next }) => {
|
|
80
|
+
* if (!ctx.user) throw new Error('Unreachable');
|
|
81
|
+
* return next({ ctx: { user: ctx.user } });
|
|
82
|
+
* })
|
|
83
|
+
* .query(({ ctx }) => {
|
|
84
|
+
* // ctx.user is non-null via middleware
|
|
85
|
+
* });
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export declare const authenticatedNarrow: NarrowingGuard<{
|
|
89
|
+
auth?: AuthContext;
|
|
90
|
+
}, AuthenticatedContext>;
|
|
91
|
+
/**
|
|
92
|
+
* Creates a role-checking guard with type narrowing.
|
|
93
|
+
*
|
|
94
|
+
* Narrows `ctx.user` to guarantee non-null with roles array.
|
|
95
|
+
*
|
|
96
|
+
* @param roles - Required role(s)
|
|
97
|
+
* @returns NarrowingGuard that guarantees user with roles
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```typescript
|
|
101
|
+
* import { hasRoleNarrow } from '@veloxts/auth';
|
|
102
|
+
*
|
|
103
|
+
* procedure()
|
|
104
|
+
* .guardNarrow(hasRoleNarrow('admin'))
|
|
105
|
+
* .mutation(({ ctx }) => {
|
|
106
|
+
* // ctx.user is typed as User (non-null)
|
|
107
|
+
* // ctx.user.roles is string[]
|
|
108
|
+
* });
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
export declare function hasRoleNarrow(roles: string | string[]): NarrowingGuard<{
|
|
112
|
+
user?: User;
|
|
113
|
+
}, RoleNarrowedContext>;
|
|
114
|
+
/**
|
|
115
|
+
* Extracts the narrowed context type from a NarrowingGuard.
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* ```typescript
|
|
119
|
+
* type Ctx = InferNarrowedContext<typeof authenticatedNarrow>;
|
|
120
|
+
* // Ctx = AuthenticatedContext = { auth: AuthContext & { isAuthenticated: true }; user: User }
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
export type InferNarrowedContext<T> = T extends NarrowingGuard<unknown, infer U> ? U : never;
|